Java设计模式

24

Java设计模式

GoF(最先开始着手进行设计模式分类整理工作)对设计模式的定义是:
设计模式是在特定的环境下为解决某一通用软件设计问题提供的一套定制的解决方案,该方案描述了对象和类之间的相互作用

为什么要遵循设计模式
为了让程序,具有更好的:

  • 代码重用性(相同功能的代码可重复使用)
  • 可读性(编程规范性,便于其他程序员阅读和理解)
  • 可靠性
  • 可扩展性
  • 使程序呈现高内聚、低耦合的特点

一、面向对象设计的七大原则

1.1 单一职责原则

定义:一个类应该只包含单一的职责,并且该职责被完整的封装在一个类中。
优点:

  • 降低类的复杂度,一个类只负责一项职责
  • 提高类的可读性,可维护性
  • 降低变更引起的风险

1.2 开闭原则(软件设计的目标)

  1. 软件实体应当对扩展(对软件提供方)开放,对修改(对软件使用方)关闭。用抽象设计框架,用实现扩展细节
  2. 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有代码来实现变化

1.3 里氏替换原则(软件设计的基础)

  1. 所有引用基类的地方必须能够透明地使用其子类。
  2. 在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法。
  3. 里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当的情况下,可以通过聚合、组合、依赖来解决问题。

1.4 依赖倒转原则(软件设计的手段)

  1. 高层模块不应该依赖于底层模块,它们都应当依赖于抽象。
  2. 抽象不应该依赖于细节,细节应该依赖于抽象。
    依赖倒转原则的核心思想是面向抽象编程。
    使用接口或抽象类的目的是定制好规范,而不涉及任何具体的操作,把展现细节的任务交给它们具体的实现类去完成。

注意事项

  1. 底层模块尽量都要有抽象类或接口,或者两者都有,这样程序的稳定性更好。
  2. 变量的声明类型尽量是抽象类或接口,这样我们的变量引用和实际对象间,就存在一个缓冲层,利于程序扩展和优化。
  3. 继承时遵循里氏替换原则。

1.5 接口隔离原则

客户端不应该依赖那些它不需要的接口。
客户端应该依赖于那些它们需要使用的最小接口。

1.6 合成复用原则

优先使用对象组合或聚合,而不是通过继承来达到复用的目的。

1.7 迪米特法则(最少知识原则)

  1. 每一个软件单位对其他单位只知道最少的知识,并且局限于那些与本单位密切相关的软件单位。
  2. 类与类之间关系越密切,耦合度越大
  3. 只与直接朋友进行通信。直接朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式有:依赖、关联、组合、聚合等,其中,我们称出现在成员变量、方法参数、方法返回值中的类为直接朋友,而出现在局部变量中的类不是直接朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部

注意事项

  • 迪米特法则的核心是降低类之间不必要的耦合

==开闭原则是目标,里氏代换原则是基础,依赖倒转原则是手段。

1.8 设计模式分类

设计模式根据目的可分为:创建型模式、结构性模式、行为型模式。

  • 创建型模式:主要用于创建和克隆对象有5种,分别是:工厂方法模式(Factory Method)、抽象工厂模式(Abstract Factory)、建造者模式(Builder)、原型模式(Prototype)、单例模式(Singleton)。

    • 目的是让对象的创建和使用分离,符合单一职责原则。
  • 结构性模式:主要用来处理类或对象的组合,有7种,分别是:适配器模式(Adapter)、桥接模式(Brige)、享元模式(Flyweight)、组合模式(Composite)、装饰模式(Decorator)、外观模式(Facade)、代理模式(Proxy)。

    • 程序结构上实现松耦合,从而可以扩大整体的类结构,用来解决更大的问题。
  • 行为型模式:主要用于描述类和对象怎样交互和怎样分配职责有11种,分别是:职责链模式(Chain of Responsibility)、命令模式(Command)、解释器模式(Interpreter)、迭代器模式(Iterator)、中介者模式(Mediator)、备忘录模式(Memento)、观察者模式(Observer)、状态模式(State)、策略模式(Strategy)、模版方法模式(Template Method)、访问者模式(Visitor)。

范围/目的创建型模式结构性模式行为型模式
类模式工厂方法模式(类)适配器模式解释器模式
模版方法模式
对象模式抽象工厂模式
建造者模式
原型模式
单例模式
(对象)适配器模式
桥接模式
组合模式
装饰模式
外观模式
享元模式
代理模式
职责链模式
命令模式
迭代器模式
中介者模式
备忘录模式
观察者模式
状态模式
策略模式
访问者模式

各类设计模式的特点

创建型模式
结构型模式
行为型模式:用于描述程序在运行时复杂的流程控制,即描述多个类和对象之间怎样相互协作共同完成单个对象无法单独完成的任务,它涉及算法与对象间职责的分配

1.9 UML

聚合和组合之间的区别
组合是一种强聚合关系,在组合关系中,部分不能脱离整体,它们具有相同的生命周期,也即整体消失的同时部分也不能独立存在。

如果一个类是通过 set 或者 constructor 注入主类中的,那么就属于聚合。
如果一个类是主类 new 出来的,那么就属于组合。

依赖和关联之间的区别
依赖:是一种短期关系,通常在方法调用或对象创建时形成,当方法执行结束或对象不再被引用时,依赖关系也就消失了。依赖关系是单向的。依赖关系通常在类的成员变量或方法参数中体现。
关联:是一种长期关系,表示对象之间的结构上的连接或合作关系。关联关系是双向的,对象之间互相知道对方的存在,并且可以互相访问对方的属性和方法。关联关系通常通过类的成员变量来表示。

通常来说,依赖关系更加动态,通常是一种短期的、单向的关系,而关联关系更加静态,通常是一种长期的、双向的关系。

二、简单工厂模式

简单工厂模式不属于GoF的23中设计模式之一,但是使用也较为广泛。

思想如下:定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。

Alt text

该模式下一般有三个角色:

  • Factory(工厂角色):负责创建具体产品
  • Product(抽象产品角色)
  • ConcreteProduct(具体产品角色)

使用技巧:

  • 可以通过配置文件在简单工厂参数和具体产品建立关联,这样做的好处是当增加具体产品的时候,只需要修改配置文件即可。

优点:

  1. 实现了对象创建和使用的分离
  2. 客户端无需知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可
  3. 通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,一定程度上提高了系统的灵活性

缺点:

  1. 简单工厂类集中了所有产品的创建逻辑,类的职责过重,一旦不能正常工作,整个系统都要受到影响(不符合单一职责原则)
  2. 系统扩展困难,如果添加新的产品就不得不修改简单工厂类,产品类型较多时可能造成工厂逻辑过于复杂,不利于系统的扩展和维护(不符合开闭原则)
  3. 简单工厂使用了静态工厂方法,造成工厂角色无法形成具有继承的等级结构

适用环境:

  1. 工厂类负责创建的对象较少,由于负责的对象较少,所以不需要担心工厂逻辑过于负责的问题
  2. 客户端只知道传入工厂类的参数,对如何创建对象并不关心

三、工厂方法模式

又叫多态工厂模式。

思想:定义一个用于创建对象的接口,但是让子类决定将具体产品的实例化。工厂方法模式让一个类的实例化延迟到其子类。

工厂方法模式与简单工厂模式的区别:简单工厂模式中简单工厂类负责所有产品的创建,而工厂方法模式中,具体的产品由具体工厂来创建,也就是一个工厂只负责一种产品的创建

Alt text

该模式包含下面四个角色:

  • Product(抽象产品):对具体产品共性的抽象
  • ConcreteProduct(具体产品)
  • Factory(抽象工厂):与抽象产品之间是依赖关系
  • ConcreteFactory(具体工厂):负责具体产品的创建

使用技巧

  • 可以通过配置文件来制定具体工厂,从而创建不同的具体产品,当增加了一种新的产品时,只需要实现新的具体工厂,然后修改配置文件即可。符合开闭原则。

优点

  1. 基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够让工厂自主决定创建何种产品对象,如何创建该对象完全封装在具体工厂内部。
  2. 使用工厂方法模式的一个好处是当增加具体产品的时候,无需修改抽象工厂和抽象产品所提供的接口,也无需修改客户端,也无需修改其他具体产品类和具体工厂,只需实现具体产品类和具体工厂即可,这使系统的可扩展性非常好。

缺点

  1. 在添加新产品时需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将呈几何倍数增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,在一定程度上增加了系统的开销。

  2. 由于考虑到了系统的可扩展性,引入了抽象层,在客户端代码中均使用抽象层进行定义,在一定程度上增加了系统的抽象性和理解难度。(面向抽象层编码的共性)

适用环境

  • 客户端不知道它所需要的对象的类,只需要知道对应的工厂即可,具体产品由具体工厂创建,可将具体工厂类的类名存储在配置文件或数据库中。(面向抽象编程)
  • 系统需要具有高可扩展性

个人理解
当简单工厂模式已经无法满足创建产品的要求时(比如产品类别增加等因素导致),可以采用工厂方法模式。具体来说,先将产品进行划分,划分为不同种类的具体产品,每种产品对应一种具体的工厂。比如说一个公司,以前只生产可口可乐饮料。但由于业务扩张,需要增加雪碧产品,此时就可以使用工厂方法模式,可乐产品工厂负责生产不同口味的可乐,雪碧产品工厂负责生产不同口味的雪碧,这两个具体工厂都继承同一个抽象工厂。

四、抽象工厂模式

思想:提供一个创建一系列相关或互相依赖对象的接口,而无需指定它们具体的类。

抽象工厂模式和工厂方法模式的区别:工厂方法模式中创建的产品都是同一类产品,因为它们具有相同的父类(抽象产品类)。而抽象工厂模式创建的是一系列相关或互相依赖的产品,也即它创建的是多种具有关联关系的产品。

这里介绍两个概念:

  • 产品等级结构:产品等级结构即产品的继承结构,例如一个抽象类是电视机,其子类可以是:海尔电视机、TCL电视机、海信电视机等。
  • 产品族:产品族指由同一个工厂生产的位于不同产品等级结构中的同一组产品,例如海尔工厂生产的海尔电视机、海尔洗衣机、海尔空调构成了一个海尔产品族。

Alt text

该模式包含下面四个角色:

  • AbstractFactory(抽象工厂):提供了生产多个抽象产品的方法
  • ConcreteFactory(具体工厂):负责实现抽象工厂的接口,提供具体类型的产品族
  • Product(抽象产品)
  • ConcreteProduct(具体产品)

应用案例:抽象工厂在JDK中用到的非常多,比如JDK提供的JWT中,window界面元素linux界面元素的风格是不同的,界面元素包括:文本框、按钮、单选框、输入框等,界面元素可以视为抽象产品,不同风格的界面元素对应不同的实现,它们分别被WindowsFactory和LinuxFactory等具体工厂所创建。

优点

  • 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象
  • 增加新的产品族很方便,无需修改已有系统,符合开闭原则
    缺点
  • 增加新的产品麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,不符合开闭原则。

适用环境

  • 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,用户无需关心对象创建过程,将对象的创建和使用解耦
  • 系统中有多于一个产品族,而每次只使用其中某一个产品族。可以通过配置文件等方式来动态改变产品族,也可以很方便地添加产品族
  • ==产品等级结构稳定==,在设计完成后不会向系统中增加新的产品等级结构或者删除已有的产品等级结构

五、建造者模式

基本思想:将一个复杂对象的构建和它的表示分离,使得同样的构建过程可以创建不同的表示。

建造者模式关注如何一步一步地创建一个复杂对象,不同的建造者定义了不同的创建过程。

Alt text
如上图,建造者模式关注的是如何将轮胎、方向盘、发动机等部件组装成一辆汽车交给用户。

建造者模式包含下面四个角色:

  • Builder(抽象建造者):该接口中一般有两类方法,一类是用户创建复杂对象的各个部件,另一类是build方法,用于返回构建的产品对象
  • ConcreteBuilder(具体建造者)
  • Product(产品):它是被构建的复杂对象
  • Director(指挥者):指挥者,它负责安排负责对象的构建次序,一般而言,客户端即为指挥者,但如果一个对象的构建过程极为复杂,则可以创建一个专门的指挥者来负责指挥构建者构建对象。

Alt text

优点

  • 客户端不需要知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象
  • 每一个具体构建者都相对独立,都实现了抽象构建者,而指挥者类针对抽象构建者进行编程,因此可以很方便的替换构建者和增加新的构建者,用户使用不同的具体构建者即可得到不同的产品,系统扩展方便,符合开闭原则
  • 可以更加精细的控制产品的创建过程。

缺点

  • 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用构建者模式,因此其使用范围受到限制

适用环境

  • 需要生成的产品对象有很复杂的内部环境,这些产品对象通常包含多个成员变量
  • 需要生成的产品对象的属性相互依赖,需要指定其生成顺序
  • 对象的创建过程独立于创建该对象的类。在建造者模式中引入了指挥者类将创建过程封装在指挥者类中,而不在建造者类和客户类中
  • 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品

六、原型模式

基本思想:使用原型实例指定待创建对象的类型,并且通过复制这个原型(Object.clone方法)来创建新的对象。

原型模式包含下面三个角色:

  • Prototype(抽象原型类):它是声明克隆方法的接口,是所有具体原型类的公共父类,它可以是抽象类也可以是接口,甚至还可以是具体实现类
  • ConcretePrototype(具体原型类):它实现了抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
  • Client(客户端):在客户端中一般通过原型对象克隆一个新的对象。

这里需要注意的是深拷贝和浅拷贝的问题,在Java中,当调用Object.clone()方法时,基本数据类型是值拷贝,而引用数据类型只拷贝了引用,是浅拷贝,所以当类中有引用对象的时候,需要自己实现深拷贝。

优点

  • 当要创建的对象较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有的实例对象可以加快创建效率
  • 原型模式简化了创建结构,无需专门的工厂类来创建产品
  • 可以使用深拷贝的方式来保存对象的状态,以便在未来恢复对象的状态

缺点

  • 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时需要修改已有的代码,违背了开闭原则
  • 在实现深克隆时需要编写较为复杂的代码,实现起来比较麻烦。

适用场景

  • 创建新对象的成本较大
  • 系统需要保存对象的状态,而对象的状态变化很小

七、单例模式

基本思想:确保一个类只有一个实例,并提供了一个全局访问点来访问这个唯一实例。

单例模式实现分为:

  • 饿汉式:将对象声明为当前类的私有静态变量,然后通过公有静态方法向外返回该实例。对象在类被加载时进行实例化。
  • 懒汉式:也叫延迟加载技术,该方式通过双重检查锁定来实现
  • 静态内部类:使用了静态内部类的加载机制来实现延迟加载效果,当第一次调用getInstance方法时,JVM会加载静态内部类,然后才会对类进行实例化。
  • 双重检查:可以解决并发环境下的冲突问题
  • 枚举:采用枚举类型(加载时实例化)的特点来实现

枚举实现

用枚举来实现单例模式,有如下有点:

  1. 线程安全:枚举类的实例在类加载时被实例化,且只会被实例化一次,因此在多线程环境下也能保证单例的唯一性。
  2. 防止反序列化创建新实例:枚举类不会被反序列化创建新的实例,因为枚举类默认实现了Serializable接口,并且枚举常量在序列化和反序列化过程中都会被忽略。
  3. 防止反射攻击:枚举类的构造方法是私有的,无法通过反射来创建实例。

优点:

  • 单例模式提供了对唯一实例的受控访问。
  • 由于系统中只存在了一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式可以提高系统的性能
  • 允许可变数目的实例。

缺点:

  • 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难
  • 单例类的职责过重,一定程度上违背了单一职责原则
  • Java有自动垃圾回收技术,如果实例化的共享对象长期得不到使用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又会重新进行实例化,这将导致共享的单例对象状态的丢失

适用环境:

  • 系统只需要一个实例对象
  • 客户调用类的单个实例只允许使用一个公共访问点

八、适配器模式

基本思想:将一个类的接口转换成客户希望的另一个类的接口。适配器模式让那些类不兼容的接口可以一起工作。

该模式有如下三种角色:

  • Target(目标抽象类):目标抽象类定义客户所需要的接口
  • Adapter(适配器类):它可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配
  • Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,它里面包含了客户希望使用的业务方法。

如果目标抽象类是一个接口,则适配器模式被称为类适配器模式。如果目标抽象类是一个类,则适配器类只能继承该目标抽象类,由于Java只支持单继承,所以与适配者类之间建立关联关系,这种被称为对象适配器模式。

对应生活中的例子,现在笔记本电脑越做越薄,很多电脑都没有网线口。因此就出现了拓展坞,用来让缺少网口的电脑进行有线上网。这里的拓展坞就类似适配器,网口类似适配者,typec接口(拓展坞是typec接口的扩展)类似这里的目标抽象类。

Alt text

类适配器模式

Alt text

对象适配器模式

缺省适配器
适配器模式中的一种特殊的类型,它提供了目标抽象类的空实现,有了缺省适配器类后可以直接继承该适配器类,根据需要有选择地覆盖在适配器类中定义的方法。

双向适配器
同时维持了对目标抽象类和适配者类的引用,适配器类可以通过它来调用适配者类的方法,同时也可以通过它调用目标抽象类中的方法

优点

  • 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无需修改原有结构
  • 增加类的透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性
  • 灵活性和扩展性都非常好,使用配置文件可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器,符合开闭原则
  • 对于类适配器来说,还具有以下优点:由于适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器类的灵活性更强
  • 对于对象适配器来说,还具有如下优点:一个适配器类可以把多个不同的适配者适配到同一个目标、可以适配一个适配者的子类,根据里氏代换原则,适配者的子类也可以通过该适配器进行适配

类适配器模式缺点

  • 对应Java等不支持多重继承的语言,一次最多只能适配一个适配者类,不能同时继承适配器和适配者(单继承)
  • 适配者类不能为最终类
  • 目标抽象类只能为接口,不能为类

对象适配器模式缺点

  • 在该模式下置换适配者类的方法较为困难和麻烦
  • 推荐使用对象适配器模式,符合合成复用原则
  • 根据”里氏替换原则“,适配者的子类也可通过该适配器进行适配

适用场景

  • 系统需要适用一些现有的类,而这些类的接口不符合系统的需要
  • 想创建一个可以重复使用的类,用于和一些彼此之间没有太大关联的类一起工作

==适配器模式解决兼容问题,桥接模式解决功能扩展问题

九、桥接模式

基本思想:将抽象部分与它的实现部分解耦,使得两者都能够独立变化

桥接模式可以用来处理具有多维度变化的情况。

使用案例:画笔有多种类型(钢笔、铅笔、毛笔等),每种类型的笔又可以有多种颜色,画笔类型和笔的颜色是不同的变化维度,这种具有多种变化维度的情况可以用桥接模式来实现。

桥接模式有如下四种角色:

  • Abstraction(抽象类):它是用于定义抽象类的接口,通常是抽象类而不是接口,其中定义了一个Implementor(实现类接口)类型的对象并可以维护该对象,它与Implementor具有关联关系
  • RefinedAbstraction(扩充抽象类):它扩充由Abstraction定义的接口,通常情况下它是具体类,实现了在Abstraction中声明的抽象业务方法,在RefinedAbstraction中可以调用Implementor中定义的业务方法
  • Implementor(实现类接口):Implementor类和Abstraction类的内部方法之间无关系,可以完全不同,Implementor声明了一些基本操作,其具体实现交给了子类,通过关联关系Abstraction不仅拥有自己的方法,还可以调用Implementor中的方法。
  • ConcreteImplementor(具体实现类):实现了Implementor中声明的业务方法

在上述的使用案例中,画笔充当了抽象类,毛笔、钢笔、铅笔等具体的笔充当了扩充抽象类,笔的颜色充当了实现类接口,红色、绿色等颜色充当了具体实现类。

Alt text

优点

  • 桥接模式体现了很多面向对象设计原则的思想,包括单一职责原则、开闭原则、里氏代换原则、依赖倒转原则、合成复用原则等
  • 分离抽象接口及其实现部分
  • 在很多情况下,桥接模式可以取代多层继承方案,多层继承方案违背了单一职责原则,复用性较差
  • 桥接模式提高了系统的可扩展性,在两个变化维度中扩展任何一个都不需要改变原有系统,符合开闭原则

缺点

  • 桥接模式会增加系统的理解和设计难度,由于关联关系建立在抽象层,所以要求开发者一开始就针对抽象层进行设计与编程
  • 桥接模式要求正确地识别系统中的两个独立变化的维度,因此使用范围有一定限制,如何正确识别两个变化维度需要一定的经验积累

适用场景

  • 如果一个系统需要在抽象化和具体化之间增加更多的灵活性,避免在两个层级之间建立静态的继承关系
  • 如果系统由两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立进行扩展。
  • 系统需要对抽象化角色和实现化角色进行动态耦合

十、组合模式

基本思想:组合多个对象形成树形结构以具有部分-整体关系的层次结构,组合模式让客户端可以统一对待单个对象和组合对象。

组合模式包含以下三个角色:

  • Compopnent(抽象构件):可以是接口或抽象类,为叶子构件和容器构建对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现。在抽象构件中定义了访问及管理它的子构件的方法,如增加子构件、获取子构件等。
  • Leaf(叶子构件):它在组合结构中表示叶子结点对象,叶子结点没有子结点,它实现了在抽象构件中定义的行为。对于那些访问及管理子构件的方法,可以通过抛异常、提示错误等方式进行处理。
  • Composite(容器构件):它在组合结构中表示容器结点对象,容器结点包含子结点,其子结点可以是叶子结点,也可以是容器结点,它提供了一个集合用于储存子结点,实现了在抽象构件中定义的行为。

Alt text

透明组合模式
在透明组合模式中,抽象构件Component中声明了所有用于管理成员对象的方法,包含add、remove、getChild,这样做的好处是确保所有的构建类都有相同的接口。在客户端看来,叶子结点和容器结点所提供的方法是一致的,客户端就可以一致地对待所有对象。

安全组合模式
在安全组合模式中,抽象构件Component只声明了叶子构件和容器构件中公共的方法,管理成员对象的方法只声明在容器构件中。客户端在使用时需要对构件类型进行判断然后处理。一般在使用过程中,我们会用这种方式,而不是在抽象构件中声明所有的方法。

优点

  • 可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制
  • 客户端可以一致地使用一个组合结构或其中的单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码
  • 在组合模式中增加新的容器构件和叶子构件都十分方法,不需要修改原有代码,符合开闭原则
  • 为树形结构面向对象设计提供了一种通用的解决方案,通过叶子对象和容器对象的递归组合构成复杂的树形结构

缺点

  • 在增加新构件时很难对容器中的构件类型进行限制。

适用场景

  • 在具有整体和部分的层次结构中希望通过一种方式忽略整体和部分的差异,客户端可以一致地对待它们。
  • 在一个使用面向对象语言开发的系统中需要处理一个树形结构
  • 在系统中能够分离出叶子对象和容器对象,而且它们的类型不固定,需要增加一些新的类型。

十一、装饰模式

基本思想:动态地给一个对象增加一些额外的职责。就扩展功能而言,装饰模式提供了一种比使用子类更加灵活的替代方案。

装饰模式包含如下四个角色:

  • Component(抽象构件):声明了在具体构件中实现的业务方法,它的引入可以使客户端一致地处理未被装饰的对象以及装饰后的对象,实现了客户端的透明操作
  • ConcreteComponent(具体构件):实现了在抽象构件中声明的方法,装饰类可以给它增加额外的职责
  • Decorator(抽象装饰类):它是抽象构件的子类,用于给抽象构件增加职责,但是具体职责在其子类中实现,它维护了一个指向抽象构件对象的引用,通过该引用可以调用装饰之前构件对象的方法,并通过其子类扩展该方法,达到装饰的目的
  • ConcreteDecorator(具体装饰类):是抽象装饰类的子类,负责向构件添加新的职责,每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法来扩充对象的行为。

Alt text

透明装饰模式
透明装饰模式要求客户端完全针对抽象编程,装饰模式的透明性要求客户端程序不应该将对象声明为具体构件类型或具体装饰类型,而应该全部声明为抽象构件类型。

半透明装饰模式
由于要求客户端完全针对抽象编程难度较大,因此半透明装饰模式允许客户端程序将对象声明为具体构件类型和具体装饰类型。

优点

  • 对于扩展一个对象的功能,装饰模式比继承更加灵活,不会导致类的个数急剧增加。
  • 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择具体的装饰类,从而实现不同的行为
  • 可以对一个对象进行多次装饰,得到功能更加强大的对象
  • 具体构建类和具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类或具体装饰类而不需要修改原有系统,符合开闭原则

缺点

  • 装饰模式提供了一种比继承更加灵活、机动的解决方案,但同时也意味着比继承更加易于出错,排错也更困难,在调试时需要逐级排查错误,较为繁琐

适用场景

  • 在不影响其他对象的情况下以动态、透明的方式给单个对象添加职责
  • 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。
  • 售卖咖啡场景,单品咖啡一般都可以加配料,加配料后会联动影响价格的计算。这里咖啡作为装饰器,配料作为具体构件,它们都实现了抽象构件,在抽象构件里定义了计算价格的方法。某种单品咖啡作为具体的装饰器,它不仅要计算自己的价格,也要加上其所包含的抽象构件的价格。

==装饰器模式在Java IO中广泛应用。比如高层的流对象(DataInputStream)可以通过构造器来注入底层的流对象(InputStream)来完成包装。==

十二、外观模式

基本思想:为子系统中的一组接口提供了一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

它包含如下两种角色:

  • Facade(外观角色):声明了客户端可以调用的方法,外观角色可以知道相关的子系统的功能和责任;在正常情况下,外观角色将客户端发来的请求委派给响应的子系统,传递给相应子系统对象来处理。
  • SubSystem(子系统):在软件系统中有一个或多个子系统存在,子系统可以是一个类也可以是一系列类的组合,实现了子系统的功能

外观模式通过外观角色将客户端想要使用的子系统功能做了高层抽象,使客户端仅知道它所需要的功能,符合迪米特法则(最少知识原则)。

优点

  • 它对客户端屏蔽了子系统组件,减少了客户端所需处理的对象数目,并使子系统使用起来更加容易。通过引入外观模式,客户端的代码将变得更加简洁,与之关联的对象也很少
  • 它降低了客户端和子系统之间的耦合,对子系统的修改不会影响到客户端,只需要修改对应的外观角色即可

缺点

  • 不能很好地限制客户端直接使用子系统类,如果对客户端访问子系统类做太多的限制则减少了可变性和灵活性
  • 如果设计不当,增加新的子系统,可能需要修改外观类的源代码,违背了开闭原则

适用环境

  • 当要为访问一系列复杂的子系统提供一个简单入口时可以考虑外观模式
  • 客户端程序与多个子系统之间存在很大的依赖性。引入外观类可以将子系统和客户端进行解耦,从而提高子系统的独立性和移植性
  • 在层次化的结构中可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度

十三、享元模式

基本思想: 运用共享技术有效地支持大量细粒度对象的复用

享元对象能做到共享的关键是区分了内部状态(Intrinsic State)和外部状态(Extrinsic State):

  • 内部状态是存储在享元对象内部并且不会随环境变换而变化的状态,内部状态可以共享
  • 外部状态是随环境改变而改变的、不可以共享的状态。享元对象的外部状态通常由客户端保存,并在享元对象被创建之后需要使用的时候再传入到享元对象内部。

使用案例:围棋有白子和黑子,棋盘上一般会出现大量的棋子,对每一个棋子都生成一个对象很浪费空间,运用享元模式进行设计,棋子作为享元对象,白和黑是棋子的内部状态,位置和状态作为棋子的外部状态。

享元模式包含如下四种角色:

  • Flyweight(抽象享元类):抽象享元类通常是一个接口或抽象类,在抽象享元类中包含了具体享元类公共的方法,这些方法可以向外部提供了享元类的内部状态,同时也可以通过这些方法来设置享元类的外部状态。
  • ConcreteFlyweight(具体享元类):具体享元类实现了抽象享元类,在具体享元类中为内部状态提供了存储空间。通常结合单例模式来设计具体享元类。
  • UnsharedConcreteFlyweight(非共享具体享元类):并不是所有的抽象享元类都可以被共享,不能被共享的具体享元类被称为非共享具体享元类,当需要一个非共享具体享元类时可以直接实例化它。
  • FlyweightFactory(享元工厂类):用来创建并管理享元对象,它针对抽象享元类进行编程,将抽象享元类储存在一个池中,当需要时,直接从池中获取。

Alt text

单纯享元模式
在单纯享元模式中所有的具体享元类都是可以共享的,不存在非共享具体享元类。

复合享元模式
Alt text
在复合享元模式中,复合具体享元类聚合了抽象享元对象,这些对象接收相同的外部状态,在operation函数中将对这些享元对象用相同的外部状态进行调用。

优点:

  • 享元模式可以减少内存中对象的数量,使得相同或者相似对象在内存中只保存一份,从而可以节约系统资源,提高系统性能
  • 享元模式的外部状态相对独立,而且不会影响到内部状态,从而使享元对象可以在不同环境中被共享

缺点:

  • 享元模式使系统变得更加复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化
  • 为了使对象可以共享,享元模式将部分状态外部化,而读取外部状态将使系统运行时间过长

适用环境:

  • 一个系统有大量相同或者相似的对象,造成内存的大量耗费
  • 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中
  • 需要维护一个享元池,所以需要多次重复使用享元对象时才使用享元模式

案例

  • 字符串常量池
  • Integer常量池 范围[-128, 127]
  • 数据库连接池

十四、代理模式

基本思想: 给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。这样做的好处是可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。

代理模式在很多流行的框架中大量使用,比如Spring Boot。面向切面编程(AOP)也即使用了代理模式。常见的代理形式有远程代理、保护代理、虚拟代理、缓冲代理、智能引用代理等。

代理模式包含如下三种角色:

  • Subject(抽象主题角色):它声明了真实主题和代理主题的共同接口,客户端需要针对抽象主题角色进行编程。
  • Proxy(代理主题角色):它包含了对真实主题角色的引用,代理主题角色提供了与抽象主题角色相同的接口。
  • RealSubject(真实主题角色):它定义了代理主题角色所代表的真实对象,在真实主题角色中实现了真实的业务操作,客户端可以通过代理主题角色间接调用真实主题角色中定义的操作。

Alt text

远程代理
远程代理也可以称为远程过程调用(RPC),是一种在网络通信框架(或分布式架构)中使用非常广泛的技术。JDK也提供了此类技术,被称为RMI(Remote Method Invocation,远程方法调用),它能够实现从一个虚拟机上调用另一台虚拟机上的方法(这两个虚拟机可以在不同机器上)。

虚拟代理
用一个“虚假”的代理对象来代表真实对象,通过代理对象来间接引用真实对象,可以在一定程度上提高系统的性能。

适用场景:

  • 对象本身的复杂性或者网络等原因导致一个对象需要较长的加载时间
  • 当一个对象十分耗费系统资源的时候也非常适合虚拟代理。虚拟代理让那些占用大量内存的对象的加载延迟到适用它们的时候才创建。

Java动态代理
Java动态代理可以使用Proxy类来进行创建,动态代理可以让系统在运行时根据实际需要来动态创建对象。

代理模式优点:

  • 能够协调调用者和被调用者,在一定程度上降低了系统的耦合度
  • 客户端可以针对抽象主题角色进行编程,增加和更换代理类无需修改源代码,符合开闭原则,系统具有较好的灵活性和可扩展性

缺点:

  • 由于在真实对象和客户端之间增加了一层代理对象,所以可能会导致请求的处理速度变慢,例如保护代理
  • 实现代理模式需要额外的工作,有些代理模式实现较为复杂,例如远程代理

适用环境:

  • 当客户端需要访问远程主机上的对象时可以使用远程代理
  • 当需要用一个消耗资源较少的对象来代表一个消耗资源较多的对象,从而降低系统开销
  • 当需要为某一个被频繁访问的操作结果提供一个临时储存空间,以供多个客户端共享访问这些结果时可以使用缓冲代理
  • 当需要控制一个对象的访问需要不同权限时可以使用保护代理
  • 当需要为一个对象的访问提供额外的操作时,可以使用智能引用代理

十五、职责链模式

基本思想 :避免将一个请求的发送者和接受者耦合在一起,让多个对象都有机会处理请求。将处理请求的对象连成一条链,让请求沿着这条链传递,直到有对象能够处理它。

在高性能网络通信框架Netty中,Pipline的设计采用了职责链模式,它将包装Handler的HandlerContext连成了一条双向链,Channel中的读请求从链头传到链尾,写请求从链尾传到链头最终写入Channel中。

职责链包含如下两种角色:

  • Handler(抽象处理者):它定义一个处理请求的接口,一般是一个抽象类,内部定义了一个抽象处理者的对象,当当前对象处理完成后,可以将消息传递给它的下家。
  • ConcreteHandler(具体处理者):抽象处理者的子类,可以处理用户的请求,在具体处理者中实现了抽象处理者中定义的抽象请求处理方法。

Alt text

纯的职责链模式


一个纯的职责链模式要求一个具体处理者只能在两个行为中选择一个,要么承担全部责任,要么将责任推给下家。纯的职责链模式要求一个请求必须被某一个处理器所接收,不能出现某个请求未被任何处理器处理的情况。

不纯的职责链模式


不纯的职责链模式允许请求被某个处理器部分处理后再向下传递,也允许某个请求不被任何处理器所处理。

优点:

  • 请求处理对象仅需维持一个指向其后继者的引用,而不需要维持它对所有的候选处理者的引用,可简化对象之间的相互连接
  • 再给对象分配职责时,职责链模式可以代理极大的灵活性,可以通过在运行时对该链进行动态的增加或修改来增加或改变处理一个请求的职责
  • 在系统中增加一个新的请求处理者无需修改原有系统代码,只需要在客户端重新建链即可,符合开闭原则

缺点:

  • 由于一个请求没有明确的接收者,可能导致请求传递到链的末端仍然没有被处理,一个请求也可能因为职责链没有被正确配置而得不到处理
  • 对于比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响,而且调试比较麻烦
  • 如果建链不当,可能造成死循环

适用环境:

  • 有多个对象可以处理同一个请求,具体哪个对象来处理该请求待运行时刻再确定
  • 可动态指定一组对象处理请求,客户端可以动态创建职责链来处理请求,还可以改变链中处理者之间的先后次序

十六、命令模式

基本思想: 将一个请求封装为一个对象,从而可用不同的请求对客户进行参数化,对请求排队或者记录请求日志,以及支持可撤销的操作。

为了降低系统之间的耦合度,将请求的发送者和接受者解耦,可以使用一种被称为命令模式的设计模式来设计系统,在命令模式中发送者和接收者之间引入了新的命令对象,将发送者的请求封装在命令对象中,在通过命令对象来调用接收者的方法。

命令模式包含下面四个角色:

  • Command(抽象命令类):一般是一个接口或者抽象类,其中声明了用于执行的execute方法
  • ConcreteCommand(具体命令类):具体命令类是抽象命令类的子类,其中实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者的动作绑定在其中。具体命令类在实现execute方法时将调用接收者对象的相关操作(Action)
  • Invoker(调用者):调用方即请求的发送者,它通过命令对象来执行请求。
  • Receiver(接收者):接收者执行与请求相关的操作,具体实现对请求的业务处理。

Alt text

这里采用了面向抽象编程的思想,抽象命令类是对一系列命令的抽象,具体的实现在具体命令类中,具体命令类通过调用接收者类来实现具体的命令功能

这里的具体命令类可以使用组合模式来将一个命令包装成一批命令一起进行处理,也即宏命令

优点:

  • 降低系统的耦合度。由于请求者与接收者之间不存在直接引用,相同的请求者可以对应不同的接收者,同样的接收者也可以对应不同的请求者,两者之间有很好的独立性
  • 新的命令很容易添加到系统中
  • 可以比较容易地设计一个命令队列或宏命令
  • 为请求的撤销和恢复提供了一种设计和实现方案

缺点:

  • 可能会导致过多的具体命令类

适用场景:

  • 系统需要将请求接收者和发送者解耦,是的调用者和接收者不直接交互
  • 系统需要在不同的时间指定请求、将请求排队和执行请求
  • 系统需要支持命令的撤销、恢复
  • 系统需要将一组操作组合在一起形成宏命令

十七、解释器模式

基本思想: 给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器适用该表示来解释语言中的句子。

在解释器模式的定义中所指的“语言”是使用指定规定格式和语法的代码,解释器模式是一种类行为模式。

使用案例:加法/减法解释器
文法规则如下:

expression :: = value | operation
operation :: = expression '+' expression | expression '-' expression
value :: = an integer

符号“::=“是定义为的意思, ”|“表示或。

解释器模式包含以下四个角色

  • AbstractExpression(抽象表达式):在抽象的表达式中声明了抽象的解释操作,它是所有终结符表达式和非终结符表达式的公共父类
  • TerminalExpression(终结符表达式):它实现了与文法中终结符相关的解释操作,在句子中每一个终结符都是该类的一个实例。终结符表达式对象可以通过非终结符表达式组成较为复杂的句子
  • NonterminalExpression(非终结符表达式):它实现了文法中非终结符的解释操作,非终结符表达式可以包含终结符表达式,也可以继续包含非终结符表达式,因此它的解释一般通过递归的方式完成
  • Context(环境类):环境类又称上下文类,它用于存储解释器之外的一些全局信息

Alt text

优点:

  • 易于改变和扩展文法。
  • 每一条文法规则都可以表示为一个类,因此可以很方便地实现一个简单的语言
  • 实现文法较为容易
  • 增加新的解释表达式较为方便

缺点:

  • 对于复杂文法难以维护
  • 执行效率较低

适用环境:

  • 可以将一个需要解释执行的语言中的句子表示为一颗抽象语法树
  • 一些重复出现的问题可以用一种简单的语言进行表达
  • 一个语言的文法较为简单
  • 执行效率不是关键问题。高效的解释器通常不是通过直接解释抽象语法树来实现的,而是通过将它们转换为其他形式,使用解释器模式的效率通常不高

十八、迭代器模式

基本思想: 提供一种方法顺序访问一个聚合对象中的各个元素,而又不用暴露该对象的内部表示。

软件系统中,一般聚合对象拥有两个职责:一是存储数据,二是遍历数据。从依赖性上来说,前者是聚合对象的基本职责,而后者是可以变化的,有时可分离的。因此可以将遍历数据的行为从聚合对象中分离出来,封装在迭代器对象中,由迭代器来负责对聚合对象的遍历,这简化了聚合对象的职责,更符合单一职责原则。

迭代器模式中有如下四个角色:

  • Iterator(抽象迭代器):定义了访问和遍历元素的接口,声明了遍历元素的接口
  • ConcreteIterator(具体迭代器):实现了抽象迭代器接口,完成对聚合对象的遍历
  • Aggregate(抽象聚合类):它用于储存和管理元素对象,声明了一个iter方法来返回一个迭代器对象
  • ConcreteAggregate(具体聚合类):它是抽象聚合类的子类,实现了在抽象聚合类中声明的iter方法,该方法返回一个与该具体聚合类相关联的具体迭代器

Alt text

使用内部类实现迭代器


在Java中可以使用内部类来实现迭代器,因为具体迭代器是和具体聚合类相关联的,且只能通过具体聚合类来生成具体迭代器,因此可以将具体迭代器类作为具体聚合类的内部类,有利于减少类文件数量

Java内置迭代器


Java内置的抽象迭代器接口为Iterator,抽象聚合类一般会实现Iterable接口。

迭代器模式的优点:

  • 迭代器模式支持以不同的方式来遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式
  • 迭代器模式简化了聚合类,减少了聚合类的职责,使聚合类更符合单一职责原则
  • 在迭代器模式中由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无需修改源代码,符合开闭原则

缺点:

  • 一定程度上增加了类的数量,增加了系统的复杂度
  • 抽象迭代器设计难度较大,需要充分考虑到系统将来的扩展

适用环境:

  • 访问一个聚合对象的内容而无需暴露它的内部表示
  • 需要为一个聚合对象提供多种遍历方式
  • 为遍历不同的聚合结构提供一个统一的接口,在该接口的实现类中为不同的聚合结构提供不同的遍历方式,而客户端可以一致性的操作该类

十九、中介者模式

基本思想: 定义一个对象来封装一系列对象的交互。中介者模式使各对象之间不需要显式地相互引用,从而使其耦合松散,而且用户可以独立地改变他们之间的交互。

如果对象之间存在多对多的关系,可以将对象之间的一些交互行为从各个对象中分离出来,集中封装在一个中介者对象中,并由该中介者进行统一协调,这样多对多的复杂关系就转换为相对简单的一对多关系。中介者模式引入了中介者来简化对象之间的复杂交互,中介者模式使迪米特法则(最少知识原则)的一个典型应用。

中介者模式包含如下四种角色:

  • Mediator(抽象中介者):它定义了一个接口,该接口用于同事对象之间的通信
  • ConcreteMediator(具体中介者):它是抽象中介者的子类,通过协调各个同事对象来实现协作行为,它维持了各个同事对象的引用
  • Colleague(抽象同事类):它定义了各个同事类公有的方法,并声明了一些抽象方法供子类实现,同时它维持了一个对抽象中介者的引用,其子类可以通过该引用与中介者进行通信
  • ConcreteColleague(具体同事类):它是抽象同事类的子类,每一个同事对象在需要与其他同时对象通信时先与中介者进行通信,通过中介者间接与其他同事对象完成通信

Alt text

中介者模式的核心在于中介者类的引入,在中介者模式中,中介者类承担了两个方面的职责:

  1. 中转作用:通过中介者类提供的中转功能,各个同事对象不再需要显式地引用其他同事
  2. 协调作用:中介者可以进一步对同事之间的关系进行封装,同事可以一致地和中介者进行交互,而不需要指明中介者需要具体怎么做,中介者根据封装在自身内部的协调逻辑对同事的请求进行进一步处理,将同事成员之间的关系行为进行分离和封装。

优点:

  • 中介者模式简化了对象之间的交互,它用中介者和同事的一对多交互代替了原来同事之间的多对多交互,一对多关系更容易理解、维护和扩展,将原本难以理解的网状结构转换成相对简单的星型结构
  • 可将各同事对象解耦
  • 可以减少子类生成,中介者将原本分布于多个对象间的行为集中在一起,改变这些行为只需生成新的中介者子类即可,这使得各个同事类可以被重用,无须直接对同事类进行扩展

缺点:

  • 在具体中介者类中包含了大量同事之间的交互细节,可能会导致具体中介者类非常复杂,使得系统难以维护

适用环境:

  • 系统中各对象之间存在复杂的引用关系
  • 一个对象直接引用了其他很多对象并且直接和这些对象通信
  • 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类

二十、备忘录模式

基本思想:
在不破坏封装的前提下捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。

备忘录模式提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时可以使用暂时储存起来的备忘录将状态恢复,当前很多软件的Undo(撤销)操作就使用了备忘录模式。

备忘录模式包含如下三个角色:

  • Originator(原发器):原发器是一个普通类,它通过创建一个备忘录来存储当前内部状态,也可以使用备忘录来恢复其内部状态,一般将需要保存内部状态的对象设计为原发器
  • Memento(备忘录):备忘录用来存储原发器的内部状态,由原发器来决定保存哪些内部状态。备忘录的设计一般参考原发器的设计
  • Caretaker(负责人):负责人负责保存备忘录,但不能对备忘录的内容进行操作和检查

Alt text

优点:

  • 提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤
  • 备忘录实现了对信息的封装,一个备忘录对象是一种原发器对象状态的表示,不会被其他代码所改动

缺点:

  • 资源消耗过大,如果要保存的原发器类的成员变量太多,就不可避免的要占用大量内存

适用场景:

  • 需要在某个情况下保存对象的状态,以供后续使用
  • 防止外界对象破坏一个对象历史状态的封装性,避免将对象历史状态的实现细节暴露给外界对象

二十一、观察者模式

基本思想: 定义对象之间的一种一对多的依赖关系,使得每当一个对象状态发生改变时其相关依赖对象皆得到通知并被自动更新。

观察者模式是使用频率较高的设计模式之一。它的别名有:发布-订阅模式、模型-视图模式、源-监听器模式、从属者模式。

观察者模式包含如下四种角色:

  • Suject(目标):目标又称为主题,它是指被观察的对象,在目标中定义了一个观察者集合,一个观察者目标可以接受任意数量观察者,它提供了一系列方法来增加和删除观察者,同时它定义了通知方法。
  • ConcreteSuject(具体目标):具体目标是目标类的子类,它包含经常发生改变的数据,当它的状态发生改变时将向它的观察者发出通知
  • Observer(观察者):观察者将对观察目标的改变做出反应,观察者一般定义为接口,接口中声明了更新数据的方法
  • ConcreteObserver(具体观察者):具体观察者是观察者的子类,实现了在观察者中定义的接口,将对目标状态的改变做出具体的业务处理。在内部维护了一个指向具体目标状态的引用(也可以不引用,通过update方法传入)

Alt text

JDK对观察者模式的支持
JDK中提供了Observable类作为目标类,Observer接口作为观察者。

观察者模式的优点:

  • 可以实现表示层和数据逻辑层的分离,定义了稳定的消息更新传递机制,并抽象了更新接口,使得各种各样不同的表示层充当具体观察者角色
  • 在观察目标和抽象观察者之间建立了一个抽象的耦合
  • 支持广播通信,观察目标会向所有已注册的观察者对象发送通知,简化了一对多系统设计的难度
  • 符合开闭原则,增加新的具体观察者无需修改原有系统代码

缺点:

  • 如果一个观察目标对象有很多直接和间接观察者,将所有的观察者都通知到会花费很多时间
  • 如果在观察者和观察目标之间存在循环依赖,观察目标会触发它们之间进行循环调用,可能导致系统奔溃

适用环境

  • 一个对象的改变将导致一个或多个对象也发生改变,而并不知道具体有多少对象将发生该拜年,也不知道这些个对象是谁

二十二、状态模式

基本思想: 允许一个对象在其内部状态改变时改变它的行为。对象似乎看起来修改了它的类。

状态模式用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题。当系统中的某个对象存在多个状态,这些状态之间可以进行切换,而且对象在不同状态下的行为不相同时可以使用状态模式。状态模式将一个对象的状态从该对象中分离出来,封装到专门的状态类中,使得对象的状态可以灵活变化

状态模式有如下三种角色:

  • Context(环境类):环境类又称上下文类,它是拥有多种状态的对象。在环境类中,将对象的状态抽象为State类,内部保存了State类的引用,这个实例定义了当前的状态,在具体的实现时它是State类的一个子类

  • State(抽象状态类):抽象状态类定义了一个接口以封装与环境类的一个特定状态相关的行为,在抽象状态类中声明了各种不同状态对应的方法,而在其子类中实现了这些方法,由于在不同状态下行为可能不同,因此在不同子类中的方法的实现有可能不同,相同的方法可以写在抽象状态类中

  • ConcreteState(具体状态类):它是抽象状态类的子类,每一个具体状态类都对应一种状态,内部方法实现了在该状态下的行为

Alt text

优点:

  • 状态模式封装了状态的转换规则,在状态模式中可以讲状态的转换代码封装在环境类或者具体状态类中,可以对状态转换代码进行集中管理,而不是分散在一个个业务中去
  • 状态模式将所有与某个状态有关的行为放到一个类中,只需要注入一个不同的状态对象即可让环境对象拥有不同的行为
  • 状态模式允许状态转换逻辑与状态对象合成一体
  • 状态模式可以让多个环境对象共享一个状态对象,从而减少了系统中对象的个数

缺点:

  • 状态模式会增加系统中类和对象的个数,导致系统开销增大
  • 状态模式的结构和实现都较为复杂,如果使用不当将导致程序结构和代码的混乱,增加系统设计的难度
  • 状态模式对开闭原则的支持不太友好,增加新的状态类需要修改状态转换的源代码

适用场景:

  • 对象的行为依赖于它的状态,状态的变化将导致行为的变化
  • 在代码中包含大量与对象状态有关的条件语句,这些条件语句的出现会导致代码的维护性和灵活性变差,不能方便地增加和删除状态,并且导致客户类与类库之间的耦合性增强

二十三、策略模式

基本思想: 定义了一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法可以独立于使用它的客户而变化。

策略模式包含如下三种角色:

  • Context(环境类):环境类是对抽象策略类进行管理的类,在内部存储了一个抽象策略对象的引用,提供给客户端进行调用。
  • Strategy(抽象策略类):抽象策略类为所支持的算法声明了抽象方法。
  • ConcreteStrategy(具体策略类):它是抽象策略类的子类,实现了在抽象策略类中声明的方法,每一种具体策略类都对应一种具体的算法。

Alt text

优点:

  • 策略模式提供了对开闭原则的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为
  • 策略模式提供了管理相关的算法族的办法
  • 策略模式提供了一种可以替换继承关系的办法
  • 使用策略模式可以避免多重条件选择语句

缺点:

  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类
  • 策略模式将造成系统产生很多具体策略类,任何细小的变化都将导致系统要增加一个新的具体策略类
  • 无法同时在客户端使用多个策略类

适用环境:

  • 一个系统要动态地在多个算法中选择一种,那么可以将这些算法用具体策略类来实现,然后在运行时动态从配置文件中选择一种
  • 不希望客户端知道复杂的、与算法相关的数据结构,在具体策略类中封装算法与相关的数据结构,可以提高算法的保密性与安全性

二十四、模版方法模式

基本思想: 定义一个操作中算法的框架,而将一些步骤延迟到子类中。模版方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

模版方法模式是一种基于继承的代码复用技术,它是一种类行为模式。它将复杂的算法流程封装在一系列基本方法中,在子类中只需要实现特定步骤的方法,即可实现不同的算法。比如Netty中的ByteToMessageDecoder抽象类,子类只需要实现decode方法即可,因为在ByteToMessageDecoder中已经实现了一系列解码步骤,在子类中无需关系整个流程,只需要关注具体某个方法即可。

模版方法模式包含如下两种角色:

  • AbstractClass(抽象类):抽象类中定义了一系列基本操作,这些基本操作可以是具体的也可以是抽象的,每一个基本操作对应算法的一个步骤,其子类可以覆盖或者实现它们。同时在抽象类中实现了一个模板方法,用于定义一个算法的框架,模版方法不仅可以调用基本方法,也可以调用抽象类中声明的抽象方法和其他对象的方法。具体包含三种类型的基本方法:抽象方法具体方法钩子方法
  • ConcreteClass(具体子类):它是抽象类的子类,用于实现抽象类声明的抽象方法,也可以覆盖抽象类中实现的基本方法

优点:

  • 在父类中形式化定义一个算法,而由它的子类来实现细节的处理,在子类实现详细的处理算法时并不会改变算法中步骤的指向次序
  • 模版方法模式是一种代码复用技术,在类库设计中尤为重要,比如AbastractList
  • 模版方法可以实现一种反向控制结构,通过子类覆盖父类中的钩子方法来决定某一特定步骤是否需要执行
  • 可以通过子类来覆盖父类的基本方法,不同的子类可以提供基本方法的不同实现,更换和新增子类都很方便,符合开闭原则

缺点:

  • 需要为每一个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统更加庞大,设计也更加抽象,此时可结合桥接模式进行设计

适用场景:

  • 对一些复杂的算法进行分割,将其中固定不变的部分作为基本方法,可以改变的细节由其子类来实现
  • 将子类中公共的行为提取出来,集中到一个公共父类中避免代码重复

案例

  • JDK源码 InputStream类,它通过定义一个抽象的read()方法,让子类来实现该方法,从而实现了反向控制,InputStream类内部也实现了一些通用方法,比如read(byte[], int offset, int len)。
  • JUC中的AbstractQueuedSynchronizer,该类是一个同步框架,提供了一些通用的方法,其子类有CountDownLatchReentrantLock等。

二十五、访问者模式

基本思想: 表示一个作用于某对象结构中的各个元素的操作。访问者模式让用户可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

访问者包含如下五个角色:

  • Visitor(抽象访问者):抽象访问者为对象结构中的每一个具体元素类声明一个访问操作,从这个操作的名称或类型参数可以清楚地知道需要访问的具体元素的类型,具体访问者需要实现这些操作方法,定义对这些元素的访问操作
  • ConcreteVisitor(具体访问这):具体访问者实现了抽象访问者中定义的操作
  • Element(抽象元素):抽象元素一般是抽象类或者接口,它声明了一个accept方法,用于接收访问这的访问操作,该方法通常以一个抽象访问者作为参数
  • ConcreteElement(具体元素):具体元素实现了抽象元素中声明的accept方法,在accept方法中调用了抽象访问者的访问方法以便完成对一个元素的操作
  • ObjectStructure(对象结构):对象结构是一个元素的集合,它用于存放元素对象,并且提供了对内部元素的遍历操作

Alt text

优点:

  • 在访问者模式中增加新的访问操作很方便
  • 访问者模式将有关元素对象的访问行为集中到一个访问者对象中,而不是分散在一个个的元素类中。类的职责更加清晰,有利于对象结构中元素对象的复用。
  • 访问者模式能让用户能够在不修改现有元素类层次结构的情况下定义作用于该层次结构的操作

缺点:

  • 在访问者模式中增加新的元素较为困难
  • 访问者模式破坏了对象的封装性

适用环境:

  • 一个对象结构包含多个类型的对象,希望对这些对象实施一些依赖其具体类型的操作。
  • 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。
  • 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作