设计模式
什么是设计模式
“设计模式是软件设计中常见问题的典型解决方案。 它们就像能根据需求进行调整的预制蓝图, 可用于解决代码中反复出现的设计问题。”
“设计模式与方法或库的使用方式不同, 你很难直接在自己的程序中套用某个设计模式。 模式并不是一段特定的代码, 而是解决特定问题的一般性概念。 你可以根据模式来实现符合自己程序实际所需的解决方案。”
“人们常常会混淆模式和算法, 因为两者在概念上都是已知特定问题的典型解决方案。 但算法总是明确定义达成特定目标所需的一系列步骤, 而模式则是对解决方案的更高层次描述。 同一模式在两个不同程序中的实现代码可能会不一样。”
“算法更像是菜谱: 提供达成目标的明确步骤。 而模式更像是蓝图: 你可以看到最终的结果和模式的功能, 但需要自己确定实现步骤。”
模式包含哪些内容
大部分模式的描述都会遵循特定的形式,以便在不同情况下使用。模式的描述通常会包括以下部分。
- 意图部分简要地描述问题和解决方案。
- 动机部分进一步解释问题并说明模式会如何提供解决方案。
- 结构部分展示模式的各个部分和它们之间的关系。
- 在不同语言中的实现提供流行编程语言的代码,让读者更好地理解模式背后的思想。
部分模式介绍中还列出了其他的一些实用细节,例如模式的适用性、实现步骤以及与其他模式的关系。
模式的分类
- 创建型模式提供创建对象的机制,增加已有代码的灵活性和可复用性。
- 结构型模式介绍如何将对象和类组成较大的结构,并同时保持结构的灵活和高效。
- 行为模式负责对象间的高效沟通和职责委派
软件设计原则
优秀设计的特征
- 代码复用
- 扩展性
SOLID原则
Single Responsibility Principle(单一职责原则)
修改一个类的原因只能有一个。
Open/closed Principle(开闭原则)
对于扩展,类应该是“开放”的,对于修改,类则应该是“封闭”的。
Liskov Substitution Principle(里氏替换原则)
当你扩展一个类时,记住你应该要能在不修改客户端代码的情况下将子类的对象作为父类进行传递。
Iterface Segregation Principle(接口隔离原则)
客户端不应被强迫依赖于其不使用的方法。
Dependency Inversion Principle(依赖倒置原则)
高层次的类不应该依赖于低层次的类。两者都应该依赖于抽象接口。抽象接口不应依赖于具体实现。具体实现应该依赖于抽象接口。
设计模式目录
创建型模式
创建型模式提供了创建对象的机制,能够提升已有代码的灵活性和可复用性
工厂方法
结构
- 产品(Product)将会对接口进行声明。对于所有由创建者及其子类构建的对象,这些接口都是通用的
- 具体产品(Concrete Products)是产品接口的不同实现
- 创建者(Creator)类声明返回产品对象的工厂方法。该方法的返回对象必须与产品接口匹配
- 具体创建者(Concrete Creators)将会重写基础工厂方法,使其发那会不同类型的产品
适用场景
-
当你在编写代码的过程中,如果无法预知对象确切类别及其依赖关系时,可使用工厂方法。
工厂方法将创建产品的代码与实际使用的产品的代码分离,从而能在不影响其他代码的情况下扩展产品创建部分代码
例如:如果需要向应用中添加一种新产品,你只需要开发新的创建者子类,然后重启其工厂方法即可 -
如果你希望用户能扩展你的软件库或框架的内部组件,可使用工厂方法
继承可能是扩展软件库或框架默认行为的最简单方法。但是当你使用资料替代标准组件时,框架如何辨识出该子类?
解决方法是家那个各框架中构造组件的代码集中到单个工厂方法中,并在机场该组件之外允许任何人对该方法进行重写 -
如果你希望复用现有对象来节省系统资源,而不是每次都重新创建对象,可使用工厂方法
实现方式
- 让所有产品都遵循同一接口。该接口必须声明对所有产品都有意义的方法。
- 在创建类中添加一个空的工厂方法。该方法的返回类型必须遵循通用的产品接口
- 在创建者代码中找到对于产品构造函数的所有引用。将他们依次替换为对于工厂方法的调用,同时将创建产品的代码移入工厂方法。你可能需要在工厂方法中添加参数来控制返回的产品类型
- 现在,为工厂方法中的每种产品编写一个创建者子类,然后在子类中重写工厂方法,将基本方法中的相关创建代码移动到工厂方法中
- 如果产品中的产品类型太多,那么为每个产品创建子类并无太大必要,这时你也可以在子类中复用基类中的控制参数
- 如果代码经过上述移动后,基础工厂方法中已经没有任何代码,你可以将其转变为抽象类。如果基础工厂方法中海油其他语句,你可以将其设置为该方法的默认行为
优缺点
优点:
- 你可以避免创建者和具体产品直接的紧密耦合
- 单一职责原则。你可以将产品创建代码发那个在程序的单一位置,从而使得代码更容易维护。
- 开闭原则。无需更改现有客户端带啊吗,你就可以在程序中引入新的产品类型
缺点:
1.应用工厂方法模式需要引入许多新的子类,代码可能因此变得更复杂。最好的情况是将该模式引入创建者类的现有层次结构中。
与其他模式关系
- 在许多设计工作的初期都会使用工厂方法(较为简单,而且可以更方便地通过子类进行定制),随后演化为使用抽象工厂、原型和生成器(更灵活但更加复杂)
- 抽象工厂模式通常基于一组工厂方法,但你也可以使用原型模式俩生成这些类的方法。
- 你可以同时使用工厂方法和迭代器来让子类集合返回不同类型的迭代器,并使得迭代器与集合想匹配
- 原型并不基于继承,因此没有继承的缺点。另一方面,原型需要对被复制对象进行复杂的初始化。工厂方法基于继承,但是它不需要初始化步骤。
- 工厂方法是模板方法的一种特殊形式。同时,工厂方法可以作为一个大型模板方法中的一个步骤。
抽象工厂
结构
- 抽象产品(Abstract Product)为构成系列产品的一组不同但相关的产品声明接口。
- 具体产品(Concrete Product)是抽象产品的多种不同类型实现。所有变体(维多利亚/现代)都必须实现相应的抽象产品(椅子/沙发)。
- 抽象工厂(Abstract Factory)接口声明了一组创建各种抽象产品的方法。
- 具体工厂(Concrete Factory)实现抽象工厂的构建方法。每个具体工厂都对应特定产品变体,且仅创建此种产品变体。
- 尽管具体工厂会对具体产品进行初始化,其构建方法签名必须返回响应的抽象产品。这样,使用工厂类的客户端啊代码就不会与工厂创建的特定产品变体耦合。客户端(Client)只需通过接口调用工厂和产品对象,就能与 任何具体工厂/产品变体交互
适用场景
-
如果代码需要与多个不同系列的相关产品交互,但是由于无法提前获取相关信息,或者出于对未来扩展性的考虑,你不希望代码基于产品的具体类进行构建,在这种情况下,你可以使用抽象工厂
抽象工厂为你提供了一个接口,可用于创建啊每个系列产品的对象。只要代码通过该接口创建对象,那么你就不会生成与应用程序已生成的产品类型不一致的产品 -
如果你有一个基于一组抽象方法的类,且其主要功能因此变得不明确,那么在这种情况下可以考虑使用抽象工厂模式
在设计良好的程序中,每个类仅负责一件事。如果一个类与多种类型产品交互,就可以考虑将工厂方法抽取到独立的工厂类或具有完整功能的抽象工厂类中。
实现方式
- 以不同的产品类型与产品变体为维度绘制矩阵。
- 为所有产品声明抽象产品接口。然后让所有具体产品类实现这些接口。
- 声明抽象工厂接口,并且在接口中为所有抽象产品提供一组构建方法。
- 为每种产品变体实现一个具体工厂类。
- 在应用程序中开发初始化代码。该代码根据应用程序配置或当前环境,对特定具体工厂类进行初始化。然后将该工厂对象传递给所有需要创建产品的类。
- 找出代码中所有对产品构造函数的直接调用,将其替换为对工厂对象中相应构建方法的调用。
优缺点
优点:
- 你可以确保同一工厂生产的产品相互匹配。
- 你可以避免客户端和具体产品代码的耦合。
- 单一职责原则。你可以将产品生产代码抽取到同一位置,使得代码易于维护。
- 开闭原则。向应用程序中引入新产品变体时,你无需修改客户端代码。
缺点:
1.由于采用该模式需要向应用中引入众多接口和类,代码可能会比之前更加复杂
与其他模式的关系
- 在许多设计工作的初期都会使用工厂方法(较为简单,而且可以更方便地通过子类进行定制),随后演化为使用抽象工厂、原型和生成器(更灵活但更加复杂)
- 生成器重点关注如何分布生成复杂对象。抽象工厂专门用于生产一系列相关对象。抽象工厂会马上返回产品,生成器则允许你在获取产品前执行一些额外构造步骤
- 抽象工厂模式通常基于一组工厂方法,但你也可以使用原型模式来生成这些类的方法。
- 当只徐亚奥对客户端代码隐藏子系统创建对象的方式时,你可以使用抽象工厂来代替外观。
- 你可以将抽象工厂和桥接搭配使用。如果由桥接定义的抽象只能与特定实现合作,这一模式搭配就非常有用。在这种模式下,抽象工厂可以对这些关系进行封装,并且对客户端代码隐藏复杂性。
- 抽象工厂、生成器和原型都可以用单例来实现
生成器
结构
- 生成器(Builder)接口声明在所有类型生成器中通用的产品构造步骤。
- 具体生成器(Concrete Builders)提供构造过程的不同实现,具体生成器也可以构造不遵循通用接口的产品。
- 产品(Products)是最终生成的对象。由不同生成器够构造的产品无需属于同一类层次结构或接口。
- 主管(Director)类定义调用构造步骤的顺序,这样你就可以创建和复用特定的产品配置。
- 客户端(Client)必须将某个生成器对象与主管类关联。一般情况下,你只需要通过主管类构造函数的参数进行一次性关联即可。此后主管类就能使用生成器对象完成后续的构造任务。但在客户端将生成器对象传递给主管类制造方法时还有另一种方式。在这种情况下,你在使用主管类生产产品时都可以使用不同的生成器
适合应用场景
-
使用生成器模式可避免"重叠构造函数(telescopic constructor)"的出现
假设你的构造函数中有十个可选参数,那么调用该函数会非常不方便;因此,你需要重载这个构造函数,新建几个只有较少参数的简化版。但这些构造函数扔需要调用主构造函数,传递一些默认数值来代替省略掉的参数 -
当你希望使用代码创建不同形式的产品(例如石头或木头房屋)时,可以使用生成器模式
如果你需要创建的各种各样的产品,它们的制造过程相似 且仅有细节上的差异,此时可使用生成器模式
基本生成器接口中定义了所有可能的制造步骤,且具体生成器将实现这些步骤来制造特定形式的产品。同时,主管类将负责管理制造步骤的顺序 -
使用生成器构造组合树或其他复杂对象
生成器模式让你能分步骤构造产品。你可以延迟执行某些步骤而不会影响最终产品。你甚至可以递归调用这些步骤,这在创建对象树时非常方便。
生成器在执行制造步骤时,不能对外发布未完成的产品。这可以避免客户端代码获取到不完整结果对象的情况
实现方法
- 清晰地定义通用步骤,确保他们可以制造所有形式的产品。否则你将无法进一步实施该模式。
- 在基本生成器接口中声明这些步骤
- 为每个形式的产品创建具体生成器类,并实现其构造步骤。
不要忘记实现获取构造结果对象的方法。你不能在生成器接口中声明该方法,因为不同生成器构造的产品可能没有公共接口,因此你就不知道该方法返回的对象类型。但是,如果所有产品都位于单一类层次中,你就可以安全地在基本接口中添加获取生成对象的方法 - 考虑创建主管类。它可以使用同一生成器对象来封装多种构造产品的方式
- 客户端代码会同时创建生成器和主管对象。构造开始前,客户端必须将生成器对象传递给主管对象。通常情况下,客户端只需要调用主管类构造函数一次即可。主管类使用生成器对象完成后续所有制造任务。还有另一种方式,那就是客户端可以将生成器对象直接传递给主管类的制造方法。
- 只有在所有产品都遵循相同接口的情况下,构造结果可以直接通过主管类获取。否则,客户端应当通过生成器获取构造结果。
优缺点
优点:
- 你可以分布创建对象,暂缓创建步骤或递归运行创建步骤。
- 生成不同形式的产品时,你可以复用相同的制造代码。
- 单一职责原则。你可以将复杂构造代码从产品的业务逻辑分离出来
缺点: - 由于该模式需要新增多个类,因此代码整体复杂程度会有所增加。
与其他模式关系
- 在许多设计工作的初期都会使用工厂方法 (较为简单, 而且可以更方便地通过子类进行定制), 随后演化为使用抽象工厂、 原型或生成器 (更灵活但更加复杂)。
- 生成器重点关注如何分步生成复杂对象。 抽象工厂专门用于生产一系列相关对象。 抽象工厂会马上返回产品, 生成器则允许你在获取产品前执行一些额外构造步骤。
- 你可以在创建复杂组合树时使用生成器,因为这可使其构造步骤以递归的方式运行。
- 你可以结合使用生成器和桥接模式:主管类负责抽象工作,各种不同的生成器负责实现工作。
- 抽象工厂、生成器、和原型都可以用单例来实现。
原型
结构
- 原型(Prototype)接口将对象克隆方法进行声明。在绝大多数情况下,其中只会有一个名为clone克隆的方法。
- 具体原型(Concrete Prototype)类将实现克隆方法。除了将原始对象的数据复制到克隆体中之外,该方法有时还需处理克隆过程中的极端情况,例如克隆关联对象和梳理递归依赖等等。
- 客户端(Client)可以实现复制实现了原型接口的任何对象
原型注册表的实现
- 原型注册表(Prototype Registry)提供了一种访问常用原型的简单方法,其中存储了一系列可供随时复制预生成对象。最简单的注册表原型是一个名称->原型的哈希表。但如果需要使用名称以外的条件进行搜索,你可以创建更加完善的注册表版本
适合应用场景
- 如果你需要复制一些对象, 同时又希望代码独立于这些对象所属的具体类, 可以使用原型模式。
这一点考量通常出现在代码需要处理第三方代码通过接口传递过来的对象时。 即使不考虑代码耦合的情况, 你的代码也不能依赖这些对象所属的具体类, 因为你不知道它们的具体信息。
原型模式为客户端代码提供一个通用接口, 客户端代码可通过这一接口与所有实现了克隆的对象进行交互, 它也使得客户端代码与其所克隆的对象具体类独立开来。
- 如果子类的区别仅在于其对象的初始化方式, 那么你可以使用该模式来减少子类的数量。 别人创建这些子类的目的可能是为了创建特定类型的对象。
在原型模式中, 你可以使用一系列预生成的、 各种类型的对象作为原型。
客户端不必根据需求对子类进行实例化,只需找到合适的原型并对其进行克隆即可。
实现方法
- 创建原型接口, 并在其中声明 克隆方法。 如果你已有类层次结构, 则只需在其所有类中添加该方法即可。
- 原型类必须另行定义一个以该类对象为参数的构造函数。 构造函数必须复制参数对象中的所有成员变量值到新建实体中。 如果你需要修改子类, 则必须调用父类构造函数, 让父类复制其私有成员变量值。
如果编程语言不支持方法重载, 那么你可能需要定义一个特殊方法来复制对象数据。 在构造函数中进行此类处理比较方便, 因为它在调用 new运算符后会马上返回结果对象。 - 克隆方法通常只有一行代码: 使用 new运算符调用原型版本的构造函数。 注意, 每个类都必须显式重写克隆方法并使用自身类名调用 new运算符。 否则, 克隆方法可能会生成父类的对象。
- 你还可以创建一个中心化原型注册表, 用于存储常用原型。
你可以新建一个工厂类来实现注册表, 或者在原型基类中添加一个获取原型的静态方法。 该方法必须能够根据客户端代码设定的条件进行搜索。 搜索条件可以是简单的字符串, 或者是一组复杂的搜索参数。 找到合适的原型后, 注册表应对原型进行克隆, 并将复制生成的对象返回给客户端。
最后还要将对子类构造函数的直接调用替换为对原型注册表工厂方法的调用。
优缺点
优点:
- 你可以克隆对象, 而无需与它们所属的具体类相耦合。
- 你可以克隆预生成原型, 避免反复运行初始化代码。
- 你可以更方便地生成复杂对象。
- 你可以用继承以外的方式来处理复杂对象的不同配置。
缺点:
1.克隆包含循环引用的复杂对象可能会非常麻烦。
与其他模式的关系
- 在许多设计工作的初期都会使用工厂方法 (较为简单, 而且可以更方便地通过子类进行定制), 随后演化为使用抽象工厂、 原型或生成器 (更灵活但更加复杂)。
- 抽象工厂模式通常基于一组工厂方法, 但你也可以使用原型模式来生成这些类的方法。
- 原型可用于保存命令的历史记录。
- 大量使用组合和装饰的设计通常可从对于原型的使用中获益。 你可以通过该模式来复制复杂结构, 而非从零开始重新构造。
- 原型并不基于继承, 因此没有继承的缺点。 另一方面, 原型需要对被复制对象进行复杂的初始化。 工厂方法基于继承, 但是它不需要初始化步骤。
- 有时候原型可以作为备忘录的一个简化版本, 其条件是你需要在历史记录中存储的对象的状态比较简单, 不需要链接其他外部资源, 或者链接可以方便地重建。
- 抽象工厂、生成器和原型都可以用单例来实现。
单例
结构
- 单例 (Singleton) 类声明了一个名为 getInstance获取实例的静态方法来返回其所属类的一个相同实例。
单例的构造函数必须对客户端 (Client) 代码隐藏。 调用 获取实例方法必须是获取单例对象的唯一方式。
适用场景
-
如果程序中的某个类对于所有客户端只有一个可用的实例, 可以使用单例模式。
单例模式禁止通过除特殊构建方法以外的任何方式来创建自身类的对象。 该方法可以创建一个新对象, 但如果该对象已经被创建, 则返回已有的对象。 -
如果你需要更加严格地控制全局变量, 可以使用单例模式。
单例模式与全局变量不同, 它保证类只存在一个实例。 除了单例类自己以外, 无法通过任何方式替换缓存的实例。
实现方式
- 在类中添加一个私有静态成员变量用于保存单例实例。
- 声明一个公有静态构建方法用于获取单例实例。
- 在静态方法中实现"延迟初始化"。 该方法会在首次被调用时创建一个新对象, 并将其存储在静态成员变量中。 此后该方法每次被调用时都返回该实例。
- 将类的构造函数设为私有。 类的静态方法仍能调用构造函数, 但是其他对象不能调用。
5.检查客户端代码, 将对单例的构造函数的调用替换为对其静态构建方法的调用。
优缺点
优点:
- 你可以保证一个类只有一个实例。
- 你获得了一个指向该实例的全局访问节点。
- 仅在首次请求单例对象时对其进行初始化。
缺点: - 违反了_单一职责原则_。 该模式同时解决了两个问题。
- 单例模式可能掩盖不良设计, 比如程序各组件之间相互了解过多等。
- 该模式在多线程环境下需要进行特殊处理, 避免多个线程多次创建单例对象。
- 单例的客户端代码单元测试可能会比较困难, 因为许多测试框架以基于继承的方式创建模拟对象。 由于单例类的构造函数是私有的, 而且绝大部分语言无法重写静态方法, 所以你需要想出仔细考虑模拟单例的方法。 要么干脆不编写测试代码, 或者不使用单例模式。
与其他模式的关系
- 外观类通常可以转换为单例类, 因为在大部分情况下一个外观对象就足够了。
- 如果你能将对象的所有共享状态简化为一个享元对象, 那么享元就和单例类似了。 但这两个模式有两个根本性的不同。
只会有一个单例实体, 但是享元类可以有多个实体, 各实体的内在状态也可以不同。
单例对象可以是可变的。 享元对象是不可变的。 - 抽象工厂、生成器和原型都可以用单例来实现
结构型模式
适配器
结构
对象适配器
实现时使用了构成原则:适配器实现了其中一个对象的接口,并对另一个对象进行封装。所有流行的编程语言都可以实现适配器
- 客户端 (Client) 是包含当前程序业务逻辑的类。
- 客户端接口 (Client Interface) 描述了其他类与客户端代码合作时必须遵循的协议。
- 服务 (Service) 中有一些功能类 (通常来自第三方或遗留系统)。 客户端与其接口不兼容, 因此无法直接调用其功能。
- 适配器 (Adapter) 是一个可以同时与客户端和服务交互的类: 它在实现客户端接口的同时封装了服务对象。 适配器接受客户端通过适配器接口发起的调用, 并将其转换为适用于被封装服务对象的调用。
- 客户端代码只需通过接口与适配器交互即可, 无需与具体的适配器类耦合。 因此, 你可以向程序中添加新类型的适配器而无需修改已有代码。 这在服务类的接口被更改或替换时很有用: 你无需修改客户端代码就可以创建新的适配器类。
类适配器
这一实现使用了继承机制:适配器同时继承两个对象接口。请注意,这种方式仅能在支持多重继承的编程语言中实现,例如 C++
- 类适配器不需要封装任何对象, 因为它同时继承了客户端和服务的行为。 适配功能在重写的方法中完成。 最后生成的适配器可替代已有的客户端类进行使用。
适用场景
-
当你希望使用某个类, 但是其接口与其他代码不兼容时, 可以使用适配器类。
适配器模式允许你创建一个中间层类, 其可作为代码与遗留类、 第三方类或提供怪异接口的类之间的转换器。 -
如果您需要复用这样一些类, 他们处于同一个继承体系, 并且他们又有了额外的一些共同的方法, 但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。
你可以扩展每个子类, 将缺少的功能添加到新的子类中。 但是, 你必须在所有新子类中重复添加这些代码, 这样会使得代码有坏味道。
将缺失功能添加到一个适配器类中是一种优雅得多的解决方案。 然后你可以将缺少功能的对象封装在适配器中, 从而动态地获取所需功能。 如要这一点正常运作, 目标类必须要有通用接口, 适配器的成员变量应当遵循该通用接口。 这种方式同装饰模式非常相似。
实现方式
- 确保至少有两个类的接口不兼容:
一个无法修改 (通常是第三方、 遗留系统或者存在众多已有依赖的类) 的功能性服务类。
一个或多个将受益于使用服务类的客户端类。 - 声明客户端接口, 描述客户端如何与服务交互。
- 创建遵循客户端接口的适配器类。 所有方法暂时都为空。
- 在适配器类中添加一个成员变量用于保存对于服务对象的引用。 通常情况下会通过构造函数对该成员变量进行初始化, 但有时在调用其方法时将该变量传递给适配器会更方便。
- 依次实现适配器类客户端接口的所有方法。 适配器会将实际工作委派给服务对象, 自身只负责接口或数据格式的转换。
- 客户端必须通过客户端接口使用适配器。 这样一来, 你就可以在不影响客户端代码的情况下修改或扩展适配器。
优缺点
优点:
- 单一职责原则_你可以将接口或数据转换代码从程序主要业务逻辑中分离。
- 开闭原则。 只要客户端代码通过客户端接口与适配器进行交互, 你就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。
缺点: - 代码整体复杂度增加, 因为你需要新增一系列接口和类。 有时直接更改服务类使其与其他代码兼容会更简单。
与其他模式的关系
- 桥接通常会于开发前期进行设计, 使你能够将程序的各个部分独立开来以便开发。 另一方面, 适配器通常在已有程序中使用, 让相互不兼容的类能很好地合作。
- 适配器可以对已有对象的接口进行修改, 装饰则能在不改变对象接口的前提下强化对象功能。 此外, 装饰还支持递归组合, 适配器则无法实现。
- 适配器能为被封装对象提供不同的接口, 代理能为对象提供相同的接口, 装饰则能为对象提供加强的接口。
- 外观为现有对象定义了一个新接口, 适配器则会试图运用已有的接口。 适配器通常只封装一个对象, 外观通常会作用于整个对象子系统上。
- 桥接、 状态和策略 (在某种程度上包括适配器) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。
桥接
结构
- 抽象部分 (Abstraction) 提供高层控制逻辑, 依赖于完成底层实际工作的实现对象。
- 实现部分 (Implementation) 为所有具体实现声明通用接口。 抽象部分仅能通过在这里声明的方法与实现对象交互。
抽象部分可以列出和实现部分一样的方法, 但是抽象部分通常声明一些复杂行为,
这些行为依赖于多种由实现部分声明的原语操作。 - 具体实现 (Concrete Implementations) 中包括特定于平台的代码。
- 精确抽象 (Refined Abstraction) 提供控制逻辑的变体。 与其父类一样, 它们通过通用实现接口与不同的实现进行交互。
- 通常情况下, 客户端 (Client) 仅关心如何与抽象部分合作。 但是, 客户端需要将抽象对象与一个实现对象连接起来。
使用场景
-
如果你想要拆分或重组一个具有多重功能的庞杂类 (例如能与多个数据库服务器进行交互的类), 可以使用桥接模式。
类的代码行数越多, 弄清其运作方式就越困难, 对其进行修改所花费的时间就越长。 一个功能上的变化可能需要在整个类范围内进行修改, 而且常常会产生错误, 甚至还会有一些严重的副作用。
桥接模式可以将庞杂类拆分为几个类层次结构。 此后, 你可以修改任意一个类层次结构而不会影响到其他类层次结构。 这种方法可以简化代码的维护工作, 并将修改已有代码的风险降到最低。 -
如果你希望在几个独立维度上扩展一个类, 可使用该模式。
桥接建议将每个维度抽取为独立的类层次。 初始类将相关工作委派给属于对应类层次的对象, 无需自己完成所有工作。 -
如果你需要在运行时切换不同实现方法,可使用桥接模式。
当然并不是说一定要实现这一点, 桥接模式可替换抽象部分中的实现对象, 具体操作就和给成员变量赋新值一样简单。
顺便提一句, 最后一点是很多人混淆桥接模式和策略模式的 主要原因。 记住, 设计模式并不仅是一种对类进行组织的方 式, 它还能用于沟通意图和解决问题。
实现方式
- 明确类中独立的维度。 独立的概念可能是: 抽象/平台, 域/ 基础设施, 前端/后端或接口/实现。
- 了解客户端的业务需求, 并在抽象基类中定义它们。
- 确定在所有平台上都可执行的业务。 并在通用实现接口中声 明抽象部分所需的业务。
- 为你域内的所有平台创建实现类, 但需确保它们遵循实现部 分的接口。
- 在抽象类中添加指向实现类型的引用成员变量。 抽象部分会 将大部分工作委派给该成员变量所指向的实现对象。
- 如果你的高层逻辑有多个变体, 则可通过扩展抽象基类为每 个变体创建一个精确抽象。
- 客户端代码必须将实现对象传递给抽象部分的构造函数才能 使其能够相互关联。 此后, 客户端只需与抽象对象进行交互, 无需和实现对象打交道。
优缺点
优点:
- 你可以创建与平台无关的类和程序。
- 客户端代码仅与高层抽象部分进行互动, 不会接触到平台的 详细信息。
- 开闭原则。 你可以新增抽象部分和实现部分, 且它们之间不会相互影响。
- 单一职责原则。 抽象部分专注于处理高层逻辑, 实现部分处 理平台细节。
缺点:
- 对高内聚的类使用该模式可能会让代码更加复杂。
与其他模式的关系
- 桥接通常会于开发前期进行设计, 使你能够将程序的各个部 分独立开来以便开发。 另一方面, 适配器通常在已有程序中 使用, 让相互不兼容的类能很好地合作。
- 桥接、状态和策略(在某种程度上包括适配器)模式的接口 非常相似。 实际上, 它们都基于组合模式——即将工作委派 给其他对象, 不过也各自解决了不同的问题。 模式并不只是 以特定方式组织代码的配方, 你还可以使用它们来和其他开 发者讨论模式所解决的问题。
- 你可以将抽象工厂和桥接搭配使用。 如果由桥接定义的抽象 只能与特定实现合作, 这一模式搭配就非常有用。 在这种情 况下, 抽象工厂可以对这些关系进行封装, 并且对客户端代 码隐藏其复杂性。
- 你可以结合使用生成器和桥接模式: 主管类负责抽象工作, 各种不同的生成器负责实现工作。
组合
结构
- 组件(Component)接口描述了树中简单项目和复杂项目所 共有的操作。
- 叶节点(Leaf)是树的基本结构,它不包含子项目。 一般情况下, 叶节点最终会完成大部分的实际工作, 因为它们无法将工作指派给其他部分。
- 容器(Container)——又名“组合(Composite)”——是包含叶 节点或其他容器等子项目的单位。 容器不知道其子项目所属 的具体类, 它只通过通用的组件接口与其子项目交互。容器接收到请求后会将工作分配给自己的子项目, 处理中间
结果, 然后将最终结果返回给客户端。 - 客户端(Client)通过组件接口与所有项目交互。因此,客户端能以相同方式与树状结构中的简单或复杂项目交互。
适用场景
-
如果你需要实现树状对象结构,可以使用组合模式。
组合模式为你提供了两种共享公共接口的基本元素类型: 简 单叶节点和复杂容器。 容器中可以包含叶节点和其他容器。 这使得你可以构建树状嵌套递归对象结构。 -
如果你希望客户端代码以相同方式处理简单和复杂元素,可以使用该模式。
组合模式中定义的所有元素共用同一个接口。 在这一接口的 帮助下, 客户端不必在意其所使用的对象的具体类。
实现方式
- 确保应用的核心模型能够以树状结构表示。 尝试将其分解为 简单元素和容器。 记住, 容器必须能够同时包含简单元素和 其他容器。
- 声明组件接口及其一系列方法, 这些方法对简单和复杂元素 都有意义。
- 创建一个叶节点类表示简单元素。 程序中可以有多个不同的 叶节点类。
- 创建一个容器类表示复杂元素。 在该类中, 创建一个数组成 员变量来存储对于其子元素的引用。 该数组必须能够同时保 存叶节点和容器, 因此请确保将其声明为组合接口类型。
实现组件接口方法时, 记住容器应该将大部分工作交给其子 元素来完成。 - 最后, 在容器中定义添加和删除子元素的方法。
优缺点
优点:
- 你可以利用多态和递归机制更方便地使用复杂树结构。
- 开闭原则。 无需更改现有代码, 你就可以在应用中添加新元素, 使其成为对象树的一部分。
缺点: - 对于功能差异较大的类, 提供公共接口或许会有困难。 在特定情况下, 你需要过度一般化组件接口, 使其变得令人难以理解。
与其他模式的关系
- 桥接、状态和策略(在某种程度上包括适配器)模式的接口 非常相似。 实际上, 它们都基于组合模式——即将工作委派 给其他对象, 不过也各自解决了不同的问题。 模式并不只是 以特定方式组织代码的配方, 你还可以使用它们来和其他开 发者讨论模式所解决的问题。
- 你可以在创建复杂组合树时使用生成器, 因为这可使其构造 步骤以递归的方式运行。
- 责任链通常和组合模式结合使用。 在这种情况下, 叶组件接 收到请求后, 可以将请求沿包含全体父组件的链一直传递至 对象树的底部。
- 你可以使用迭代器来遍历组合树。
- 你可以使用访问者对整个组合树执行操作。
- 你可以使用享元实现组合树的共享叶节点以节省内存。
- 组合和装饰的结构图很相似, 因为两者都依赖递归组合来组 织无限数量的对象。
装饰类似于组合, 但其只有一个子组件。 此外还有一个明显 不同: 装饰为被封装对象添加了额外的职责, 组合仅对其子 节点的结果进行了“求和”。
但是, 模式也可以相互合作: 你可以使用装饰来扩展组合树 中特定对象的行为。 - 大量使用组合和装饰的设计通常可从对于原型的使用中获益。 你可以通过该模式来复制复杂结构, 而非从零开始重新构造。
装饰
结构
- 部件(Component)声明封装器和被封装对象的公用接口。
- 具体部件(Concrete Component)类是被封装对象所属的类。
它定义了基础行为, 但装饰类可以改变这些行为。 - 基础装饰(Base Decorator)类拥有一个指向被封装对象的引 用成员变量。 该变量的类型应当被声明为通用部件接口, 这样它就可以引用具体的部件和装饰。 装饰基类会将所有操作委派给被封装的对象。
- 具体装饰类(Concrete Decorators)定义了可动态添加到部 件的额外行为。 具体装饰类会重写装饰基类的方法, 并在调 用父类方法之前或之后进行额外的行为。
- 客户端(Client)可以使用多层装饰来封装部件,只要它能 使用通用接口与所有对象互动即可。
适用场景
-
如果你希望在无需修改代码的情况下即可使用对象,且希望在运行时为对象新增额外的行为,可以使用装饰模式。
装饰能将业务逻辑组织为层次结构, 你可为各层创建一个装 饰, 在运行时将各种不同逻辑组合成对象。 由于这些对象都 遵循通用接口, 客户端代码能以相同的方式使用这些对象。 -
如果用继承来扩展对象行为的方案难以实现或者根本不可行, 你可以使用该模式。
许多编程语言使用 final 最终 关键字来限制对某个类的进 一步扩展。 复用最终类已有行为的唯一方法是使用装饰模式: 用封装器对其进行封装。
实现方式
- 确保业务逻辑可用一个基本组件及多个额外可选层次表示。
- 找出基本组件和可选层次的通用方法。 创建一个组件接口并 在其中声明这些方法。
- 创建一个具体组件类, 并定义其基础行为。
- 创建装饰基类, 使用一个成员变量存储指向被封装对象的引 用。 该成员变量必须被声明为组件接口类型, 从而能在运行 时连接具体组件和装饰。 装饰基类必须将所有工作委派给被 封装的对象。
- 确保所有类实现组件接口
- 将装饰基类扩展为具体装饰。 具体装饰必须在调用父类方法
(总是委派给被封装对象) 之前或之后执行自身的行为。 - 客户端代码负责创建装饰并将其组合成客户端所需的形式
优缺点
优点:
- 你无需创建新子类即可扩展对象的行为。
- 你可以在运行时添加或删除对象的功能。
- 你可以用多个装饰封装对象来组合几种行为。
- 单一职责原则。 你可以将实现了许多不同行为的一个大类拆 分为多个较小的类。
缺点: - 在封装器栈中删除特定封装器比较困难。
- 实现行为不受装饰栈顺序影响的装饰比较困难。
- 各层的初始化配置代码看上去可能会很糟糕。
与其他模式关系
- 适配器可以对已有对象的接口进行修改, 装饰则能在不改变 对象接口的前提下强化对象功能。 此外, 装饰还支持递归组 合, 适配器则无法实现。
- 适配器能为被封装对象提供不同的接口, 代理能为对象提供 相同的接口, 装饰则能为对象提供加强的接口。
- 责任链和装饰模式的类结构非常相似。 两者都依赖递归组合 将需要执行的操作传递给一系列对象。 但是, 两者有几点重 要的不同之处。
责任链的管理者可以相互独立地执行一切操作, 还可以随时 停止传递请求。 另一方面, 各种装饰可以在遵循基本接口的 情况下扩展对象的行为。 此外, 装饰无法中断请求的传递。
• 组合和装饰的结构图很相似, 因为两者都依赖递归组合来组 织无限数量的对象。
装饰类似于组合, 但其只有一个子组件。 此外还有一个明显 不同: 装饰为被封装对象添加了额外的职责, 组合仅对其子 节点的结果进行了“求和”。
但是, 模式也可以相互合作: 你可以使用装饰来扩展组合树 中特定对象的行为。
• 大量使用组合和装饰的设计通常可从对于原型的使用中获益。 你可以通过该模式来复制复杂结构, 而非从零开始重新构造。
• 装饰可让你更改对象的外表, 策略则让你能够改变其本质。
• 装饰和代理有着相似的结构, 但是其意图却非常不同。 这两 个模式的构建都基于组合原则, 也就是说一个对象应该将部 分工作委派给另一个对象。 两者之间的不同之处在于代理通 常自行管理其服务对象的生命周期, 而装饰的生成则总是由 客户端进行控制。
外观
结构
- 外观(Facade)提供了一种访问特定子系统功能的便捷方式,
其了解如何重定向客户端请求, 知晓如何操作一切活动部件。 - 创建附加外观(Additional Facade)类可以避免多种不相关的功能污染单一外观,使其变成又一个复杂结构。 客户端和 其他外观都可使用附加外观。
- 复杂子系统(Complex Subsystem)由数十个不同对象构成。如果要用这些对象完成有意义的工作,你必须深入了解子系统的实现细节,比如按照正确顺序初始化对象和为其提供正确格式的数据。
子系统类不会意识到外观的存在,它们在系统内运作并且相互之间可直接进行交互。 - 客户端(Client)使用外观代替对子系统对象的直接调用。
适用场景
-
如果你需要一个指向复杂子系统的直接接口,且该接口的功能有限,则可以使用外观模式。
子系统通常会随着时间的推进变得越来越复杂。 即便是应用 了设计模式, 通常你也会创建更多的类。 尽管在多种情形中 子系统可能是更灵活或易于复用的, 但其所需的配置和样板 代码数量将会增长得更快。 为了解决这个问题, 外观将会提 供指向子系统中最常用功能的快捷方式, 能够满足客户端的 大部分需求。 -
如果需要将子系统组织为多层结构,可以使用外观
创建外观来定义子系统中各层次的入口。 你可以要求子系统
仅使用外观来进行交互, 以减少子系统之间的耦合。
让我们回到视频转换框架的例子。 该框架可以拆分为两个层 次: 音频相关和视频相关。 你可以为每个层次创建一个外观, 然后要求各层的类必须通过这些外观进行交互。 这种方式看 上去与中介者模式非常相似。
实现方式
- 考虑能否在现有子系统的基础上提供一个更简单的接口。 如果该接口能让客户端代码独立于众多子系统类,那么你的方 向就是正确的。
- 在一个新的外观类中声明并实现该接口。外观应将客户端代码的调用重定向到子系统中的相应对象处。 如果客户端代码 没有对子系统进行初始化,也没有对其后续生命周期进行管理,那么外观必须完成此类工作。
- 如果要充分发挥这一模式的优势,你必须确保所有客户端代码仅通过外观来与子系统进行交互。 此后客户端代码将不会 受到任何由子系统代码修改而造成的影响, 比如子系统升级后,你只需修改外观中的代码即可。
- 如果外观变得过于臃肿, 你可以考虑将其部分行为抽取为一个新的专用外观类。
优缺点
优点:
- 你可以让自己的代码独立于复杂子系统。
缺点: - 外观可能成为与程序中所有类都耦合的上帝对象。
与其他模式的关系
- 外观为现有对象定义了一个新接口, 适配器则会试图运用已 有的接口。 适配器通常只封装一个对象, 外观通常会作用于 整个对象子系统上。
- 当只需对客户端代码隐藏子系统创建对象的方式时, 你可以 使用抽象工厂来代替外观。
- 享元展示了如何生成大量的小型对象, 外观则展示了如何用 一个对象来代表整个子系统。
- 外观和中介者的职责类似: 它们都尝试在大量紧密耦合的类 中组织起合作。
外观为子系统中的所有对象定义了一个简单接口,但是它 不提供任何新功能。 子系统本身不会意识到外观的存在。 子系统中的对象可以直接进行交流。
中介者将系统中组件的沟通行为中心化。各组件只知道中 介者对象, 无法直接相互交流。 - 外观类通常可以转换为单例类, 因为在大部分情况下一个外 观对象就足够了。
- 外观与代理的相似之处在于它们都缓存了一个复杂实体并自 行对其进行初始化。 代理与其服务对象遵循同一接口, 使得 自己和服务对象可以互换, 在这一点上它与外观不同。
享元
结构
- 享元模式只是一种优化。 在应用该模式之前, 你要确定程序 中存在与大量类似对象同时占用内存相关的内存消耗问题, 并且确保该问题无法使用其他更好的方式来解决。
- 享元(Flyweight)类包含原始对象中部分能在多个对象中共 享的状态。 同一享元对象可在许多不同情景中使用。 享元中 存储的状态被称为“内在状态”。 传递给享元方法的状态被 称为“外在状态”。
- 情景(Context)类包含原始对象中各不相同的外在状态。情 景与享元对象组合在一起就能表示原始对象的全部状态。
- 通常情况下, 原始对象的行为会保留在享元类中。 因此调用 享元方法必须提供部分外在状态作为参数。 但你也可将行为 移动到情景类中, 然后将连入的享元作为单纯的数据对象。
- 客户端(Client)负责计算或存储享元的外在状态。在客户 端看来, 享元是一种可在运行时进行配置的模板对象, 具体 的配置方式为向其方法中传入一些情景数据参数。
- 享元工厂(Flyweight Factory)会对已有享元的缓存池进行 管理。 有了工厂后, 客户端就无需直接创建享元, 它们只需 调用工厂并向其传递目标享元的一些内在状态即可。 工厂会 根据参数在之前已创建的享元中进行查找, 如果找到满足条 件的享元就将其返回; 如果没有找到就根据参数新建享元。
适用场景
- 仅在程序必须支持大量对象且没有足够的内存容量时使用享 元模式。
应用该模式所获的收益大小取决于使用它的方式和情景。 它在下列情况中最有效:
程序需要生成数量巨大的相似对象
这将耗尽目标设备的所有内存
对象中包含可抽取且能在多个对象间共享的重复状态。
实现方式
- 将需要改写为享元的类成员变量拆分为两个部分:
内在状态:包含不变的、可在许多对象中重复使用的数据 的成员变量。
外在状态:包含每个对象各自不同的情景数据的成员变量 - 保留类中表示内在状态的成员变量, 并将其属性设置为不可 修改。这些变量仅可在构造函数中获得初始数值。
- 找到所有使用外在状态成员变量的方法,为在方法中所用的每个成员变量新建一个参数,并使用该参数代替成员变量。
- 你可以有选择地创建工厂类来管理享元缓存池, 它负责在新建享元时检查已有的享元。 如果选择使用工厂,客户端就只能通过工厂来请求享元,它们需要将享元的内在状态作为参数传递给工厂。
- 客户端必须存储和计算外在状态(情景)的数值,因为只有 这样才能调用享元对象的方法。 为了使用方便, 外在状态和 引用享元的成员变量可以移动到单独的情景类中。
优缺点
优点:
- 如果程序中有很多相似对象, 那么你将可以节省大量内存。
缺点: - 你可能需要牺牲执行速度来换取内存,因为他人每次调用享元方法时都需要重新计算部分情景数据。
- 代码会变得更加复杂。 团队中的新成员总是会问:“为什么要像这样拆分一个实体的状态?”。
与其他模式关系
- 你可以使用享元实现组合树的共享叶节点以节省内存。
- 享元展示了如何生成大量的小型对象, 外观则展示了如何用 一个对象来代表整个子系统。
- 如果你能将对象的所有共享状态简化为一个享元对象, 那么享元就和单例类似了。 但这两个模式有两个根本性的不同。
只会有一个单例实体,但是享元类可以有多个实体,各实 体的内在状态也可以不同。
单例对象可以是可变的。享元对象是不可变的。
代理
结构
- 服务接口(Service Interface)声明了服务接口。代理必须遵 循该接口才能伪装成服务对象。
- 服务(Service)类提供了一些实用的业务逻辑。
- 代理(Proxy)类包含一个指向服务对象的引用成员变量。代 理完成其任务(例如延迟初始化、 记录日志、 访问控制和缓 存等) 后会将请求传递给服务对象。 通常情况下, 代理会对 其服务对象的整个生命周期进行管理。
- 客户端(Client) 能通过同一接口与服务或代理进行交互, 所以你可在一切需要服务对象的代码中使用代理。
适用场景
-
延迟初始化(虚拟代理)。如果你有一个偶尔使用的重量级服 务对象, 一直保持该对象运行会消耗系统资源时, 可使用代理模式。
你无需在程序启动时就创建该对象, 可将对象的初始化延迟到真正有需要的时候。 -
访问控制(保护代理)。如果你只希望特定客户端使用服务对象,这里的对象可以是操作系统中非常重要的部分, 而客户端则是各种已启动的程序(包括恶意程序),此时可使用代理 模式。
代理可仅在客户端凭据满足要求时将请求传递给服务对象。 -
本地执行远程服务(远程代理)。适用于服务对象位于远程服务器上的情形。
在这种情形中, 代理通过网络传递客户端请求, 负责处理所有与网络相关的复杂细节。 -
记录日志请求(日志记录代理)。适用于当你需要保存对于服 务对象的请求历史记录时。 代理可以在向服务传递请求前进 行记录。
缓存请求结果(缓存代理)。 适用于需要缓存客户请求结果 并对缓存生命周期进行管理时, 特别是当返回结果的体积非常大时。
代理可对重复请求所需的相同结果进行缓存, 还可使用请求参数作为索引缓存的键值。 -
智能引用。可在没有客户端使用某个重量级对象时立即销毁 该对象。
代理会将所有获取了指向服务对象或其结果的客户端记录在 案。 代理会时不时地遍历各个客户端, 检查它们是否仍在运 行。 如果相应的客户端列表为空, 代理就会销毁该服务对象, 释放底层系统资源。
代理还可以记录客户端是否修改了服务对象。 其他客户端还 可以复用未修改的对象。
实现方法
- 如果没有现成的服务接口, 你就需要创建一个接口来实现代 理和服务对象的可交换性。 从服务类中抽取接口并非总是可 行的, 因为你需要对服务的所有客户端进行修改, 让它们使 用接口。 备选计划是将代理作为服务类的子类, 这样代理就能继承服务的所有接口了。
- 创建代理类, 其中必须包含一个存储指向服务的引用的成员 变量。 通常情况下, 代理负责创建服务并对其整个生命周期 进行管理。 在一些特殊情况下, 客户端会通过构造函数将服 务传递给代理。
- 根据需求实现代理方法。 在大部分情况下, 代理在完成一些任务后应将工作委派给服务对象。
- 可以考虑新建一个构建方法来判断客户端可获取的是代理还 是实际服务。 你可以在代理类中创建一个简单的静态方法, 也可以创建一个完整的工厂方法。
- 可以考虑为服务对象实现延迟初始化。
优缺点
优点:
- 你可以在客户端毫无察觉的情况下控制服务对象。
- 如果客户端对服务对象的生命周期没有特殊要求, 你可以对生命周期进行管理。
- 即使服务对象还未准备好或不存在, 代理也可以正常工作。
- 开闭原则。 你可以在不对服务或客户端做出修改的情况下创 建新代理。
缺点: - 代码可能会变得复杂, 因为需要新建许多类。
- 服务响应可能会延迟。
与其他模式的关系
- 适配器能为被封装对象提供不同的接口, 代理能为对象提供相同的接口, 装饰则能为对象提供加强的接口。
- 外观与代理的相似之处在于它们都缓存了一个复杂实体并自 行对其进行初始化。 代理与其服务对象遵循同一接口, 使得 自己和服务对象可以互换, 在这一点上它与外观不同。
- 装饰和代理有着相似的结构, 但是其意图却非常不同。 这两 个模式的构建都基于组合原则, 也就是说一个对象应该将部 分工作委派给另一个对象。 两者之间的不同之处在于代理通 常自行管理其服务对象的生命周期, 而装饰的生成则总是由 客户端进行控制。
行为模式
责任链
结构
- 处理者(Handler)声明了所有具体处理者的通用接口。该接 口通常仅包含单个方法用于请求处理, 但有时其还会包含一 个设置链上下个处理者的方法。
- 基础处理者(Base Handler)是一个可选的类,你可以将所 有处理者共用的样本代码放置在其中。
通常情况下, 该类中定义了一个保存对于下个处理者引用的 成员变量。 客户端可通过将处理者传递给上个处理者的构造 函数或设定方法来创建链。 该类还可以实现默认的处理行为: 确定下个处理者存在后再将请求传递给它。 - 具体处理者(Concrete Handlers)包含处理请求的实际代码。 每个处理者接收到请求后, 都必须决定是否进行处理, 以及 是否沿着链传递请求。
处理者通常是独立且不可变的, 需要通过构造函数一次性地 获得所有必要地数据。 - 客户端(Client)可根据程序逻辑一次性或者动态地生成链。 值得注意的是, 请求可发送给链上的任意一个处理者, 而非 必须是第一个处理者。
适用场景
- 当程序需要使用不同方式处理不同种类请求,而且请求类型和顺序预先未知时,可以使用责任链模式。
该模式能将多个处理者连接成一条链。 接收到请求后, 它会 “询问” 每个处理者是否能够对其进行处理。 这样所有处理者都有机会来处理请求。 - 当必须按顺序执行多个处理者时,可以使用该模式。
无论你以何种顺序将处理者连接成一条链, 所有请求都会严格按照顺序通过链上的处理者。 - 如果所需处理者及其顺序必须在运行时进行改变,可以使用 责任链模式。
如果在处理者类中有对引用成员变量的设定方法, 你将能动态地插入和移除处理者, 或者改变其顺序。
实现方式
- 声明处理者接口并描述请求处理方法的签名。
确定客户端如何将请求数据传递给方法。 最灵活的方式是将 请求转换为对象, 然后将其以参数的形式传递给处理函数。 - 为了在具体处理者中消除重复的样本代码, 你可以根据处理 者接口创建抽象处理者基类。
该类需要有一个成员变量来存储指向链上下个处理者的引用。 你可以将其设置为不可变类。 但如果你打算在运行时对链进 行改变, 则需要定义一个设定方法来修改引用成员变量的值。为了使用方便, 你还可以实现处理方法的默认行为。 如果还 有剩余对象, 该方法会将请求传递给下个对象。 具体处理者 还能够通过调用父对象的方法来使用这一行为。 - 依次创建具体处理者子类并实现其处理方法。 每个处理者在 接收到请求后都必须做出两个决定:
是否自行处理这个请求。
是否将该请求沿着链进行传递。 - 客户端可以自行组装链, 或者从其他对象处获得预先组装好 的链。 在后一种情况下, 你必须实现工厂类以根据配置或环 境设置来创建链。
- 客户端可以触发链中的任意处理者, 而不仅仅是第一个。 请求将通过链进行传递, 直至某个处理者拒绝继续传递, 或者请求到达链尾。
- 由于链的动态性, 客户端需要准备好处理以下情况:
链中可能只有单个链接。
部分请求可能无法到达链尾。
其他请求可能直到链尾都未被处理。
优缺点
优点:
- 你可以控制请求处理的顺序
- 单一职责原则。 你可对发起操作和执行操作的类进行解耦。
- 开闭原则。 你可以在不更改现有代码的情况下在程序中新增处理者。
缺点: - 部分请求可能未被处理。
与其他模式的关系
- 责任链、命令、中介者和观察者用于处理请求发送者和接收 者之间的不同连接方式:
责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
命令在发送者和请求者之间建立单向连接。
中介者清除了发送者和请求者之间的直接连接,强制它们
通过一个中介对象进行间接沟通。
观察者允许接收者动态地订阅或取消接收请求。 - 责任链通常和组合模式结合使用。 在这种情况下, 叶组件接 收到请求后, 可以将请求沿包含全体父组件的链一直传递至 对象树的底部。
- 责任链的管理者可使用命令模式实现。 在这种情况下, 你可 以对由请求代表的同一个上下文对象执行许多不同的操作。
还有另外一种实现方式, 那就是请求自身就是一个命令对象。 在这种情况下, 你可以对由一系列不同上下文连接而成的链 执行相同的操作。 - 责任链和装饰模式的类结构非常相似。 两者都依赖递归组合 将需要执行的操作传递给一系列对象。 但是, 两者有几点重 要的不同之处。
责任链的管理者可以相互独立地执行一切操作, 还可以随时 停止传递请求。 另一方面, 各种装饰可以在遵循基本接口的 情况下扩展对象的行为。 此外, 装饰无法中断请求的传递。
命令
结构
- 发 送 者 (Sender)—— 亦 称 “触 发 者 (Invoker)”—— 类 负 责 对 请求进行初始化, 其中必须包含一个成员变量来存储对于命 令对象的引用。 发送者触发命令, 而不向接收者直接发送请 求。 注意, 发送者并不负责创建命令对象: 它通常会通过构 造函数从客户端处获得预先生成的命令。
- 命令(Command)接口通常仅声明一个执行命令的方法。
- 具体命令(Concrete Commands) 会实现各种类型的请求。 具体命令自身并不完成工作, 而是会将调用委派给一个业务 逻辑对象。 但为了简化代码, 这些类可以进行合并。
接收对象执行方法所需的参数可以声明为具体命令的成员变 量。 你可以将命令对象设为不可变, 仅允许通过构造函数对 这些成员变量进行初始化。 - 接收者(Receiver)类包含部分业务逻辑。几乎任何对象都 可以作为接收者。 绝大部分命令只处理如何将请求传递到接 收者的细节, 接收者自己会完成实际的工作。
- 客户端(Client)会创建并配置具体命令对象。客户端必须 将包括接收者实体在内的所有请求参数传递给命令的构造函 数。 此后, 生成的命令就可以与一个或多个发送者相关联了。
适用场景
- 如果你需要通过操作来参数化对象,可使用命令模式。
命令模式可将特定的方法调用转化为独立对象。 这一改变也 带来了许多有趣的应用: 你可以将命令作为方法的参数进行 传递、 将命令保存在其他对象中, 或者在运行时切换已连接 的命令等。
举个例子: 你正在开发一个 GUI 组件(例如上下文菜单), 你希望用户能够配置菜单项, 并在点击菜单项时触发操作。 - 如果你想要将操作放入队列中、操作的执行或者远程执行操 作,可使用命令模式。
同其他对象一样, 命令也可以实现序列化(序列化的意思是 转化为字符串), 从而能方便地写入文件或数据库中。 一段 时间后, 该字符串可被恢复成为最初的命令对象。 因此, 你 可以延迟或计划命令的执行。 但其功能远不止如此! 使用同 样的方式, 你还可以将命令放入队列、 记录命令或者通过网 络发送命令。 - 如果你想要实现操作回滚功能,可使用命令模式。
尽管有很多方法可以实现撤销和恢复功能, 但命令模式可能 是其中最常用的一种。
为了能够回滚操作, 你需要实现已执行操作的历史记录功能。 命令历史记录是一种包含所有已执行命令对象及其相关程序 状态备份的栈结构。
这种方法有两个缺点。 首先, 程序状态的保存功能并不容易 实现, 因为部分状态可能是私有的。 你可以使用备忘录模式 来在一定程度上解决这个问题。
其次, 备份状态可能会占用大量内存。 因此, 有时你需要借 助另一种实现方式: 命令无需恢复原始状态, 而是执行反向 操作。 反向操作也有代价: 它可能会很难甚至是无法实现。
实现方法
- 声明仅有一个执行方法的命令接口。
- 抽取请求并使之成为实现命令接口的具体命令类。 每个类都 必须有一组成员变量来保存请求参数和对于实际接收者对象 的引用。 所有这些变量的数值都必须通过命令构造函数进行 初始化。
- 找到担任发送者职责的类。 在这些类中添加保存命令的成员 变量。 发送者只能通过命令接口与其命令进行交互。 发送者 自身通常并不创建命令对象, 而是通过客户端代码获取。
- 修改发送者使其执行命令, 而非直接将请求发送给接收者。
- 客户端必须按照以下顺序来初始化对象:
创建接收者。
创建命令,如有需要可将其关联至接收者。
创建发送者并将其与特定命令关联。
优缺点
优点:
- 单一职责原则。 你可以解耦触发和执行操作的类。
- 开闭原则。 你可以在不修改已有客户端代码的情况下在程序 中创建新的命令。
- 你可以实现撤销和恢复功能。
- 你可以实现操作的延迟执行。
- 你可以将一组简单命令组合成一个复杂命令。
缺点: - 代码可能会变得更加复杂, 因为你在发送者和接收者之间增 加了一个全新的层次。
与其他模式的关系
- 责任链、 命令、 中介者和观察者用于处理请求发送者和接收
者之间的不同连接方式:
责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
命令在发送者和请求者之间建立单向连接。
中介者清除了发送者和请求者之间的直接连接,强制它们 通过一个中介对象进行间接沟通。
观察者允许接收者动态地订阅或取消接收请求。 - 责任链的管理者可使用命令模式实现。 在这种情况下, 你可以对由请求代表的同一个上下文对象执行许多不同的操作。
还有另外一种实现方式, 那就是请求自身就是一个命令对象。 在这种情况下, 你可以对由一系列不同上下文连接而成的链 执行相同的操作。 - 你可以同时使用命令和备忘录来实现“撤销”。 在这种情况 下, 命令用于对目标对象执行各种不同的操作, 备忘录用来 保存一条命令执行前该对象的状态。
- 命令和策略看上去很像, 因为两者都能通过某些行为来参数 化对象。 但是, 它们的意图有非常大的不同。
你可以使用命令来将任何操作转换为对象。操作的参数将 成为对象的成员变量。 你可以通过转换来延迟操作的执 行、 将操作放入队列、 保存历史命令或者向远程服务发送 命令等。
另一方面,策略通常可用于描述完成某件事的不同方式,让你能够在同一个上下文类中切换算法。 - 原型可用于保存命令的历史记录。
- 你可以将访问者视为命令模式的加强版本, 其对象可对不同类的多种对象执行操作。
迭代器
结构
- 迭代器(Iterator)接口声明了遍历集合所需的操作:获取下一个元素、 获取当前位置和重新开始迭代等。
- 具体迭代器(Concrete Iterators)实现遍历集合的一种特定 算法。 迭代器对象必须跟踪自身遍历的进度。 这使得多个迭 代器可以相互独立地遍历同一集合。
- 集合(Collection)接口声明一个或多个方法来获取与集合兼 容的迭代器。 请注意, 返回方法的类型必须被声明为迭代器 接口, 因此具体集合可以返回各种不同种类的迭代器。
- 具体集合(Concrete Collections)会在客户端请求迭代器时 返回一个特定的具体迭代器类实体。 你可能会琢磨, 剩下的 集合代码在什么地方呢? 不用担心, 它也会在同一个类中。 只是这些细节对于实际模式来说并不重要, 所以我们将其省 略了而已。
- 客户端(Client)通过集合和迭代器的接口与两者进行交互。 这样一来客户端无需与具体类进行耦合, 允许同一客户端代 码使用各种不同的集合和迭代器。客户端通常不会自行创建迭代器, 而是会从集合中获取。 但 在特定情况下, 客户端可以直接创建一个迭代器(例如当客 户端需要自定义特殊迭代器时)。
适应应用场景
-
当集合背后为复杂的数据结构,且你希望对客户端隐藏其复 杂性时(出于使用便利性或安全性的考虑),可以使用迭代器 模式。
迭代器封装了与复杂数据结构进行交互的细节, 为客户端提 供多个访问集合元素的简单方法。 这种方式不仅对客户端来 说非常方便, 而且能避免客户端在直接与集合交互时执行错 误或有害的操作, 从而起到保护集合的作用。 -
使用该模式可以减少程序中重复的遍历代码。
重要迭代算法的代码往往体积非常庞大。 当这些代码被放置 在程序业务逻辑中时, 它会让原始代码的职责模糊不清, 降 低其可维护性。 因此, 将遍历代码移到特定的迭代器中可使 程序代码更加精炼和简洁。 -
如果你希望代码能够遍历不同的甚至是无法预知的数据结构,可以使用迭代器模式。
该模式为集合和迭代器提供了一些通用接口。 如果你在代码 中使用了这些接口, 那么将其他实现了这些接口的集合和迭 代器传递给它时, 它仍将可以正常运行。
实现方式
- 声明迭代器接口。 该接口必须提供至少一个方法来获取集合 中的下个元素。 但为了使用方便, 你还可以添加一些其他方 法, 例如获取前一个元素、 记录当前位置和判断迭代是否已 结束。
- 声明集合接口并描述一个获取迭代器的方法。 其返回值必须 是迭代器接口。 如果你计划拥有多组不同的迭代器, 则可以 声明多个类似的方法。
- 为希望使用迭代器进行遍历的集合实现具体迭代器类。 迭代 器对象必须与单个集合实体链接。 链接关系通常通过迭代器 的构造函数建立。
- 在你的集合类中实现集合接口。 其主要思想是针对特定集合 为客户端代码提供创建迭代器的快捷方式。 集合对象必须将 自身传递给迭代器的构造函数来创建两者之间的链接。
- 检查客户端代码, 使用迭代器替代所有集合遍历代码。 每当客户端需要遍历集合元素时都会获取一个新的迭代器。
优缺点
优点:
- 单一职责原则。 通过将体积庞大的遍历算法代码抽取为独立的类, 你可对客户端代码和集合进行整理。
- 开闭原则。 你可实现新型的集合和迭代器并将其传递给现有 代码, 无需修改现有代码。
- 你可以并行遍历同一集合, 因为每个迭代器对象都包含其自 身的遍历状态。
- 相似的, 你可以暂停遍历并在需要时继续。
缺点: - 如果你的程序只与简单的集合进行交互, 应用该模式可能会 矫枉过正。
- 对于某些特殊集合, 使用迭代器可能比直接遍历的效率低。
与其他模式的关系
- 你可以使用迭代器来遍历组合树。
- 你可以同时使用工厂方法和迭代器来让子类集合返回不同类 型的迭代器, 并使得迭代器与集合相匹配。
- 你可以同时使用备忘录和迭代器来获取当前迭代器的状态,并且在需要的时候进行回滚。
- 可以同时使用访问者和迭代器来遍历复杂数据结构, 并对其 中的元素执行所需操作, 即使这些元素所属的类完全不同。
中介者
结构
- 组件(Component)是各种包含业务逻辑的类。每个组件都 有一个指向中介者的引用, 该引用被声明为中介者接口类型。 组件不知道中介者实际所属的类, 因此你可通过将其连接到 不同的中介者以使其能在其他程序中复用。
- 中介者(Mediator)接口声明了与组件交流的方法,但通常 仅包括一个通知方法。 组件可将任意上下文(包括自己的对 象) 作为该方法的参数, 只有这样接收组件和发送者类之间 才不会耦合。
- 具体中介者(Concrete Mediator)封装了多种组件间的关系。 具体中介者通常会保存所有组件的引用并对其进行管理, 甚 至有时会对其生命周期进行管理。
- 组件并不知道其他组件的情况。 如果组件内发生了重要事件, 它只能通知中介者。 中介者收到通知后能轻易地确定发送者, 这或许已足以判断接下来需要触发的组件了。
对于组件来说, 中介者看上去完全就是一个黑箱。 发送者不 知道最终会由谁来处理自己的请求, 接收者也不知道最初是 谁发出了请求。
适用场景
- 当一些对象和其他对象紧密耦合以致难以对其进行修改时, 可使用中介者模式。
该模式让你将对象间的所有关系抽取成为一个单独的类, 以 使对于特定组件的修改工作独立于其他组件。 - 当组件因过于依赖其他组件而无法在不同应用中复用时,可使用中介者模式。
应用中介者模式后, 每个组件不再知晓其他组件的情况。 尽 管这些组件无法直接交流, 但它们仍可通过中介者对象进行 间接交流。 如果你希望在不同应用中复用一个组件, 则需要 为其提供一个新的中介者类。 - 如果为了能在不同情景下复用一些基本行为,导致你需要被 迫创建大量组件子类时,可使用中介者模式。
由于所有组件间关系都被包含在中介者中, 因此你无需修改 组件就能方便地新建中介者类以定义新的组件合作方式。
实现方法
- 找到一组当前紧密耦合, 且提供其独立性能带来更大好处的类(例如更易于维护或更方便复用)。
- 声明中介者接口并描述中介者和各种组件之间所需的交流接 口。 在绝大多数情况下, 一个接收组件通知的方法就足够了。 如果你希望在不同情景下复用组件类, 那么该接口将非常重 要。 只要组件使用通用接口与其中介者合作, 你就能将该组 件与不同实现中的中介者进行连接。
- 实现具体中介者类。 该类可从自行保存其下所有组件的引用 中受益。
- 你可以更进一步, 让中介者负责组件对象的创建和销毁。 此 后, 中介者可能会与工厂或外观类似。
- 组件必须保存对于中介者对象的引用。 该连接通常在组件的 构造函数中建立, 该函数会将中介者对象作为参数传递。
- 修改组件代码, 使其可调用中介者的通知方法, 而非其他组 件的方法。 然后将调用其他组件的代码抽取到中介者类中, 并在中介者接收到该组件通知时执行这些代码。
优缺点
优点:
- 单一职责原则。 你可以将多个组件间的交流抽取到同一位置,使其更易于理解和维护。
- 开闭原则。 你无需修改实际组件就能增加新的中介者。
- 你可以减轻应用中多个组件间的耦合情况。
- 你可以更方便地复用各个组件。
缺点: - 一段时间后, 中介者可能会演化成为上帝对象。
与其他模式的关系
- 责任链、 命令、 中介者和观察者用于处理请求发送者和接收 者之间的不同连接方式:
责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
命令在发送者和请求者之间建立单向连接。
中介者清除了发送者和请求者之间的直接连接,强制它们通过一个中介对象进行间接沟通。
观察者允许接收者动态地订阅或取消接收请求。 - 外观和中介者的职责类似: 它们都尝试在大量紧密耦合的类 中组织起合作。
外观为子系统中的所有对象定义了一个简单接口,但是它 不提供任何新功能。 子系统本身不会意识到外观的存在。 子系统中的对象可以直接进行交流。
中介者将系统中组件的沟通行为中心化。各组件只知道中 介者对象, 无法直接相互交流。 - 中介者和观察者之间的区别往往很难记住。 在大部分情况下, 你可以使用其中一种模式, 而有时可以同时使用。 让我们来 看看如何做到这一点。
中介者的主要目标是消除一系列系统组件之间的相互依赖。 这些组件将依赖于同一个中介者对象。 观察者的目标是在对 象之间建立动态的单向连接, 使得部分对象可作为其他对象 的附属发挥作用。
有一种流行的中介者模式实现方式依赖于观察者。 中介者对 象担当发布者的角色, 其他组件则作为订阅者, 可以订阅中 介者的事件或取消订阅。 当中介者以这种方式实现时, 它可 能看上去与观察者非常相似。
当你感到疑惑时, 记住可以采用其他方式来实现中介者。 例 如, 你可永久性地将所有组件链接到同一个中介者对象。 这 种实现方式和观察者并不相同, 但这仍是一种中介者模式。
假设有一个程序, 其所有的组件都变成了发布者, 它们之间 可以相互建立动态连接。 这样程序中就没有中心化的中介者 对象, 而只有一些分布式的观察者。
备忘录
结构
基于嵌套的实现
该模式的经典实现方式依赖于许多流行编程语言(例如 C++、 C# 和 Java)所支持的嵌套类。
- 原发器(Originator)类可以生成自身状态的快照,也可以在需要时通过快照恢复自身状态。
- 备忘录(Memento) 是原发器状态快照的值对象(value object)。 通常做法是将备忘录设为不可变的, 并通过构造函 数一次性传递数据。
- 负责人(Caretaker)仅知道“何时”和“为何”捕捉原发器 的状态, 以及何时恢复状态。
负责人通过保存备忘录栈来记录原发器的历史状态。 当原发 器需要回溯历史状态时, 负责人将从栈中获取最顶部的备忘 录,并将其传递给原发器的恢复(restoration)方法。 - 在该实现方法中, 备忘录类将被嵌套在原发器中。 这样原发 器就可访问备忘录的成员变量和方法, 即使这些方法被声明 为私有。 另一方面, 负责人对于备忘录的成员变量和方法的 访问权限非常有限: 它们只能在栈中保存备忘录, 而不能修 改其状态。
基于中间接口的实现
另外一种实现方法适用于不支持嵌套类的编程语言(没错, 我说的就是 PHP)。
- 在没有嵌套类的情况下, 你可以规定负责人仅可通过明确声 明的中间接口与备忘录互动, 该接口仅声明与备忘录元数据 相关的方法, 限制其对备忘录成员变量的直接访问权限。
- 另一方面, 原发器可以直接与备忘录对象进行交互, 访问备 忘录类中声明的成员变量和方法。 这种方式的缺点在于你需 要将备忘录的所有成员变量声明为公有。
封装更加严格的实现
如果你不想让其他类有任何机会通过备忘录来访问原发器的 状态, 那么还有另一种可用的实现方式。
- 这种实现方式允许存在多种不同类型的原发器和备忘录。 每 种原发器都和其相应的备忘录类进行交互。 原发器和备忘录 都不会将其状态暴露给其他类。
- 负责人此时被明确禁止修改存储在备忘录中的状态。 但负责 人类将独立于原发器, 因为此时恢复方法被定义在了备忘录 类中。
- 每个备忘录将与创建了自身的原发器连接。 原发器会将自己 及状态传递给备忘录的构造函数。 由于这些类之间的紧密联 系, 只要原发器定义了合适的设置器(setter), 备忘录就能 恢复其状态。
适用场景
- 当你需要创建对象状态快照来恢复其之前的状态时,可以使用备忘录模式。
备忘录模式允许你复制对象中的全部状态(包括私有成员变 量), 并将其独立于对象进行保存。 尽管大部分人因为“撤 销”这个用例才记得该模式,但其实它在处理事务(比如需 要在出现错误时回滚一个操作) 的过程中也必不可少。 - 当直接访问对象的成员变量、获取器或设置器将导致封装被突破时,可以使用该模式。
备忘录让对象自行负责创建其状态的快照。 任何其他对象都 不能读取快照, 这有效地保障了数据的安全性。
实现方式
- 确定担任原发器角色的类。 重要的是明确程序使用的一个原发器中心对象, 还是多个较小的对象。
- 创建备忘录类。 逐一声明对应每个原发器成员变量的备忘录 成员变量。
- 将备忘录类设为不可变。 备忘录只能通过构造函数一次性接 收数据。 该类中不能包含设置器。
- 如果你所使用的编程语言支持嵌套类, 则可将备忘录嵌套在 原发器中; 如果不支持, 那么你可从备忘录类中抽取一个空 接口, 然后让其他所有对象通过接口来引用备忘录。 你可在 该接口中添加一些元数据操作, 但不能暴露原发器的状态。
- 在原发器中添加一个创建备忘录的方法。 原发器必须通过备 忘录构造函数的一个或多个实际参数来将自身状态传递给备 忘录。
该方法返回结果的类型必须是你在上一步中抽取的接口(如 果你已经抽取了)。 实际上, 创建备忘录的方法必须直接与 备忘录类进行交互。 - 在原发器类中添加一个用于恢复自身状态的方法。 该方法接 受备忘录对象作为参数。 如果你在之前的步骤中抽取了接口, 那么可将接口作为参数的类型。 在这种情况下, 你需要将输 入对象强制转换为备忘录, 因为原发器需要拥有对该对象的 完全访问权限。
- 无论负责人是命令对象、历史记录或其他完全不同的东西, 它都必须要知道何时向原发器请求新的备忘录、 如何存储备 忘录以及何时使用特定备忘录来对原发器进行恢复。
- 负责人与原发器之间的连接可以移动到备忘录类中。 在本例 中, 每个备忘录都必须与创建自己的原发器相连接。 恢复方 法也可以移动到备忘录类中, 但只有当备忘录类嵌套在原发 器中, 或者原发器类提供了足够多的设置器并可对其状态进 行重写时, 这种方式才能实现。
优缺点
优点:
- 你可以在不破坏对象封装情况的前提下创建对象状态快照。
- 你可以通过让负责人维护原发器状态历史记录来简化原发器 代码。
缺点: - 如果客户端过于频繁地创建备忘录, 程序将消耗大量内存。
- 负责人必须完整跟踪原发器的生命周期, 这样才能销毁弃用 的备忘录。
- 绝大部分动态编程语言(例如 PHP、 Python 和 JavaScript) 不能确保备忘录中的状态不被修改。
与其他模式的关系
- 你可以同时使用命令和备忘录来实现“撤销”。 在这种情况 下, 命令用于对目标对象执行各种不同的操作, 备忘录用来 保存一条命令执行前该对象的状态。
- 你可以同时使用备忘录和迭代器来获取当前迭代器的状态, 并且在需要的时候进行回滚。
- 有时候原型可以作为备忘录的一个简化版本, 其条件是你需 要在历史记录中存储的对象的状态比较简单, 不需要链接其 他外部资源, 或者链接可以方便地重建。
观察者
结构
- 发布者(Publisher)会向其他对象发送值得关注的事件。事 件会在发布者自身状态改变或执行特定行为后发生。 发布者 中包含一个允许新订阅者加入和当前订阅者离开列表的订阅 构架。
- 当新事件发生时, 发送者会遍历订阅列表并调用每个订阅者 对象的通知方法。 该方法是在订阅者接口中声明的。
- 订阅者(Subscriber)接口声明了通知接口。在绝大多数情 况下, 该接口仅包含一个 update 更新 方法。 该方法可以 拥有多个参数, 使发布者能在更新时传递事件的详细信息。
- 具体订阅者(Concrete Subscribers)可以执行一些操作来回 应发布者的通知。 所有具体订阅者类都实现了同样的接口, 因此发布者不需要与具体类相耦合。
- 订阅者通常需要一些上下文信息来正确地处理更新。 因此, 发布者通常会将一些上下文数据作为通知方法的参数进行传 递。 发布者也可将自身作为参数进行传递, 使订阅者直接获 取所需的数据。
- 客户端(Client)会分别创建发布者和订阅者对象,然后为 订阅者注册发布者更新。
适用场景
-
当一个对象状态的改变需要改变其他对象,或实际对象是事 先未知的或动态变化的时,可使用观察者模式。
当你使用图形用户界面类时通常会遇到一个问题。 比如, 你 创建了自定义按钮类并允许客户端在按钮中注入自定义代码, 这样当用户按下按钮时就会触发这些代码。
观察者模式允许任何实现了订阅者接口的对象订阅发布者对 象的事件通知。 你可在按钮中添加订阅机制, 允许客户端通 过自定义订阅类注入自定义代码。 -
当应用中的一些对象必须观察其他对象时,可使用该模式。 但仅能在有限时间内或特定情况下使用。
订阅列表是动态的, 因此订阅者可随时加入或离开该列表。
实现方式
- 仔细检查你的业务逻辑, 试着将其拆分为两个部分: 独立于 其他代码的核心功能将作为发布者; 其他代码则将转化为一 组订阅类。
- 声明订阅者接口。 该接口至少应声明一个 update 方法。
- 声明发布者接口并定义一些接口来在列表中添加和删除订阅
对象。 记住发布者必须仅通过订阅者接口与它们进行交互。 - 确定存放实际订阅列表的位置并实现订阅方法。 通常所有类 型的发布者代码看上去都一样, 因此将列表放置在直接扩展 自发布者接口的抽象类中是显而易见的。 具体发布者会扩展 该类从而继承所有的订阅行为。
但是, 如果你需要在现有的类层次结构中应用该模式, 则可 以考虑使用组合的方式: 将订阅逻辑放入一个独立的对象, 然后让所有实际订阅者使用该对象。 - 创建具体发布者类。 每次发布者发生了重要事件时都必须通 知所有的订阅者。
- 在具体订阅者类中实现通知更新的方法。 绝大部分订阅者需 要一些与事件相关的上下文数据。 这些数据可作为通知方法 的参数来传递。
但还有另一种选择。 订阅者接收到通知后直接从通知中获取 所有数据。 在这种情况下, 发布者必须通过更新方法将自身 传递出去。 另一种不太灵活的方式是通过构造函数将发布者 与订阅者永久性地连接起来。 - 客户端必须生成所需的全部订阅者, 并在相应的发布者处完 成注册工作。
优缺点
优点:
- 开闭原则。 你无需修改发布者代码就能引入新的订阅者类 (如果是发布者接口则可轻松引入发布者类)。
- 你可以在运行时建立对象之间的联系。
缺点: - 订阅者的通知顺序是随机的。
与其他模式的关系
- 责任链、 命令、 中介者和观察者用于处理请求发送者和接收者之间的不同连接方式:
责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
命令在发送者和请求者之间建立单向连接。
中介者清除了发送者和请求者之间的直接连接,强制它们通过一个中介对象进行间接沟通。
观察者允许接收者动态地订阅或取消接收请求。 - 中介者和观察者之间的区别往往很难记住。 在大部分情况下, 你可以使用其中一种模式, 而有时可以同时使用。 让我们来 看看如何做到这一点。
中介者的主要目标是消除一系列系统组件之间的相互依赖。 这些组件将依赖于同一个中介者对象。 观察者的目标是在对 象之间建立动态的单向连接, 使得部分对象可作为其他对象 的附属发挥作用。
有一种流行的中介者模式实现方式依赖于观察者。 中介者对 象担当发布者的角色, 其他组件则作为订阅者, 可以订阅中 介者的事件或取消订阅。 当中介者以这种方式实现时, 它可 能看上去与观察者非常相似。
当你感到疑惑时, 记住可以采用其他方式来实现中介者。 例 如, 你可永久性地将所有组件链接到同一个中介者对象。 这 种实现方式和观察者并不相同, 但这仍是一种中介者模式。
假设有一个程序, 其所有的组件都变成了发布者, 它们之间 可以相互建立动态连接。 这样程序中就没有中心化的中介者 对象, 而只有一些分布式的观察者。
状态
结构
- 上下文(Context)保存了对于一个具体状态对象的引用,并 会将所有与该状态相关的工作委派给它。 上下文通过状态接 口与状态对象交互, 且会提供一个设置器用于传递新的状态 对象。
- 状态(State)接口会声明特定于状态的方法。这些方法应能 被其他所有具体状态所理解, 因为你不希望某些状态所拥有 的方法永远不会被调用。
- 具体状态(Concrete States)会自行实现特定于状态的方法。 为了避免多个状态中包含相似代码, 你可以提供一个封装有 部分通用行为的中间抽象类。
状态对象可存储对于上下文对象的反向引用。 状态可以通过 该引用从上下文处获取所需信息, 并且能触发状态转移。 - 上下文和具体状态都可以设置上下文的下个状态, 并可通过 替换连接到上下文的状态对象来完成实际的状态转换。
适用场景
-
如果对象需要根据自身当前状态进行不同行为,同时状态的 数量非常多且与状态相关的代码会频繁变更的话, 可使用状 态模式。
模式建议你将所有特定于状态的代码抽取到一组独立的类中。 这样一来, 你可以在独立于其他状态的情况下添加新状态或 修改已有状态, 从而减少维护成本。 -
如果某个类需要根据成员变量的当前值改变自身行为,从而 需要使用大量的条件语句时,可使用该模式。
状态模式会将这些条件语句的分支抽取到相应状态类的方法 中。 同时, 你还可以清除主要类中与特定状态相关的临时成 员变量和帮手方法代码。 -
当相似状态和基于条件的状态机转换中存在许多重复代码时, 可使用状态模式。
状态模式让你能够生成状态类层次结构, 通过将公用代码抽 取到抽象基类中来减少重复。
实现方式
- 确定哪些类是上下文。 它可能是包含依赖于状态的代码的已 有类; 如果特定于状态的代码分散在多个类中, 那么它可能 是一个新的类。
- 声明状态接口。 虽然你可能会需要完全复制上下文中声明的 所有方法, 但最好是仅把关注点放在那些可能包含特定于状 态的行为的方法上。
- 为每个实际状态创建一个继承于状态接口的类。 然后检查上 下文中的方法并将与特定状态相关的所有代码抽取到新建的 类中。
在将代码移动到状态类的过程中, 你可能会发现它依赖于上 下文中的一些私有成员。 你可以采用以下几种变通方式:
将这些成员变量或方法设为公有。
将需要抽取的上下文行为更改为上下文中的公有方法,然 后在状态类中调用。 这种方式简陋却便捷, 你可以稍后再 对其进行修补。
将状态类嵌套在上下文类中。这种方式需要你所使用的编 程语言支持嵌套类。 - 在上下文类中添加一个状态接口类型的引用成员变量, 以及 一个用于修改该成员变量值的公有设置器。
- 再次检查上下文中的方法, 将空的条件语句替换为相应的状态对象方法。
- 为切换上下文状态, 你需要创建某个状态类实例并将其传递 给上下文。 你可以在上下文、 各种状态或客户端中完成这项 工作。 无论在何处完成这项工作, 该类都将依赖于其所实例 化的具体类。
优缺点
优点:
- 单一职责原则。 将与特定状态相关的代码放在单独的类中
- 开闭原则。 无需修改已有状态类和上下文就能引入新状态。
- 通过消除臃肿的状态机条件语句简化上下文代码。
缺点: - 如果状态机只有很少的几个状态, 或者很少发生改变, 那么 应用该模式可能会显得小题大作。
与其他模式的关系
- 桥接、状态和策略(在某种程度上包括适配器)模式的接口 非常相似。 实际上, 它们都基于组合模式——即将工作委派 给其他对象, 不过也各自解决了不同的问题。 模式并不只是 以特定方式组织代码的配方, 你还可以使用它们来和其他开 发者讨论模式所解决的问题。
- 状态可被视为策略的扩展。 两者都基于组合机制: 它们都通 过将部分工作委派给“帮手”对象来改变其在不同情景下的 行为。 策略使得这些对象相互之间完全独立, 它们不知道其 他对象的存在。 但状态模式没有限制具体状态之间的依赖, 且允许它们自行改变在不同情景下的状态。
策略
结构
- 上下文(Context)维护指向具体策略的引用,且仅通过策略 接口与该对象进行交流。
- 策略(Strategy)接口是所有具体策略的通用接口,它声明 了一个上下文用于执行策略的方法。
- 具体策略(Concrete Strategies)实现了上下文所用算法的各 种不同变体。
- 当上下文需要运行算法时, 它会在其已连接的策略对象上调 用执行方法。 上下文不清楚其所涉及的策略类型与算法的执 行方式。
- 客户端(Client)会创建一个特定策略对象并将其传递给上 下文。 上下文则会提供一个设置器以便客户端在运行时替换 相关联的策略。
适合应用场景
- 当你想使用对象中各种不同的算法变体,并希望能在运行时切换算法时,可使用策略模式。
策略模式让你能够将对象关联至可以不同方式执行特定子任 务的不同子对象, 从而以间接方式在运行时更改对象行为。 - 当你有许多仅在执行某些行为时略有不同的相似类时,可使 用策略模式。
策略模式让你能将不同行为抽取到一个独立类层次结构中, 并将原始类组合成同一个, 从而减少重复代码。 - 如果算法在上下文的逻辑中不是特别重要,使用该模式能将 类的业务逻辑与其算法实现细节隔离开来。
策略模式让你能将各种算法的代码、 内部数据和依赖关系与 其他代码隔离开来。 不同客户端可通过一个简单接口执行算 法, 并能在运行时进行切换。 - 当类中使用了复杂条件运算符以在同一算法的不同变体中切 换时,可使用该模式。
策略模式将所有继承自同样接口的算法抽取到独立类中, 因 此不再需要条件语句。 原始对象并不实现所有算法的变体, 而是将执行工作委派给其中的一个独立算法对象。
实现方式
- 从上下文类中找出修改频率较高的算法(也可能是用于在运行时选择某个算法变体的复杂条件运算符)。
- 声明该算法所有变体的通用策略接口。
- 将算法逐一抽取到各自的类中, 它们都必须实现策略接口。
- 在上下文类中添加一个成员变量用于保存对于策略对象的引 用。 然后提供设置器以修改该成员变量。 上下文仅可通过策 略接口同策略对象进行交互, 如有需要还可定义一个接口来 让策略访问其数据。
- 客户端必须将上下文类与相应策略进行关联, 使上下文可以预期的方式完成其主要工作。
优缺点
优点:
- 你可以在运行时切换对象内的算法。
- 你可以将算法的实现和使用算法的代码隔离开来。
- 你可以使用组合来代替继承。
- 开闭原则。 你无需对上下文进行修改就能够引入新的策略。
缺点: - 如果你的算法极少发生改变, 那么没有任何理由引入新的类 和接口。 使用该模式只会让程序过于复杂。
- 客户端必须知晓策略间的不同——它需要选择合适的策略。
- 许多现代编程语言支持函数类型功能, 允许你在一组匿名函 数中实现不同版本的算法。 这样, 你使用这些函数的方式就 和使用策略对象时完全相同, 无需借助额外的类和接口来保 持代码简洁。
与其他模式的关系
- 桥接、状态和策略(在某种程度上包括适配器)模式的接口 非常相似。 实际上, 它们都基于组合模式——即将工作委派 给其他对象, 不过也各自解决了不同的问题。 模式并不只是 以特定方式组织代码的配方, 你还可以使用它们来和其他开 发者讨论模式所解决的问题。
- 命令和策略看上去很像, 因为两者都能通过某些行为来参数化对象。 但是, 它们的意图有非常大的不同。
你可以使用命令来将任何操作转换为对象。操作的参数将 成为对象的成员变量。 你可以通过转换来延迟操作的执 行、 将操作放入队列、 保存历史命令或者向远程服务发送 命令等。
另一方面,策略通常可用于描述完成某件事的不同方式, 让你能够在同一个上下文类中切换算法。 - 装饰可让你更改对象的外表, 策略则让你能够改变其本质。
- 模板方法基于继承机制: 它允许你通过扩展子类中的部分内 容来改变部分算法。 策略基于组合机制: 你可以通过对相应 行为提供不同的策略来改变对象的部分行为。 模板方法在类 层次上运作, 因此它是静态的。 策略在对象层次上运作, 因 此允许在运行时切换行为。
• 状态可被视为策略的扩展。 两者都基于组合机制: 它们都通 过将部分工作委派给“帮手”对象来改变其在不同情景下的 行为。 策略使得这些对象相互之间完全独立, 它们不知道其 他对象的存在。 但状态模式没有限制具体状态之间的依赖, 且允许它们自行改变在不同情景下的状态。
模板方法
结构
- 抽象类(AbstractClass) 会声明作为算法步骤的方法, 以 及依次调用它们的实际模板方法。 算法步骤可以被声明为 抽象 类型,也可以提供一些默认实现。
- 具体类(ConcreteClass)可以重写所有步骤,但不能重写模板方法自身。
适用场景
- 当你只希望客户端扩展某个特定算法步骤,而不是整个算法或其结构时,可使用模板方法模式。
模板方法将整个算法转换为一系列独立的步骤, 以便子类能 对其进行扩展, 同时还可让超类中所定义的结构保持完整。 - 当多个类的算法除一些细微不同之外几乎完全一样时,你可 使用该模式。 但其后果就是, 只要算法发生变化, 你就可能 需要修改所有的类。
在将算法转换为模板方法时, 你可将相似的实现步骤提取到 超类中以去除重复代码。 子类间各不同的代码可继续保留在 子类中。
实现方式
- 分析目标算法, 确定能否将其分解为多个步骤。 从所有子类 的角度出发, 考虑哪些步骤能够通用, 哪些步骤各不相同。
- 创建抽象基类并声明一个模板方法和代表算法步骤的一系列 抽象方法。 在模板方法中根据算法结构依次调用相应步骤。 可用 final 最终 修饰模板方法以防止子类对其进行重写。
- 虽然可将所有步骤全都设为抽象类型, 但默认实现可能会给 部分步骤带来好处, 因为子类无需实现那些方法。
- 可考虑在算法的关键步骤之间添加钩子。
- 为每个算法变体新建一个具体子类, 它必须实现所有的抽象步骤, 也可以重写部分可选步骤。
优缺点
优点:
1.你可仅允许客户端重写一个大型算法中的特定部分, 使得算 法其他部分修改对其所造成的影响减小。
2. 你可将重复代码提取到一个超类中。
缺点
- 部分客户端可能会受到算法框架的限制。
- 通过子类抑制默认步骤实现可能会导致违反_里氏替换原则_。
- 模板方法中的步骤越多, 其维护工作就可能会越困难。
与其他模式的关系
- 工厂方法是模板方法的一种特殊形式。 同时, 工厂方法可以 作为一个大型模板方法中的一个步骤。
- 模板方法基于继承机制: 它允许你通过扩展子类中的部分内 容来改变部分算法。 策略 基于组合机制: 你可以通过对相应 行为提供不同的策略来改变对象的部分行为。 模板方法在类 层次上运作, 因此它是静态的。 策略在对象层次上运作, 因 此允许在运行时切换行为。
访问者
结构
- 访问者(Visitor)接口声明了一系列以对象结构的具体元素 为参数的访问者方法。 如果编程语言支持重载, 这些方法的 名称可以是相同的, 但是其参数一定是不同的。
- 具体访问者(Concrete Visitor)会为不同的具体元素类实现相同行为的几个不同版本。
- 元素(Element) 接口声明了一个方法来“接收” 访问者。 该方法必须有一个参数被声明为访问者接口类型。
- 具体元素(Concrete Element)必须实现接收方法。该方法 的目的是根据当前元素类将其调用重定向到相应访问者的方 法。 请注意, 即使元素基类实现了该方法, 所有子类都必须 对其进行重写并调用访问者对象中的合适方法。
- 客户端(Client)通常会作为集合或其他复杂对象(例如一 个组合树) 的代表。 客户端通常不知晓所有的具体元素类, 因为它们会通过抽象接口与集合中的对象进行交互。
适用场景
- 如果你需要对一个复杂对象结构(例如对象树)中的所有元 素执行某些操作,可使用访问者模式。
访问者模式通过在访问者对象中为多个目标类提供相同操作 的变体, 让你能在属于不同类的一组对象上执行同一操作。 - 可使用访问者模式来清理辅助行为的业务逻辑。
该模式会将所有非主要的行为抽取到一组访问者类中, 使得程序的主要类能更专注于主要的工作。 - 当某个行为仅在类层次结构中的一些类中有意义,而在其他 类中没有意义时,可使用该模式。
你可将该行为抽取到单独的访问者类中, 只需实现接收相关 类的对象作为参数的访问者方法并将其他方法留空即可。
实现方式
- 在访问者接口中声明一组“访问”方法,分别对应程序中的 每个具体元素类。
- 声明元素接口。 如果程序中已有元素类层次接口, 可在层次 结构基类中添加抽象的“接收”方法。该方法必须接受访问 者对象作为参数。
- 在所有具体元素类中实现接收方法。 这些方法必须将调用重 定向到当前元素对应的访问者对象中的访问者方法上。
- 元素类只能通过访问者接口与访问者进行交互。 不过访问者 必须知晓所有的具体元素类, 因为这些类在访问者方法中都 被作为参数类型引用。
- 为每个无法在元素层次结构中实现的行为创建一个具体访问 者类并实现所有的访问者方法。
你可能会遇到访问者需要访问元素类的部分私有成员变量的 情况。 在这种情况下, 你要么将这些变量或方法设为公有, 这将破坏元素的封装; 要么将访问者类嵌入到元素类中。 后 一种方式只有在支持嵌套类的编程语言中才可能实现。 - 客户端必须创建访问者对象并通过“接收”方法将其传递给 元素。
优缺点
优点:
- 开闭原则。 你可以引入在不同类对象上执行的新行为, 且无 需对这些类做出修改。
- 单一职责原则。 可将同一行为的不同版本移到同一个类中。
- 访问者对象可以在与各种对象交互时收集一些有用的信息。 当你想要遍历一些复杂的对象结构(例如对象树), 并在结 构中的每个对象上应用访问者时, 这些信息可能会有所帮助。
缺点: - 每次在元素层次结构中添加或移除一个类时, 你都要更新所 有的访问者。
- 在访问者同某个元素进行交互时, 它们可能没有访问元素私 有成员变量和方法的必要权限。
与其他模式的关系
- 你可以将访问者视为命令模式的加强版本, 其对象可对不同
类的多种对象执行操作。 - 你可以使用访问者对整个组合树执行操作。
- 可以同时使用访问者和迭代器来遍历复杂数据结构, 并对其 中的元素执行所需操作, 即使这些元素所属的类完全不同。