设计模式
设计模式
1.单一职责原则
单一职责原则:一个类或者模块只负责完成一个职责(或者功能)
一个类只负责完成一个职责或者功能。也就是说,不要设计大而全的类,要设计粒度小、功能单一的类。换个角度来讲就是,一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成多个功能更加单一、粒度更细的类。
评价一个类的职责是否足够单一,我们并没有一个非常明确的、可以量化的标准,可以说,这是件非常主观、仁者见仁智者见智的事情。实际上,在真正的软件开发中,我们也没必要过于未雨绸缪,过度设计。所以, 我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类。这就是所谓的持续重构(后面的章节中我们会讲到)。
2.开闭原则
“对扩展开放、对修改关闭”。添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
怎么实现“对扩展开放、对修改关闭”?
在写代码的时候后,我们要多花点时间往前多思考一下,这段代码未来可能有哪些需求变更、如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,不需要改动代码整体结构、做到最小代码改动的情况下,新的代码能够很灵活地插入到扩展点上,做到“对扩展开放、对修改关闭”。
我们还要善于识别代码中的可变部分和不可变部分,把可变部分封装
0.设计模式的划分
1.1 根据模式的目的划分
根据模式是用来完成什么样的工作来划分,这种方法可分为创建型模式、结构型模式、行为型模式3种。
1.1.1 创建型设计模式
用于描述“怎么创建对象”。它的主要特点是“将对象的创建与使用分离”。如,单例、原型、工厂方法、抽象工厂、建造者等5种创建型模式。
创建型设计模式主要解决对象的创建问题,封装复杂的创建过程,以及解耦对象的创建代码和使用代码。其中,单例模式用来创建全局唯一的对象;工厂模式用来创建类型不同但相关的对象(继承同一父类或接口的一组子类),由给定的参数来决定创建哪种类型的对象:建造者模式用来创建复杂的对象,可以通过设置不同的可选参数,定制化地创建不同的对象;
1.1.2 结构型模式
用于描述“如何将类或对象按某种布局组成更大的结构”。如,代理、适配器、桥接、装饰、外观、享元、组合等7种结构型模式。
1.1.3 行为型模式
用于描述“类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责”。如,模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录模式、解释器等11中行为模式。
1.2 根据模式的作用划分
根据模式的主要用于类上还是主要用户对象上来分,这种方式可分为类模式和对象模式两种。
1.2.1 类模式
用于处理类与子类之间的关系,这些关系通过继承来建立,是静态的,在编译时便确定下来了。如,工厂方法、(类)适配器、模板方法、解释器等4种类模式。
1.2.2 对象模式
用户处理对象之间关系的,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。
类模式 | 工厂方法 | (类)适配器 | 模板方法 解释器 |
---|---|---|---|
对象模式 | 单例 原型 抽象工厂 建造者 | 代理 (对象)适配器 桥接 装饰 外观 享元 组合 | 策略 命令 职责链 状态 观察者 中介者 迭代器 访问者 备忘录 |
1. 工厂方法模式
1.什么是工厂模式?
工厂模式是一种创建型设计模式,它通过定义一个创建对象的接口来封装实例化对象的行为,让子类决定实例化哪一个类。
工厂模式的概念来源于传统的工厂生产流程,其主要目的是将对象的创建和使用分离,使得客户端不需要知道具体的产品类,只需通过工厂请求所需的产品即可。这样做的好处在于增加了系统的灵活性和可维护性,同时能够降低代码的耦合度。
2.工厂模式的具体作用
具体到工厂模式的作用,主要体现在以下几个方面:
- 解耦对象创建和使用过程:客户端代码不直接调用构造函数来创建对象,而是通过工厂类的接口来获取所需的对象,这样可以减少客户端与具体类之间的依赖关系。
- 降低重复代码:如果多个地方需要创建同一个复杂对象,可以通过工厂方法统一管理,减少重复的代码,并便于后期维护。
- 增强系统的扩展性:当系统需要新增产品时,只需要扩展新的工厂类而不用修改原有代码,符合开闭原则。
- 隐藏具体实现:客户端只关心产品的接口而不是具体的实现,使得在不改变客户端代码的情况下更换或者升级产品成为可能。
- 提高代码的可管理性和灵活性:通过工厂模式,可以方便地管理和切换不同的产品实现,适应业务需求变化。
总之,工厂模式不仅有助于构建清晰、易于维护和扩展的代码结构,而且能够在多产品或变体间提供灵活的切换机制,是面向对象设计中常用的一种高效设计模式。
将创建代码抽离出来,让原本的函数或类职责更单一,代码更简洁。封装复杂的创建逻辑,调用者无需了解如何创建对象。
3.工厂模式的优缺点
工厂模式,特别是在软件工程中,主要目的是创建对象,同时将对象的创建过程和使用过程解耦。它包括几种不同的变体,如简单工厂、工厂方法、抽象工厂等。
工厂模式的优点主要包括:
- 封装性:客户端代码不需要知道如何创建所需的对象,只需传递正确的参数即可获取需要的对象。
- 灵活性和可维护性:当添加新产品时,只需扩展工厂类而不必修改客户端代码,这符合开闭原则,即对扩展开放,对修改封闭。
- 隔离变化:工厂模式把对象创建过程中易变的部分隔离起来,有助于控制变化。
然而,工厂模式也有一些缺点:
- 复杂性增加:由于引入了工厂类(在简单工厂模式中),系统的复杂性有所增加。尤其是在简单工厂模式中,如果产品类层次结构很复杂,那么工厂类的职责会变得沉重,因为它需要包含所有产品的创建逻辑。
- 违反单一职责原则:简单工厂模式可能会违反单一职责原则,因为工厂类既要负责创建对象,又要包含业务逻辑判断。
- 扩展困难:每次新增或者删除产品时,可能需要修改工厂类的代码,尤其是在简单工厂模式中,这一点尤为明显。
综上所述,工厂模式通过封装和隔离对象创建的细节,提供了一种灵活且易于维护的方式来管理对象的生命周期。但同时,它也增加了系统的复杂性并可能限制了其扩展性。在应用工厂模式时,应根据具体场景仔细考量这些优缺点,以实现最佳的设计决策。
单一职责原则
单一职责原则(Single Responsibility Principle,简称SRP)是面向对象编程中的一个重要设计原则。它的核心思想是:一个类或者模块应该只负责一项职责,如果有多个职责,就应该拆分成多个类或模块。
这个原则的主要目的是为了降低代码的复杂性,提高代码的可读性和可维护性。如果一个类承担了过多的职责,那么当其中一个职责发生变化时,可能会影响到其他的职责,这样就增加了代码的耦合度,降低了代码的可维护性。
例如,假设有一个类叫做“Employee”,它既负责处理员工的工资计算,又负责处理员工的考勤记录。那么当工资计算的规则发生变化时,可能会影响到考勤记录的功能,反之亦然。如果我们按照单一职责原则来设计,就应该将“Employee”类拆分为两个类,一个负责工资计算,一个负责考勤记录,这样就可以降低代码的耦合度,提高代码的可维护性。
开闭原则
开闭原则:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。
4.3种工厂模式
三种工厂
- 简单工厂模式
- 工厂方法模式
- 抽象工厂模式
1.2 简单工厂模式
简单工厂不是一种设计模式,反而比较像是一种编程习惯。把对象的
1.2.1 结构创建交给工厂类
简单工厂包含如下角色:
- 抽象产品 :定义了产品的规范,描述了产品的主要特性和功能。
- 具体产品 :实现或者继承抽象产品的子类
- 具体工厂 :提供了创建产品的方法,调用者通过该方法来获取产品。
1.2.2 实现
现在使用简单工厂对上面案例进行改进,类图如下:
1 | interface AbstractProduct{ |
工厂(factory)处理创建对象的细节,一旦有了SimpleCoffeeFactory,CoffeeStore类中的orderCoffee()就变成此对象的客户,后期如果需要Coffee对象直接从工厂中获取即可。这样也就解除了和Coffee实现类(具体产品)的耦合,同时又产生了新的耦合,CoffeeStore对象和SimpleCoffeeFactory工厂对象的耦合,工厂对象和商品对象的耦合。
后期如果再加新品种的咖啡,我们势必要需求修改SimpleCoffeeFactory的代码,违反了开闭原则。工厂类的客户端可能有很多,比如创建美团外卖等,这样只需要修改工厂类的代码,省去其他的修改操作。
1.2.3 优缺点
优点:
封装了创建对象的过程,可以通过参数直接获取对象。把对象的创建和业务逻辑层分开,这样以后就避免了修改客户代码,如果要实现新产品直接修改工厂类,而不需要在原代码中修改,这样就降低了客户代码修改的可能性,更加容易扩展。
缺点:
增加新产品时还是需要修改工厂类的代码,违背了“开闭原则”。
1.3 工厂方法模式
针对上例中的缺点,使用工厂方法模式就可以完美的解决,完全遵循开闭原则。
1.3.1 概念
定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象。工厂方法使一个产品类的实例化延迟到其工厂的子类。具体工厂只用来创建具体产品
1.3.2 结构
工厂方法模式的主要角色:
- 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。
- 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。
1.3.3 实现
使用工厂方法模式对上例进行改进,类图如下:
流程:
代码如下:
1 | interface AbstractProduct{ |
从以上的编写的代码可以看到,要增加产品类时也要相应地增加工厂类,不需要修改工厂类的代码了,这样就解决了简单工厂模式的缺点。
工厂方法模式是简单工厂模式的进一步抽象。由于使用了多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。
1.3.4 优缺点
优点:
- 用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程;
- 在系统增加新的产品时只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行任何修改,满足开闭原则;
缺点:
- 每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度。
1.4 抽象工厂模式
本节要介绍的抽象工厂模式将考虑多等级产品的生产,将同一个具体工厂所生产的位于不同等级的一组产品称为一个产品族,下图所示
- 产品族:一个品牌下面的所有产品;例如华为下面的电脑、手机称为华为的产品族;
- 产品等级:多个品牌下面的同种产品;例如华为和小米都有手机电脑为一个产品等级;
1.4.1 概念
是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。
抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。
一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂
1.4.2 结构
抽象工厂模式的主要角色如下:
- 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品。
- 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它 同具体工厂之间是多对一的关系。
1.4.4 优缺点
优点:
当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
缺点:
当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。
1.5 Spring中使用到的工厂设计模式
Spring使用工厂模式可以通过 BeanFactory
或 ApplicationContext
创建 bean 对象。
spring中的BeanFactory用的是工厂模式里面的简单工厂模式(由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类),根据传入一个唯一的标识来获得bean对象
两者对比:
BeanFactory
:延迟注入(使用到某个/ bean 的时候才会注入),相比于ApplicationContext
来说会占用更少的内存,程序启动速度更快。ApplicationContext
:容器启动的时候,不管你用没用到,一次性创建所有 bean 。BeanFactory
仅提供了最基本的依赖注入支持,ApplicationContext
扩展了BeanFactory
,除了有BeanFactory
的功能之外还有额外更多功能,所以一般开发人员使用ApplicationContext
会更多。
2 策略模式
见:设计模式之策略模式 | Java学习&面试指南-程序员大彬 (topjavaer.cn)
1.什么是策略模式?
策略模式是一种行为型设计模式,用于在运行时选择不同的算法或者策略。
策略模式的核心思想是将一系列的算法或行为封装起来,使得它们可以相互替换。这种模式主要有三个组成部分:抽象策略类(Strategy),具体策略类(Concrete Strategy),以及环境类(Context)。抽象策略类定义了一个公共接口,让所有具体策略类实现这个接口,确保它们可以互换使用。具体策略类实现了抽象策略中定义的接口,提供具体的算法实现。环境类则用来维护对某个具体策略对象的引用,并在需要的时候调用其算法。
2.策略模式的作用
策略模式的主要用途包括封装多种算法、动态切换算法、避免使用多重条件转移语句、提供管理相关的操作等。具体内容如下:
- 封装多种算法:当系统需要支持多种算法或行为,且需要在运行时根据不同情况选择合适的算法时,可以使用策略模式来避免硬编码这些选择逻辑。
- 动态切换算法:策略模式允许客户端在运行时根据不同的情况动态地切换算法或行为。这一点特别适用于那些在不同条件下行为差异很大的场合。
- 避免使用多重条件转移语句:通过策略模式,可以将一系列相关的条件分支逻辑封装在不同的策略类中,从而减少原有业务代码中复杂的条件判断。
- 提供管理相关的操作:策略模式不仅分离了算法的定义和使用,还提供了一种途径来管理相关的操作,比如获取当前正在使用的策略或切换到另一种策略。
2.策略模式角色组成
策略模式主要由这三个角色组成,环境角色(Context)、抽象策略角色(Strategy)和具体策略角色(ConcreteStrategy)。
- 环境角色(Context):也叫做上下文角色, 起承上启下封装作用, 屏蔽高层模块对策略、 算法的直接访问,封装可能存在的变化。
- 抽象策略角色(Strategy):这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
- 具体策略角色(ConcreteStrategy):包装了相关的算法或行为。
3.具体代码
1 | //**抽象策略角色**(Strategy) |
测试:
1 | public class StrategyClient { |
4.使用策略模式简化多重if-else场景
假设我们要处理一个office文件,分为三种类型 docx、xlsx、pptx,分别表示Word文件、Excel文件、PPT文件,根据文件后缀分别解析。
常规写法:
1 | public class OfficeHandler { |
处理逻辑全部放在一个类中,会导致整个类特别庞大,假设我们要新增一种类型处理,比如对于2007版之前的office文件,后缀分别是 doc/xls/ppt,那我们得增加 else if 逻辑,违反了开闭原则,如何解决这种问题呢,答案就是通过策略模式。
1 | public interface OfficeHandlerStrategy { |
4.策略模式优缺点
策略模式作为一种设计模式,它的优势和局限性都是相对明显的。以下是策略模式的优缺点:
优点:
- 开闭原则:策略模式支持开闭原则,意味着系统可以在不修改原有代码的基础上引入新的策略类,实现新的行为或算法。这有助于系统维护和扩展。
- 避免条件语句:通过策略模式,可以将多重条件语句替换为对策略对象的调用,这样可以减少条件判断,使系统更加清晰易读。
- 自由切换算法:策略模式允许客户端在运行时根据不同情况选择或切换算法,提高了系统的灵活性。
- 良好的扩展性:由于策略模式的结构清晰,新策略的添加通常不会影响其他代码,这使得系统具有良好的扩展性。
- 管理算法族:如果系统中存在多个相似算法,策略模式可以通过继承机制来共享代码,减少重复代码。
缺点:
- 策略类数量增加:每增加一个策略,就需要增加一个具体的策略类,可能导致类的数量增多。
- 对外暴露策略:所有的策略类都需要对外暴露,这可能会增加系统的复杂性。
在实际使用策略模式时,需要权衡其优势和局限性,确保它适用于当前的应用场景。例如,在有多算法或行为需要在不同场景下互换使用的情况下,策略模式是一个不错的选择。同时,也要注意控制策略类的数量,避免系统过于复杂难以维护。
5.策略模式应用场景
策略模式用于实现算法的互换,使得算法可以独立于客户端独立变化。以下是该模式的具体作用:
- 行为切换:当系统存在多个类,且它们的主要区别在于行为时,策略模式允许客户端在运行时动态选择需要的行为。这有助于避免使用多重继承来处理不同的行为,简化了类的设计与维护。
- 算法选择:策略模式适用于那些需要从多种算法中动态选择一种来执行的情况。例如,根据用户的不同需求,可能需要选择不同的排序或支付算法。这种模式使得算法可以独立于客户端进行变更和扩展,符合开闭原则。
3 责任链设计模式
1.什么是责任链模式?
责任链模式是一种行为型设计模式,允许多个对象按照顺序处理请求,每个对象可以自行决定是否处理或将请求传递给链上的下一个对象。
责任链模式通过创建一系列处理对象,每个对象都有机会处理请求。这些对象连成一条链,请求沿着这条链传递,直到某个对象处理它为止。在责任链模式中,通常包含了以下角色:
- 抽象处理者(Handler):定义处理请求的接口,并持有对下一个处理者的引用。
- 具体处理者:实现抽象处理者接口的具体类,可以处理请求或将其传递给链上的下一个对象。
2.责任链模式的作用
责任链模式的主要用途包括:
- 解耦发送者和接收者:请求的发送者不需要知道哪个对象将处理请求,也不需关心请求的传递细节。
- 增强系统可扩展性:可以根据需要增加新的处理类,满足开闭原则,方便地对责任链进行动态的添加或者删除。
- 简化对象之间的连接:每个对象只需保持一个指向其后继者的引用,简化了对象间的连接。
- 避免使用多重条件选择语句:如大量的 if 或 if···else 语句,使得代码更加清晰。
责任链模式适用于需要多个对象协作处理一个请求的场景,例如Web开发中的Filter过滤器、权限验证等场景。它的优点在于降低了对象之间的耦合度,并且可以动态地改变处理请求的对象和顺序。然而,如果责任链过长或者处理时间过长,可能会影响性能。
总的来说,责任链模式是一种有效的行为型设计模式,用于处理可以在预定义的一组对象中传递的请求。通过将请求的发送者和接收者解耦,增强了系统的灵活性和可扩展性。
3.责任链模式的优缺点
责任链模式的优点主要包括以下方面:
- 降低耦合度:它实现了请求的发送者和接收者之间的解耦,发送者无需知道具体的处理对象,只需将请求发送到责任链上。
- 增强灵活性和可扩展性:通过配置责任链,可以灵活地组合处理对象,实现不同的处理流程,并且可以在运行时动态改变处理的顺序。遵循开闭原则,新的处理者可以随时被加入到责任链中,而无需修改已有代码。
- 增强代码的可维护性:每个处理者只关注自己负责的请求,职责单一,使得代码更加清晰、可读性更高。
然而,责任链模式也存在一些缺点:
- 不保证请求一定被处理:由于责任链中的每个处理者都可以选择是否处理请求,如果没有正确配置或者某个处理者没有处理请求,可能导致请求无法被处理。
- 可能影响性能:如果责任链过长,或者处理时间过长,可能会对系统性能造成影响。同时,过多的责任链也可能导致管理上的复杂性增加。
总的来说,责任链模式提供了一种灵活且易于扩展的方式来处理一系列相关的请求。在设计系统时,使用责任链模式可以帮助我们创建出更加模块化和易于维护的代码结构。
3.1 概述
在现实生活中,常常会出现这样的事例:一个请求有多个对象可以处理,但每个对象的处理条件或权限不同。例如,公司员工请假,可批假的领导有部门负责人、副总经理、总经理等,但每个领导能批准的天数不同,员工必须根据自己要请假的天数去找不同的领导签名,也就是说员工必须记住每个领导的姓名、电话和地址等信息,这增加了难度。这样的例子还有很多,如找领导出差报销、生活中的“击鼓传花”游戏等。
定义:
又名职责链模式,为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
比较常见的springmvc中的拦截器,web开发中的filter过滤器
3.2 结构
职责链模式主要包含以下角色:
- 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
- 具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
- 客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。
3.2 案例实现
处理订单的操作
类图:
代码:
抽象处理者
1 | package com.itheima.designpattern.chain; |
订单信息类:
1 | package com.itheima.designpattern.chain; |
具体处理者:
1 | /** |
客户类:
1 | public class Application { |
3.3 优缺点
优点
- 降低了对象之间的耦合度该模式降低了请求发送者和接收者的耦合度。
- 增强了系统的可扩展性可以根据需要增加新的请求处理类,满足开闭原则。
- 增强了给对象指派职责的灵活性当工作流程发生变化,可以动态地改变链内的成员或者修改它们的次序,也可动态地新增或者删除责任。
- 责任链简化了对象之间的连接一个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。
- 责任分担每个类只需要处理自己该处理的工作,不能处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。
缺点:
- 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
- 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
- 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。
4.单例模式
1.什么是单例模式?
单例模式(Singleton Pattern)作为一种创建型设计模式,它的核心是控制对象的创建过程,保证某个类在系统中只有一个实例存在,并且提供一个访问该实例的全局访问点。这种模式通过将构造函数设为私有,避免了外部通过new操作符来创建类的实例。他有2种实现方式:单例类的实例通常在第一次使用时创建,称为懒汉式;也可以在类加载时就立即创建,称为饿汉式。
2.单例模式的作用
单例模式的作用可以总结为以下几点:
- 控制资源访问:当需要协调多个部分操作或访问共享资源时,单例可以作为一个中心点来控制资源的访问,如打印机管理器、线程池等。
- 性能优化:对于频繁创建和销毁的对象,单例模式可以节约系统资源,提高性能。因为单例模式减少了对象实例化的次数,降低了系统开销。
- 维持状态一致:由于单例模式确保了系统中只存在一个对象实例,它可以帮助保持某个特定状态的一致性,例如配置管理、会话管理等。
- 提供对唯一实例的受控访问:单例类自己负责创建唯一的对象实例,并提供给其他对象使用。这有助于防止其他对象随意创建实例,从而避免潜在的设计问题。
- 全局访问点:单例模式提供了一个全局访问点,方便在不同的程序模块之间共享访问同一个对象。
总得来说,单例模式主要用于那些系统中只需要一个实例的场景,比如配置文件的读取、数据库连接池、线程池或者某些需要频繁创建和销毁的对象的管理等场景。使用单例模式可以确保这些对象在系统中只有一个实例,从而节省资源、提高效率,并维持应用状态的一致性。
采取一定的办法保证在整个软件系统中,确保对于某个类只能存在一个实例。单例模式有如下三个特点:
①、单例类只能有一个实例
②、单例类必须自己创建自己的实例
③、单例类必须提供外界获取这个实例的方法
3.单例模式的优缺点
单例模式作为一种设计模式,它的优点和缺点都同样明显。
优点方面:
- 提供对唯一实例的受控访问:单例模式通过确保一个类只有一个实例并提供一个全局访问点,有助于控制实例的创建和访问,避免了多个实例可能引起的冲突和不一致。
- 节约系统资源:由于在系统内存中只存在一个对象实例,单例模式可以显著减少系统资源的消耗,尤其适用于那些创建和销毁频繁的对象,如数据库连接池、线程池等。
- 灵活性:单例模式使得类自身控制了实例化过程,提供了更大的灵活性,可以根据需要更改实例化过程。
缺点方面:
- 难以扩展:由于单例模式中通常没有抽象层,这可能导致单例类难以扩展。如果未来需要改变或扩展功能,可能会遇到困难。
- 违背单一职责原则:单例类往往承担了过多的职责,因为它们不仅要管理自己的实例,还要确保只有一个实例存在。这在一定程度上违背了单一职责原则。
- 潜在的资源浪费:如果单例对象长时间不被利用,系统可能会将其视为垃圾回收,导致对象状态的丢失。此外,如果管理不当,如数据库连接池被设计为单例,可能会导致资源溢出问题。
- 开发混淆:使用单例模式时,开发人员必须记住不能使用new关键字来实例化对象,这可能会导致一些混淆,尤其是在类库中定义的单例对象,应用程序开发人员可能无法直接实例化此类。
综上所述,单例模式适用于需要协调多个部分操作或访问共享资源的情况,以及需要频繁创建和销毁的对象的管理等场景。但同时,它的局限性也不容忽视,特别是在可扩展性和资源管理方面的潜在问题。因此,在决定是否使用单例模式时,需要根据具体的应用场景和需求进行权衡。
4.单例类的实现方式
1.饿汉模式(线程安全,但不支持延迟加载)
这种模式在类加载的时候实例 singleton 就已经创建并初始化好了,所以是线程安全的。
不过这种模式不支持延迟加载(第一次需要得到这个单例的时候,才回去创建它的实例)
1 | class Singleton { |
2.懒汉模式(支持延迟加载,但线程不安全)
实现了延迟加载,在第一次需要得到这个单例的时候,才回去创建它的实例,以后再需要就可以不用创建,直接获取了。但不是线程安全。
1 | //懒汉模式 |
3.懒汉模式(支持延迟加载且线程安全)
在这段代码中,使用了synchronized
和volatile
关键字来确保单例模式的线程安全性。
synchronized
使用Singleton
类的Class对象作为锁对象,当多个线程同时访问getInstance()
方法时,只有一个线程能够进入同步代码块然后实例化Singleton类,其他线程就直接返回这个实例对象了,从而保证了单例的唯一性。volatile
关键字用于修饰实例变量,确保变量的可见性和禁止指令重排序。volatile
关键字的作用是防止编译器对代码进行优化,保证每次读取singleton
变量时都从主内存中获取最新值,而不是从缓存中读取。这样可以确保多线程环境下对singleton
变量的读写操作都是正确的。
使用 volatile 关键字修饰实例
volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
1 | public class Singleton { |
3.Spring中用到的单例设计模式
在Spring中最明显的使用场景是在配置文件中配置注册bean对象的时候设置scope的值为singleton 。
为什么bean设置成单例?
Spring中的bean默认被设置为singleton作用域,这意味着在Spring容器中,每个bean定义只会创建一个实例。这种设计决策背后有以下几个原因:
- 性能优化:单例模式可以减少对象的创建和销毁次数,从而减少系统开销,提高性能
- 资源共享:Singleton bean在容器中只有一个实例,方便实现资源共享,例如数据库连接池、配置管理器等,这些资源通常需要在整个应用程序中共享使用。
- 易于管理:由于Singleton bean在容器中只有一个实例,因此管理和维护起来更加简单。不需要担心多个实例之间的同步和一致性问题。
- 依赖注入:在使用依赖注入时,Singleton作用域的bean可以确保它们的依赖关系在整个应用程序生命周期内保持一致,这有助于减少潜在的错误和不一致。
总的来说,将Spring的bean设置为singleton作用域是为了提高性能、简化资源共享、保证无状态性和线程安全、以及作为默认行为等方面的原因。然而,如果bean包含特定于客户端的状态或需要每次请求都创建新实例,那么应该使用其他作用域,如prototype作用域。
5.代理模式
1.什么是代理模式
代理模式是一种结构型设计模式,代理模式通过创建一个具有相同接口的代理对象来控制对原始对象的访问。代理对象可以在调用原始对象之前或之后执行一些额外的操作。在代理模式中通常有三个角色,即代理类、目标类和客户端。其中,代理类包含与目标类相同的接口,并在内部维护一个对目标类的引用;目标类是实际执行任务的类;客户端则通过代理类来访问目标类。
2.代理模式的作用
代理模式的主要用途如下:
- 控制访问:代理模式可以用于控制对对象的访问权限。例如,如果某个对象不应该直接被外部访问,可以通过代理来提供访问途径,同时代理可以在访问前后添加必要的处理逻辑。
- 增强功能:代理不仅可以控制访问,还可以在不改变原对象的基础上增加额外的功能。比如,代理可以在调用原对象的方法前后添加日志记录、性能监控或者异常处理等功能。
- 简化接口:有时候,原对象的接口可能很复杂,代理可以提供一个更简单的接口给客户端使用,从而隐藏原对象的复杂性。
- 延迟加载:代理可以用于延迟对象的创建和初始化,直到真正需要使用该对象时才创建,这有助于提高系统的性能。
- 分布式访问:在分布式系统中,代理可以作为远程对象的本地代表,处理与远程对象通信的细节,从而简化客户端的操作。
在实际生活中,代理模式也有广泛的应用。例如,房屋租赁中的中介就是房东(目标对象)的代理,租客通过中介来完成租房过程,而不需要直接与房东联系。
总的来说,代理模式提供了一种灵活而有效的方法来控制和增强对对象的访问,同时也能够简化接口和实现延迟加载等高级功能。
3.代理模式的优缺点
代理模式作为一种常用的设计模式,它的优势在于能够有效地分离关注点和职责,但同时也带来了一些潜在的缺点。
首先,让我们看看代理模式的优点:
- 降低耦合度:通过引入代理对象,客户端与目标对象之间的直接依赖被解除,这样可以减少系统各部分之间的直接交互,从而降低系统的耦合度。
- 增强功能:代理可以在不改变原有目标对象的基础上增加额外的功能,如权限控制、延迟初始化等,这样可以在不影响原有业务逻辑的情况下对功能进行扩展。
- 保护目标对象:代理可以作为目标对象的保护层,防止外部对目标对象的直接访问,从而起到保护作用。
然而,代理模式也存在一些缺点:
- 增加系统复杂度:引入代理对象会增加系统的类数量,这可能会导致系统的结构变得更加复杂,对于初学者来说可能不太容易理解。
- 影响性能:每次请求都需要经过代理层的处理,这可能会引入额外的处理时间和资源消耗,从而对系统性能产生一定的影响。
总的来说,代理模式是一种强大的设计工具,它可以帮助开发者实现更加灵活和可维护的设计。然而,使用代理模式时需要权衡其带来的额外复杂性和性能影响。在实际开发中,应当根据具体的应用场景和需求来决定是否使用代理模式以及如何设计代理结构。
2.代理模式角色
Subject(抽象主题角色) :定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法;RealSubject(真实主题角色):也叫被代理角色/委托类/目标对象,是业务逻辑的具体执行者,真正实现业务逻辑的类;Proxy(代理主题角色) :也叫代理类/代理对象,它负责对真实角色的应用,把所有抽象主题类定义的方法限制委托给真实主题角色实现,并在真实主题角色处理前后做一些预处理或善后工作。
3.两种代理模式
1.静态代理
静态代理:代理类在编译阶段生成,在编译阶段将通知织入Java字节码中,也称编译时增强。
静态代理优缺点:
优点:
通过静态代理,我们达到了功能增强的目的,而且没有侵入原代码,这是静态代理的一个优点。静态代理实现简单,且不侵入原代码。
缺点
1、 当需要代理多个类的时候,由于代理对象要实现与目标对象一致的接口,有两种方式:
- 只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大
- 新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类
2、 当接口需要增加、删除、修改方法的时候,目标对象与代理类都要同时修改,不易维护。
1 | //抽象主题类 |
2.动态代理
1.JDK动态代理
如果目标类实现了接口,Spring AOP会选择使用JDK动态代理目标类。代理类根据目标类实现的接口动态生成,不需要自己编写,生成的动态代理类和目标类都实现相同的接口。JDK动态代理的核心是InvocationHandler
接口和Proxy
类。
缺点:目标类必须有实现的接口。如果某个类没有实现接口,那么这个类就不能用JDK动态代理。
2.CGLIB动态代理
通过继承实现。如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library)可以在运行时动态生成类的字节码,动态创建目标类的子类对象,在子类对象中增强目标类。
优点:目标类不需要实现特定的接口,更加灵活。
缺点:CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final
,那么它是无法使用CGLIB做动态代理的。
3.静态代理和动态代理的对比)
- 灵活性:动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
- JVM 层面:静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
4.JDK动态代理代码举例
1 | import java.lang.reflect.InvocationHandler; |
4.Spring中的代理模式
Spring中AOP的实现.
Spring
的AOP
是通过动态代理实现的。如果我们为Spring
的某个bean
配置了切面,那么Spring
在创建这个bean
的时候,实际上创建的是这个bean
的一个代理对象,我们后续对bean
中方法的调用,实际上调用的是代理类重写的代理方法。而Spring
的AOP
使用了两种动态代理,分别是JDK的动态代理,以及CGLib的动态代理。
6.模板方法模式
1.什么是模板方法模式
模板模式是一种行为型设计模式,它在一个抽象类中定义了执行它的方法的方式/模板。
模板模式的核心思想是将一个算法分解为多个步骤,这些步骤可以在抽象类中预设,而将具体实现推迟到子类中进行。这样做的目的是让子类在不改变算法结构的前提下,可以重定义算法的某些特定步骤。这种模式通常用于处理具有相同流程但步骤细节不同的场景。
2.模板模式的作用
模板模式的用途包括代码复用、提高扩展性、符合开闭原则、分离不变与可变部分等,具体如下:
- 代码复用:通过把相同的代码放在抽象类中,可以避免重复编写相似功能的代码,提高代码复用性。
- 提高扩展性:不同的子类可以根据具体需求重写某些步骤,这样当需要添加新的功能时,只需扩展子类而不用修改抽象类的代码。
- 符合开闭原则:模板模式允许程序对扩展开放(可以通过继承机制增加新的行为),同时对修改封闭(不需要改动原有代码)。
- 分离不变与可变部分:不变的部分在父类中定义,可变的部分留给子类去实现,使得代码结构清晰,易于维护。
总之,模板模式适用于有固定流程但各步骤具体实现可能不同的场景,它通过抽象类定义算法骨架,而将具体实现留给子类来完成,既保证了算法结构的一致性,又提供了足够的灵活性供子类根据需要进行定制。
3.模板模式的优缺点
模板模式作为一种行为型设计模式,它的优势在于提升代码复用性和扩展性。具体如下:
- 代码复用性:模板模式通过将不变的部分放在抽象类中,而将可变的部分留给子类来实现,这样相同的处理逻辑就可以在抽象父类中得到共享,从而提高了代码的复用性。
- 扩展性:当需要对算法的某些步骤进行定制时,可以通过继承抽象类并重写相应的方法来实现,这样新的子类就可以在不改变原有结构的情况下增加新的行为,提高了系统的扩展性。
然而,模板模式也有一些缺点需要考虑:
- 复杂性:由于模板模式涉及到抽象类的使用和继承机制,对于初学者来说,可能会增加理解和使用的复杂性。
- 灵活性限制:模板模式要求子类遵守父类设定的模板,这可能会限制子类的灵活性,因为子类必须在父类的模板方法定义的框架内进行操作。
综上所述,模板模式通过提供一个算法的框架,允许子类在不改变算法结构的前提下重新定义算法的某些步骤,适用于有固定流程但各步骤具体实现可能不同的场景。
4.模板方法模式角色
- 抽象模板:负责给出一个算法的轮廓和骨架,它由模板方法,基本方法,抽象方法构成。
- 模板方法:定义了算法的骨架,按某种顺序调用执行不同基本方法。实现对基本方法、抽象方法的调度,完成固定的逻辑。为了防止恶意操作,通常模板方法都加上 final 关键字,不允许覆写。基本方法:在抽象类中已经实现了的方法,基本方法尽量设计为protected类型, 符合迪米特法则(一个对象应当对其他对象有尽可能少的了解)抽象方法:在抽象类中还没有实现的方法,是实现算法各个步骤的方法,尽量设计为protected类型, 符合迪米特法则(一个对象应当对其他对象有尽可能少的了解)
- 具体模板:实现父类定义的抽象方法,也就是实现特定步骤。
5.模板模式通用代码
1 | public abstract class AbstractTemplate { |
6.使用场景
算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。
7.装饰器模式
1.什么是装饰器模式
装饰器(Decorator)模式的定义:指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式。
2.装饰器模式的角色有哪些
装饰器模式主要包含以下角色。
- 抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
- 具体构件(ConcreteComponent)角色:实现抽象构件,通过装饰角色为其添加一些职责。
- 抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
- 具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
3.通用代码
1 | package decorator; |