浅析设计模式

Entropy Tree Lv4

简单地分析一下 23 种设计模式之间的联系以及如何选择比较合适的设计模式。

对于设计模式的理解,首先从设计模式遵循的七大原则开始。

设计模式的原则

设计模式原则是设计模式的基础,其核心原则是 SOLID 原则。SOLID 原则最初是针对面向对象编程(OOP)提出的,它在形式上依赖于 OOP 的许多概念,例如类、继承、封装和多态。但是 SOLID 原则背后的思想——创建灵活、可维护和可扩展的软件——是可以跨编程范式应用的。

通常在讨论设计模式的原则时,还会包括另外两个原则 (LoD 和 CARP),它们与 SOLID 原则一起被称为 “设计模式七大原则”。

关于设计模式的原则,存在六大原则和七大原则的说法,两者的区别主要在于是否将“组合/聚合复用原则”单独列出。

SOLID 原则

SOLID 原则是一组软件设计原则,用于指导软件开发人员设计和实现高质量的、易于维护和扩展的软件。它是由 Robert C. Martin 在其著作《Agile Software Development, Principles, Patterns, and Practices》中提出的,是目前软件工程界被广泛接受的一种软件设计理念。

SOLID 原则包括以下五个原则,其名称就是由这五个原则的英文名称首字母组成

  • Singe Responsibility Principle, SRP
  • Open-Closed Principle, OCP
  • Liskov Substitution Principle, LSP
  • Interface Segregation Principle, ISP
  • Dependency Inversion Principle, DIP

单一职责原则 SRP

说明:一个类应该只有一个引起它变化的原因。一个类只负责一项职责。

例子:一个 User 类通常只负责记录用户的数据,另一个类 UserValidator 负责验证用户数据。

但是职责的划分有时并不是非常明确的,在不同的情况下有不同的划分,User 类也可以兼有验证数据的职责。个人认为某个职责是否应该单独划分,取决于职责的复杂程度。如果验证的逻辑足够复杂,那么可以将这部分职责单独分离到 UserValidator 中,如果验证的逻辑非常简单,那么 User 类可以自行实现,而不必单独划分。最终目的是确保代码易于理解和维护。

开放封闭原则 OCP

说明:类应该对扩展开放,对修改关闭。软件实体应该可以扩展,但不可修改。

例子:扩展 Shape 类,优先考虑继承 Shape 类,增加新的 RectangleCircle 类,而不是修改 Shape 类本身。

OCP 的核心思想是在不修改现有代码的基础上,允许扩展功能,从而提高系统的灵活性和可维护性。最常见的形式就是类的继承、接口的实现。

此外,策略模式和装饰者模式都是在不修改已有代码的基础上扩展系统功能的设计模式。

子类重写父类中已经实现的方法,也违反了开闭原则,所以通常父类需要扩展的功能被定义为抽象方法。

里氏替换原则 LSP

说明:子类对象应该能够替换掉它们的父类对象,而不影响程序的行为。

例子Bird 类有 fly() 抽象方法,子类 Sparrow 也实现 fly() 方法,但 Penguin 类不应继承 Bird 类,因为它不能飞。

关于 LSP 的一个误解是,它似乎禁止子类对象拥有父类没有的方法 (这与开闭原则冲突)。实际上,LSP 所表达的是子类在重写或新增方法时,不应该改变父类方法的预期行为。Sparrow 类可以在 fly() 方法中进一步描述它如何 fly,但是不能把 “如何 fly” 描述为 “如何 run”,这已经篡改了方法原本的意图。

子类不能完全替换父类,有时候可能是因为父类的抽象程度不够高。

接口隔离原则 ISP

说明:不应该强迫客户端依赖于它们不使用的接口,使用多个特定接口而非单个总接口。

例子Printer 类实现 IPrinter 接口和 IScanner 接口,而不是实现一个大的 IMachine 接口。

ISP 可以被视为是针对接口的单一职责原则,要求每个接口只包含一个客户端所需的方法集合,确保接口聚焦于特定功能来实现。将大型接口分解为小型、特定的接口,通过分离关注点来提高代码的可维护性。前前面的 IMachine 接口为了兼顾到所有客户端的功能实现,会尽可能地包含所有的方法。对于单个客户端来说,这些方法中可能只有一小部分是有意义的,但由于接口的特性,每个实现了接口的客户端必须编写大量无意义的代码去实现不属于自身的方法。而且在调用者在调用某个客户端的方法时需要在一大堆无意义的方法中找到几个有实际意义的方法,增加了代码的维护难度。

ISP 中的 “客户端” 通常是指使用接口的类或组件,包括接口的实现类、接口的消费者、接口的调用者、接口的依赖者。

依赖倒置原则 DIP

说明:高层模块不应该依赖于低层模块,两者都应该依赖于抽象,而非具体实现。

例子Service 类依赖于 RepositoryInterface 接口,而不是具体的 MySQLRepository 实现。

DIP 通过接口将类与类进行解耦,Service 类如果直接依赖于 MySQLRepository 的实现,那么每新增一种实现,就需要修改 Service 类。依赖接口而非具体实现,可以使 Service 类只需要通过一个接口就能获取所有依赖于该接口的低层模块实例。

DIP 一个广泛典型的应用就是 Spring 框架中的控制反转和依赖注入。在传统的程序设计中,高层模块通常负责创建和维护低层模块的实例。控制反转使得高层模块不再负责创建低层模块的实例,而是由其他特殊模块进行实例的创建和维护。高层模块只需要提供方法,接受它们需要的低层模块实例作为参数,而不是自己创建这些实例。

另外两个原则

设计模式七大原则中除去 SOLID 五原则后,另外两个原则通常是 迪米特法则 (Law of Demeter, LoD) 与 合成/聚合复用原则 (Composite/Aggregate Reuse Principle, CARP)

迪米特法则 LoD

迪米特法则,也称为最少知识原则

说明:一个对象应尽量少地了解其他对象。

例子Car 类只调用 Engine 类的 start() 方法,而不直接调用 Piston 类的 move() 方法。

CarEngine 是直接关联,PistonEngine 直接关联,但是 Car 不应该直接操作 Piston 而是通过调用 Enginestart() 方法,start() 方法内部再去调用 move() 方法。本质上是通过建立间接关系,而非直接依赖,从而降低耦合度。

合成/聚合复用原则 CARP

说明:优先使用对象的组合/聚合,而不是通过继承来复用它们。

例子Car 类有一个 Engine 对象作为其成员,而不是从 Engine 类继承。

组合/聚合优于继承。优先考虑组合/聚合,而不是继承去实现类的复用。其最大的优势就是可以在运行时操作类的实例。

组合/聚合与继承对应的就是面向对象的经典关系 HAS-A 和 IS-A。

HAS-A 表示了部分与整体的关系,一个类是另一个类的一部分。

IS-A 表示了抽象与具体的关系,子类是父类的特化。

在对七大原则有了初步理解后,接下来就是分析具体的设计模式。设计模式有很多种,最经典的设计模式有 23 种。

设计模式的应用

设计模式被分为创建型模式、结构型模式、行为模式。

创建型模式

创建型模式提供了创建对象的机制,能够提升已有代码的灵活性和可复用性。该分类下有 5 种具体的设计模式。

工厂方法

工厂方法模式在父类中提供一个创建对象的方法,允许子类决定实例化对象的类型。

工厂方法模式包含了产品、具体产品、创建者、具体创建者。

  • 产品 (Product) 将会对接口进行声明。对于所有由创建者及其子类构建的对象,这些接口都是通用的。
  • 具体产品 (Concrete Products) 是产品接口的不同实现。
  • 创建者 (Creator) 类声明返回产品对象的工厂方法。该方法的返回对象类型必须与产品接口相匹配。
  • 具体创建者 (Concrete Creators) 将会重写基础工厂方法,使其返回不同类型的产品。

注意,并不一定每次调用工厂方法都会创建新的实例。工厂方法也可以返回缓存、对象池或其他来源的已有对象。

工厂方法模式的应用场景

  • 无法预知对象确切类别及其依赖关系时
  • 希望其他用户能扩展软件库或框架的内部组件
  • 希望复用现有对象来节省系统资源,而不是每次都重新创建对象。

工厂方法可以避免创建者和具体产品之间的紧密耦合,但是应用工厂方法模式需要引入许多新的子类,代码可能会因此变得更复杂。最好的情况是将该模式引入创建者类的现有层次结构中 (简单工厂)

与其他模式的关系

抽象工厂

抽象工厂模式能创建一系列相关的对象 (产品簇),而无需指定其具体类。

在抽象工厂模式中,产品簇是指工厂生产出的产品之间具备强关联。

产品簇的划分维度有很多,可以是同一种风格或品牌下的产品系列,例如,Windows 或 Mac 下的 GUI 风格组件。也可以基于产品的用途来分类,即使产品在视觉或技术上有所不同,例如不同类型的交通工具。在某些情况下,还可以按同一个产品的不同规格配置或生产年代来划分,例如不同年代生产的不同内存规格的 iPad 产品。

抽象工厂模式包含了抽象产品、具体产品、抽象工厂、具体工厂、客户端。

  • 抽象产品 (Abstract Product) 为构成系列产品的一组不同但相关的产品声明接口。

  • 具体产品 (Concrete Product) 是抽象产品的多种不同类型实现。所有变体都必须实现相应的抽象产品。

  • 抽象工厂 (Abstract Factory) 接口声明了一组创建各种抽象产品的方法。

  • 具体工厂 (Concrete Factory) 实现抽象工厂的构建方法。每个具体工厂都对应特定产品变体,且仅创建此种产品变体。

    尽管具体工厂会对具体产品进行初始化,其构建方法签名必须返回相应的抽象产品。这样,使用工厂类的客户端代码就不会与工厂创建的特定产品变体耦合。

  • 客户端 (Client) 只需通过抽象接口调用工厂和产品对象,就能与任何具体工厂/产品变体交互。

抽象工厂模式的应用场景

  • 代码需要与多个不同系列的相关产品交互,但是由于无法提前获取相关信息,或者出于对未来扩展性的考虑,不希望代码基于产品的具体类进行构建。
  • 有一个基于一组抽象方法的类,且其主要功能因此变得不明确。

与其他模式的关系

  • 抽象工厂模式 通常基于一组工厂方法 ,但你也可以使用原型模式 来生成这些类的方法。

  • 当只需对客户端代码隐藏子系统创建对象的方式时,你可以使用抽象工厂 来代替外观模式

  • 你可以将抽象工厂 桥接模式 搭配使用。如果由桥接定义的抽象只能与特定实现合作,这一模式搭配就非常有用。在这种情况下,抽象工厂可以对这些关系进行封装,并且对客户端代码隐藏其复杂性。

抽象工厂与工厂方法最大的区别就是产品簇的划分。

工厂方法是简单的工厂与产品的一对一关系,一个工厂负责创建一个具体的产品类的实例。

抽象工厂是一种工厂与产品的一对多关系,一个抽象工厂可以创建多个属于同一产品簇的产品。

生成器

生成器模式能够将复杂对象分步骤创建,可以使用相同的创建代码生成不同类型的和形式的对象。

生成器模式包括生成器、具体生成器、产品、主管、客户端。

  • 生成器 (Builder) 接口声明在所有类型生成器中通用的产品构造步骤。
  • 具体生成器 (Concrete Builders) 提供构造过程的不同实现。具体生成器也可以构造不遵循通用接口的产品。
  • 产品 (Products) 是最终生成的对象。由不同生成器构造的产品无需属于同一类层次结构或接口。
  • 主管 (Director) 类定义调用构造步骤的顺序,这样就可以创建和复用特定的产品配置。
  • 客户端 (Client) 必须将某个生成器对象与主管类关联。

一般情况下,你只需通过主管类构造函数的参数进行一次性关联即可。此后主管类就能使用生成器对象完成后续所有的构造任务。但在客户端将生成器对象传递给主管类制造方法时还有另一种方式。在这种情况下,你在使用主管类生产产品时每次都可以使用不同的生成器

生成器模式的应用场景

  • 使用生成器模式可避免 “重叠构造函数 (telescoping constructor)” 的出现。
  • 希望使用代码创建不同形式的产品时。
  • 构造组合树或其他复杂对象。

生成器模式可以分步创建对象,暂缓创建步骤或递归运行创建步骤。生成不同形式的产品时,可以复用相同的制造代码。遵循单一职责原则,将复杂构造代码从产品的业务逻辑中分离出来。

与其他模式的关系

  • 生成器 重点关注如何分步生成复杂对象。抽象工厂 专门用于生产一系列相关对象。抽象工厂会马上返回产品,生成器则允许在获取产品前执行一些额外构造步骤。

  • 可以在创建复杂组合模式 树时使用生成器 ,因为这可使其构造步骤以递归的方式运行。

  • 可以结合使用生成器 桥接模式 :主管类负责抽象工作,各种不同的生成器负责实现工作。

原型

原型模式能够复制已有对象,而又无需使代码依赖它们所属的类。

原型模式包括原型、具体原型、客户端。此外还有原型注册表进一步改造代码。

  • 原型 (Prototype) 接口将对克隆方法进行声明。在绝大多数情况下,其中只会有一个名为 clone克隆的方法。
  • 具体原型 (Concrete Prototype) 类将实现克隆方法。除了将原始对象的数据复制到克隆体中之外,该方法有时还需处理克隆过程中的极端情况,例如克隆关联对象和梳理递归依赖等等 (深拷贝)。
  • 客户端 (Client) 可以复制实现了原型接口的任何对象。

原型注册表

  • 原型注册表 (Prototype Registry) 提供了一种访问常用原型的简单方法,其中存储了一系列可供随时复制的预生成对象。最简单的注册表原型是一个 名称 → 原型 的哈希表。但如果需要使用名称以外的条件进行搜索,你可以创建更加完善的注册表版本。

原型模式的一个显著特征就是原型具有 clone() 方法。

另外,通常提到原型模式中的 clone() 方法,默认情况下它指的是浅拷贝。在 Java 中,Object 类的 clone() 方法是一个 native 修饰的方法,这意味着它是由 JVM 底层实现的,是通过直接复制对象在内存中的数据来实现的一种浅拷贝。

因此,如果自定义实现 clone() 方法,而不是实现 Cloneable 接口,并不会获得 JVM 的性能优化效果。

原型模式的应用场景

  • 需要复制一些对象,同时又希望代码独立于这些对象所属的具体类。
  • 如果子类的区别仅在于其对象的初始化方式,那么可以使用的该模式来减少子类的数量。

原型模式在克隆包含循环引用的复杂对象时可能会非常麻烦,需要手动处理循环引用。

与其他模式的关系

  • 原型 可用于保存命令模式 的历史记录。

  • 大量使用组合模式 装饰模式 的设计通常可从对于原型 的使用中获益。你可以通过该模式来复制复杂结构,而非从零开始重新构造。

  • 原型 并不基于继承,因此没有继承的缺点。另一方面,原型需要对被复制对象进行复杂的初始化。工厂方法 基于继承,但是它不需要初始化步骤。

  • 有时候原型 可以作为备忘录模式 的一个简化版本,其条件是你需要在历史记录中存储的对象的状态比较简单,不需要链接其他外部资源,或者链接可以方便地重建。

单例

单例模式能够保证一个类只有一个实例,并提供一个访问该实例的全局节点。

单例模式包含了最重要的单例类,其中定义了获取实例的静态方法,该方法也是获取实例的唯一方式。

  • 单例 (Singleton) 类声明了一个名为 get­Instance 获取实例的静态方法来返回其所属类的一个相同实例。

    单例的构造函数必须对客户端 (Client) 代码隐藏。调用 获取实例 方法必须是获取单例对象的唯一方式。

单例模式的显著特征是私有化构造方法。

单例模式的应用场景

  • 程序中的某个类对于所有客户端只有一个可用的实例。
  • 需要更加严格地控制全局变量。

单例模式是违反了单一职责原则的设计模式,它确保了实例的唯一性并提供了全局访问点。单例模式在多线程环境下需要进行特殊处理,避免多个线程多次创建对象,实现方式可参考菜鸟教程 。单例模式的客户端代码由于私有化构造函数,难以使用单元测试。

与其他模式的关系

结构型模式

结构型模式介绍如何将对象和类组装成较大的结构,并同时保持结构的灵活和高效。该分类下有 7 种模式具体的设计模式。

适配器

适配器模式能使接口不兼容的对象能够相互合作。

适配器模式包含了客户端、客户端接口、服务、适配器。

  • 客户端 (Client) 是包含当前程序业务逻辑的类。
  • 客户端接口 (Client Interface) 描述了其他类与客户端代码合作时必须遵循的协议。
  • 服务 (Service) 中有一些功能类(通常来自第三方或遗留系统)。客户端与其接口不兼容,因此无法直接调用其功能。
  • 适配器 (Adapter) 是一个可以同时与客户端和服务交互的类:它在实现客户端接口的同时封装了服务对象。适配器接受客户端通过适配器接口发起的调用,并将其转换为适用于被封装服务对象的调用。

客户端代码只需通过接口与适配器交互即可,无需与具体的适配器类耦合。因此,你可以向程序中添加新类型的适配器而无需修改已有代码。这在服务类的接口被更改或替换时很有用:你无需修改客户端代码就可以创建新的适配器类。

关于适配器模式中的适配,被适配的一方通常指的是源代码不可修改的组件或系统,适配的一方则是我们可以控制源代码的组件。

适配器模式的应用场景

  • 希望使用某个类,但是其接口与其他代码不兼容时。
  • 需要复用这样一些类,他们处于同一个继承体系,并且他们又有了额外的一些共同的方法,但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。

与其他模式的关系

  • 适配器 可以对已有对象的接口进行修改,装饰模式 则能在不改变对象接口的前提下强化对象功能。此外,装饰还支持递归组合,适配器则无法实现。

桥接

桥接模式可将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构,从而能在开发时分别使用。

桥接模式包含了抽象部分、实现部分、具体实现、精确抽象 (可选)、客户端。

  • 抽象部分 (Abstraction) 提供高层控制逻辑,依赖于完成底层实际工作的实现对象。

  • 实现部分 (Implementation) 为所有具体实现声明通用接口。抽象部分仅能通过在这里声明的方法与实现对象交互。

    抽象部分可以列出和实现部分一样的方法,但是抽象部分通常声明一些复杂行为,这些行为依赖于多种由实现部分声明的原语操作。

  • 具体实现 (Concrete Implementations) 中包括特定于平台的代码。

  • 精确抽象 (Refined Abstraction) 提供控制逻辑的变体。与其父类一样,它们通过通用实现接口与不同的实现进行交互。

  • 通常情况下,客户端 (Client) 仅关心如何与抽象部分合作。但是,客户端需要将抽象对象与一个实现对象连接起来。

桥接模式的应用场景

  • 拆分或重组一个具有多重功能的庞杂类 (例如能与多个数据库服务器进行交互的类) 时。
  • 希望在几个独立维度上扩展一个类时。
  • 需要在运行时切换不同实现方法时。

桥接模式下可以创建与平台无关的类和程序,客户端代码仅与高层抽象部分进行互动,不会接触到平台的详细信息。桥接模式遵循开闭原则,可以新增抽象部分和实现,且它们之间不会相互影响。桥接模式遵循单一职责原则,抽象部分专注于处理高层逻辑,实现部分处理平台细节。但是对高内聚的类使用该模式可能会让代码更加复杂。

与其他模式的关系

  • 桥接模式 通常会于开发前期进行设计,使你能够将程序的各个部分独立开来以便开发。另一方面,适配器模式 通常在已有程序中使用,让相互不兼容的类能很好地合作。

组合

组合模式将对象组合成树状结构,并且能像使用独立对象一样使用它们。

组合模式包含了组件、叶节点、容器、客户端。

  • 组件 (Component) 接口描述了树中简单项目和复杂项目所共有的操作。

  • 叶节点 (Leaf) 是树的基本结构,它不包含子项目。

    一般情况下,叶节点最终会完成大部分的实际工作,因为它们无法将工作指派给其他部分。

  • 容器 (Container) ——又名 “组合 (Composite)” —— 是包含叶节点或其他容器等子项目的单位。容器不知道其子项目所属的具体类,它只通过通用的组件接口与其子项目交互。

    容器接收到请求后会将工作分配给自己的子项目,处理中间结果,然后将最终结果返回给客户端。

  • 客户端 (Client) 通过组件接口与所有项目交互。因此,客户端能以相同方式与树状结构中的简单或复杂项目交互。

组合模式的应用场景

  • 需要实现树状对象结构时。
  • 希望客户端代码以相同方式处理简单和复杂元素时。

对于功能差异较大的类,提供公共接口会有困难,因此不推荐使用组合模式。在特定情况下,你需要过度一般化组件接口,使其变得令人难以理解。

与其他模式的关系

  • 桥接模式 状态模式 策略模式 (在某种程度上包括适配器模式 ) 的接口非常相似。实际上,它们都基于组合模式 —— 即将工作委派给其他对象,不过也各自解决了不同的问题。模式并不只是以特定方式组织代码的配方,你还可以使用它们来和其他开发者讨论模式所解决的问题。

  • 责任链模式 通常和组合模式 结合使用。在这种情况下,叶组件接收到请求后,可以将请求沿包含全体父组件的链一直传递至对象树的底部。

  • 你可以使用迭代器模式 来遍历组合 树。

  • 你可以使用访问者模式 对整个组合 树执行操作。

  • 你可以使用享元模式 实现组合 树的共享叶节点以节省内存。

  • 组合 装饰模式 的结构图很相似,因为两者都依赖递归组合来组织无限数量的对象。

    装饰类似于组合,但其只有一个子组件。此外还有一个明显不同:装饰为被封装对象添加了额外的职责,组合仅对其子节点的结果。进行了“求和”。

    但是,模式也可以相互合作:你可以使用装饰来扩展组合树中特定对象的行为。

装饰

装饰模式允许你通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。

装饰模式包含了部件、具体部件、基础装饰、具体装饰类、客户端。

  • 部件 (Component) 声明封装器和被封装对象的公用接口。
  • 具体部件 (Concrete Component) 类是被封装对象所属的类。它定义了基础行为,但装饰类可以改变这些行为。
  • 基础装饰 (Base Decorator) 类拥有一个指向被封装对象的引用成员变量。该变量的类型应当被声明为通用部件接口,这样它就可以引用具体的部件和装饰。装饰基类会将所有操作委派给被封装的对象。
  • 具体装饰类 (Concrete Decorators) 定义了可动态添加到部件的额外行为。具体装饰类会重写装饰基类的方法,并在调用父类方法之前或之后进行额外的行为。
  • 客户端 (Client) 可以使用多层装饰来封装部件,只要它能使用通用接口与所有对象互动即可。

装饰模式的应用场景

  • 希望在无需修改代码的情况下即可使用对象,且希望在运行时为对象新增额外的行为。
  • 如果用继承来扩展对象行为的方案难以实现或者根本不可行时,可以使用该模式。

装饰模式无需创建新子类即可扩展对象的行为,可以在运行时添加或删除功能。可以用多个装饰封装对象来组合几种行为。可以将实现了许多不同行为的一个大类拆分为多个较小的类。但是在封装器栈中删除特定封装器比较困难,实现行为不受装饰栈顺序影响的装饰也比较困难,各层的初始化配置代码看上去可能难以理解。

与其他模式的关系

  • 装饰 可让你更改对象的外表,策略模式 则让你能够改变其本质。
  • 装饰 代理 有着相似的结构,但是其意图却非常不同。这两个模式的构建都基于组合原则,也就是说一个对象应该将部分工作委派给另一个对象。两者之间的不同之处在于代理通常自行管理其服务对象的生命周期,而装饰的生成则总是由客户端进行控制。

外观

外观模式能为程序库、框架或其他复杂类提供一个简单的接口。

外观模式包含了外观、附加外观、复杂子系统、客户端。

  • 外观 (Facade) 提供了一种访问特定子系统功能的便捷方式,其了解如何重定向客户端请求,知晓如何操作一切活动部件。

  • 创建附加外观 (Additional Facade) 类可以避免多种不相关的功能污染单一外观,使其变成又一个复杂结构。客户端和其他外观都可使用附加外观。

  • 复杂子系统 (Complex Subsystem) 由数十个不同对象构成。如果要用这些对象完成有意义的工作,你必须深入了解子系统的实现细节,比如按照正确顺序初始化对象和为其提供正确格式的数据。

    子系统类不会意识到外观的存在,它们在系统内运作并且相互之间可直接进行交互。

  • 客户端 (Client) 使用外观代替对子系统对象的直接调用。

外观模式的应用场景

  • 需要一个指向复杂子系统的直接接口,且该接口的功能有限。
  • 需要将子系统组织为多层结构,可以使用此模式。

外观模式可以让自己的代码独立于复杂子系统,但是外观也可能成为与程序中所有类都耦合的上帝对象。

与其他模式的关系

  • 外观模式 为现有对象定义了一个新接口,适配器模式 则会试图运用已有的接口。适配器通常只封装一个对象,外观通常会作用于整个对象子系统上。
  • 外观 中介者模式 的职责类似:它们都尝试在大量紧密耦合的类中组织起合作
    • 外观为子系统中的所有对象定义了一个简单接口,但是它不提供任何新功能。子系统本身不会意识到外观的存在。子系统中的对象可以直接进行交流。
    • 中介者将系统中组件的沟通行为中心化。各组件只知道中介者对象,无法直接相互交流。
  • 外观 类通常可以转换为单例模式 类,因为在大部分情况下一个外观对象就足够了。
  • 外观 代理模式 的相似之处在于它们都缓存了一个复杂实体并自行对其进行初始化。代理与其服务对象遵循同一接口,使得自己和服务对象可以互换,在这一点上它与外观不同。

享元

享元模式摒弃了在每个对象中保存所有数据的方式,通过共享多个对象的所共有的相同状态,让你能在有限的内存容量中载入更多对象。

享元模式包含了享元、情景、享元工厂、客户端。

  • 享元 (Flyweight) 类包含原始对象中部分能在多个对象中共享的状态。同一享元对象可在许多不同情景中使用。享元中存储的状态被称为 “内在状态”。传递给享元方法的状态被称为 “外在状态”。

  • 情景 (Context) 类包含原始对象中各不相同的外在状态。情景与享元对象组合在一起就能表示原始对象的全部状态。

    通常情况下,原始对象的行为会保留在享元类中。因此调用享元方法必须提供部分外在状态作为参数。但你也可将行为移动到情景类中,然后将连入的享元作为单纯的数据对象。

  • 客户端 (Client) 负责计算或存储享元的外在状态。在客户端看来,享元是一种可在运行时进行配置的模板对象,具体的配置方式为向其方法中传入一些情景数据参数。

  • 享元工厂 (Flyweight Factory) 会对已有享元的缓存池进行管理。有了工厂后,客户端就无需直接创建享元,它们只需调用工厂并向其传递目标享元的一些内在状态即可。工厂会根据参数在之前已创建的享元中进行查找,如果找到满足条件的享元就将其返回;如果没有找到就根据参数新建享元。

享元模式只是一种优化。在应用该模式之前,你要确定程序中存在与大量类似对象同时占用内存相关的内存消耗问题,并且确保该问题无法使用其他更好的方式来解决。

享元模式的应用场景

  • 仅在程序必须支持大量对象且没有足够的内存容量时。

享元模式的应用场景很明确,但是要考虑牺牲执行速度来换取内存。

与其他模式的关系

  • 可以使用享元模式 实现组合模式 树的共享叶节点以节省内存。
  • 享元 展示了如何生成大量的小型对象,外观模式 则展示了如何用一个对象来代表整个子系统。
  • 如果你能将对象的所有共享状态简化为一个享元对象,那么享元模式 就和单例 类似了。但这两个模式有两个根本性的不同。
    1. 单例类只会有一个单例实体,但是享元类可以有多个实体,各实体的内在状态也可以不同。
    2. 单例对象可以是可变的。享元对象是不可变的。

代理

代理模式能够提供对象的替代品或其占位符。代理控制着对于原对象的访问,并允许在将请求提交给对象前后进行一些处理。

代理模式包含了服务接口、服务、代理、客户端。

  • 服务接口 (Service Interface) 声明了服务接口。代理必须遵循该接口才能伪装成服务对象。

  • 服务 (Service) 类提供了一些实用的业务逻辑。

  • 代理 (Proxy) 类包含一个指向服务对象的引用成员变量。代理完成其任务(例如延迟初始化、记录日志、访问控制和缓存等)后会将请求传递给服务对象。

    通常情况下,代理会对其服务对象的整个生命周期进行管理。

  • 客户端 (Client) 能通过同一接口与服务或代理进行交互,所以你可在一切需要服务对象的代码中使用代理。

代理模式的应用场景

使用代理模式的方式多种多样,以下最常见的几种

  • 延迟初始化 (虚拟代理)。如果你有一个偶尔使用的重量级服务对象,一直保持该对象运行会消耗系统资源时,可使用该模式。
  • 访问控制 (保护代理)。如果你只希望特定客户端使用服务对象,这里的对象可以是操作系统中非常重要的部分,而客户端则是各种已启动的程序 (包括恶意程序),此时可用代理模式。
  • 本地执行远程服务 (远程代理)。适用于服务对象位于远程服务器上的情景。
  • 记录日志请求 (日志记录代理)。适用于当你需要保存对于服务对象的请求历史记录时。
  • 缓存请求结果 (缓存代理)。适用于是需要缓存客户端请求结果并对缓存生命周期进行管理时,特别是当返回结果的体积非常大时。
  • 智能引用。可在没有客户端使用某个重量级对象时立即销毁该对象。

代理模式可以在客户端毫无察觉的情况下控制服务对象,可以对其生命周期进行管理,即使服务对象还未准备好或不存在,代理也可以正常工作。代理模式遵循开闭原则,可以在不对服务或客户端做出修改的情况下创建新代理。但是代理模式下服务响应可能会延迟。

与其他模式的关系

  • 适配器 能为被封装对象提供不同的接口,代理模式 能为对象提供相同的接口,装饰 则能为对象提供加强的接口。

行为模式

行为模式负责对象间的高效沟通和职责委派。该分类下有 11 种具体的设计模式,但是由于解释器设计模式适用于编译原理或语法引擎等相关的领域,这里不会详细介绍此模式。

责任链

责任链模式允许你将请求沿着处理者链进行发送。收到请求后,每个处理者均可对请求进行处理,或将其传递给链上的下个处理者。

责任链模式包含了处理者、基础处理者、具体处理者、客户端。

  • 处理者 (Handler) 声明了所有具体处理者的通用接口。该接口通常仅包含单个方法用于请求处理,但有时其还会包含一个设置链上下个处理者的方法。

  • 基础处理者 (Base Handler) 是一个可选的类,你可以将所有处理者共用的样本代码放置在其中。

    通常情况下,该类中定义了一个保存对于下个处理者引用的成员变量。客户端可通过将处理者传递给上个处理者的构造函数或设定方法来创建链。该类还可以实现默认的处理行为:确定下个处理者存在后再将请求传递给它。

  • 具体处理者 (Concrete Handlers) 包含处理请求的实际代码。每个处理者接收到请求后,都必须决定是否进行处理,以及是否沿着链传递请求。

    处理者通常是独立且不可变的,需要通过构造函数一次性地获得所有必要地数据。

  • 客户端 (Client) 可根据程序逻辑一次性或者动态地生成链。值得注意的是,请求可发送给链上的任意一个处理者,而非必须是第一个处理者。

责任链模式的应用场景

  • 当程序需要使用不同方式处理不同种类请求,而且请求类型和顺序预先未知时,可以使用责任链模式。
  • 当必须按顺序执行多个处理者时,可以使用该模式。
  • 如果所需处理者及其顺序必须在运行时进行改变,可以使用责任链模式。

与其他模式的关系

  • 责任链 的管理者可使用命令模式 实现。在这种情况下,你可以对由请求代表的同一个上下文对象执行许多不同的操作。

    还有另外一种实现方式,那就是请求自身就是一个命令对象。在这种情况下,你可以对由一系列不同上下文连接而成的链执行相同的操作。

  • 责任链模式 装饰模式 的类结构非常相似。两者都依赖递归组合将需要执行的操作传递给一系列对象。但是,两者有几点重要的不同之处。

    责任链 的管理者可以相互独立地执行一切操作,还可以随时停止传递请求。另一方面,各种装饰可以在遵循基本接口的情况下扩展对象的行为。此外,装饰无法中断请求的传递。

命令

命令模式可将请求转换为一个包含与请求相关的所有信息的独立对象。该转换让你能根据不同的请求将方法参数化、延迟请求执行或将其放入队列中,且能实现可撤销操作。

命令模式包含了发送者、命令、具体命令、接收者、客户端。

  • 发送者 (Sender) ——亦称 “触发者 (Invoker)” —— 类负责对请求进行初始化,其中必须包含一个成员变量来存储对于命令对象的引用。发送者触发命令,而不向接收者直接发送请求。注意,发送者并不负责创建命令对象:它通常会通过构造函数从客户端处获得预先生成的命令。

  • 命令 (Command) 接口通常仅声明一个执行命令的方法。

  • 具体命令 (Concrete Commands) 会实现各种类型的请求。具体命令自身并不完成工作,而是会将调用委派给一个业务逻辑对象。但为了简化代码,这些类可以进行合并。

    接收对象执行方法所需的参数可以声明为具体命令的成员变量。你可以将命令对象设为不可变,仅允许通过构造函数对这些成员变量进行初始化。

  • 接收者 (Receiver) 类包含部分业务逻辑。几乎任何对象都可以作为接收者。绝大部分命令只处理如何将请求传递到接收者的细节,接收者自己会完成实际的工作。

  • 客户端 (Client) 会创建并配置具体命令对象。客户端必须将包括接收者实体在内的所有请求参数传递给命令的构造函数。此后,生成的命令就可以与一个或多个发送者相关联了。

命令模式的应用场景

  • 如果你需要通过操作来参数化对象,可使用命令模式。
  • 如果你想要将操作放入队列中、操作的执行或者远程执行操作,可使用命令模式。
  • 如果你想要实现操作回滚功能,可使用命令模式。

与其他模式的关系

  • 你可以同时使用命令 备忘录模式 来实现“撤销”。在这种情况下,命令用于对目标对象执行各种不同的操作,备忘录用来保存一条命令执行前该对象的状态。
  • 命令 策略模式 看上去很像,因为两者都能通过某些行为来参数化对象。但是,它们的意图有非常大的不同。
    • 你可以使用命令来将任何操作转换为对象。操作的参数将成为对象的成员变量。你可以通过转换来延迟操作的执行、将操作放入队列、保存历史命令或者向远程服务发送命令等。
    • 另一方面,策略通常可用于描述完成某件事的不同方式,让你能够在同一个上下文类中切换算法。
  • 你可以将访问者模式 视为命令模式 的加强版本,其对象可对不同类的多种对象执行操作。

迭代器

迭代器模式让你能在不暴露集合底层表现形式(列表、栈和树等)的情况下遍历集合中所有的元素。

迭代器模式包含了迭代器、具体迭代器、集合、具体集合、客户端。

  • 迭代器 (Iterator) 接口声明了遍历集合所需的操作:获取下一个元素、获取当前位置和重新开始迭代等。

  • 具体迭代器 (Concrete Iterators) 实现遍历集合的一种特定算法。迭代器对象必须跟踪自身遍历的进度。这使得多个迭代器可以相互独立地遍历同一集合。

  • 集合 (Collection) 接口声明一个或多个方法来获取与集合兼容的迭代器。请注意,返回方法的类型必须被声明为迭代器接口,因此具体集合可以返回各种不同种类的迭代器。

  • 具体集合 (Concrete Collections) 会在客户端请求迭代器时返回一个特定的具体迭代器类实体。你可能会琢磨,剩下的集合代码在什么地方呢?不用担心,它也会在同一个类中。只是这些细节对于实际模式来说并不重要,所以我们将其省略了而已。

  • 客户端 (Client) 通过集合和迭代器的接口与两者进行交互。这样一来客户端无需与具体类进行耦合,允许同一客户端代码使用各种不同的集合和迭代器。

    客户端通常不会自行创建迭代器,而是会从集合中获取。但在特定情况下,客户端可以直接创建一个迭代器(例如当客户端需要自定义特殊迭代器时)。

迭代器模式的应用场景

  • 当集合背后为复杂的数据结构,且你希望对客户端隐藏其复杂性时(出于使用便利性或安全性的考虑),可以使用迭代器模式。
  • 使用该模式可以减少程序中重复的遍历代码。
  • 如果你希望代码能够遍历不同的甚至是无法预知的数据结构,可以使用迭代器模式。

与其他模式的关系

  • 你可以同时使用备忘录模式 迭代器 来获取当前迭代器的状态,并且在需要的时候进行回滚。
  • 可以同时使用访问者模式 迭代器 来遍历复杂数据结构,并对其中的元素执行所需操作,即使这些元素所属的类完全不同。

中介者

中介者模式能让你减少对象之间混乱无序的依赖关系。该模式会限制对象之间的直接交互,迫使它们通过一个中介者对象进行合作。

中介者模式包含了组件、中介者、具体中介者。

  • 组件 (Component) 是各种包含业务逻辑的类。每个组件都有一个指向中介者的引用,该引用被声明为中介者接口类型。组件不知道中介者实际所属的类,因此你可通过将其连接到不同的中介者以使其能在其他程序中复用。
  • 中介者 (Mediator) 接口声明了与组件交流的方法,但通常仅包括一个通知方法。组件可将任意上下文(包括自己的对象)作为该方法的参数,只有这样接收组件和发送者类之间才不会耦合。
  • 具体中介者 (Concrete Mediator) 封装了多种组件间的关系。 具体中介者通常会保存所有组件的引用并对其进行管理, 甚至有时会对其生命周期进行管理。

组件并不知道其他组件的情况。如果组件内发生了重要事件,它只能通知中介者。中介者收到通知后能轻易地确定发送者,这或许已足以判断接下来需要触发的组件了。

对于组件来说,中介者看上去完全就是一个黑箱。发送者不知道最终会由谁来处理自己的请求,接收者也不知道最初是谁发出了请求。

中介者模式的应用场景

  • 当一些对象和其他对象紧密耦合以致难以对其进行修改时,可使用中介者模式。
  • 当组件因过于依赖其他组件而无法在不同应用中复用时,可使用中介者模式。
  • 如果为了能在不同情景下,复用一些基本行为,导致你需要被迫创建大量组件子类时,可使用中介者模式。

与其他模式的关系

  • 外观模式 中介者 的职责类似:它们都尝试在大量紧密耦合的类中组织起合作。

    • 外观为子系统中的所有对象定义了一个简单接口,但是它不提供任何新功能。子系统本身不会意识到外观的存在。子系统中的对象可以直接进行交流。
    • 中介者将系统中组件的沟通行为中心化。各组件只知道中介者对象,无法直接相互交流。
  • 中介者 观察者 之间的区别往往很难记住。在大部分情况下,你可以使用其中一种模式,而有时可以同时使用。

    中介者的主要目标是消除一系列系统组件之间的相互依赖。这些组件将依赖于同一个中介者对象。观察者的目标是在对象之间建立动态的单向连接,使得部分对象可作为其他对象的附属发挥作用。

    有一种流行的中介者模式实现方式依赖于观察者。中介者对象担当发布者的角色,其他组件则作为订阅者,可以订阅中介者的事件或取消订阅。当中介者以这种方式实现时,它可能看上去与观察者非常相似。

    当你感到疑惑时,记住可以采用其他方式来实现中介者。例如,你可永久性地将所有组件链接到同一个中介者对象。这种实现方式和观察者并不相同,但这仍是一种中介者模式。

    假设有一个程序,其所有的组件都变成了发布者,它们之间可以相互建立动态连接。这样程序中就没有中心化的中介者对象,而只有一些分布式的观察者。

备忘录

备忘录模式允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。

备忘录模式包含了原发器、备忘录、负责人。

  • 原发器 (Originator) 类可以生成自身状态的快照,也可以在需要时通过快照恢复自身状态。

  • 备忘录 (Memento) 是原发器状态快照的值对象(value object)。通常做法是将备忘录设为不可变的,并通过构造函数一次性传递数据。

  • 负责人 (Caretaker) 仅知道 “何时” 和 “为何” 捕捉原发器的状态,以及何时恢复状态。

    负责人通过保存备忘录栈来记录原发器的历史状态。当原发器需要回溯历史状态时,负责人将从栈中获取最顶部的备忘录,并将其传递给原发器的恢复(restoration)方法。

在该实现方法中,备忘录类将被嵌套在原发器中。这样原发器就可访问备忘录的成员变量和方法,即使这些方法被声明为私有。另一方面,负责人对于备忘录的成员变量和方法的访问权限非常有限:它们只能在栈中保存备忘录,而不能修改其状态。

如果你不想让其他类有任何机会通过备忘录来访问原发器的状态, 那么还有另一种可用的实现方式

  • 这种实现方式允许存在多种不同类型的原发器和备忘录。每种原发器都和其相应的备忘录类进行交互。原发器和备忘录都不会将其状态暴露给其他类。
  • 负责人此时被明确禁止修改存储在备忘录中的状态。但负责人类将独立于原发器,因为此时恢复方法被定义在了备忘录类中。
  • 每个备忘录将与创建了自身的原发器连接。原发器会将自己及状态传递给备忘录的构造函数。由于这些类之间的紧密联系,只要原发器定义了合适的设置器(setter),备忘录就能恢复其状态。

备忘录模式的应用场景

  • 当你需要创建对象状态快照来恢复其之前的状态时,可以使用备忘录模式。
  • 当直接访问对象的成员变量、获取器或设置器将导致封装被突破时,可以使用该模式。

备忘录模式可以在不破环对象封装情况的前提下创建对象状态快照,并可以通过让负责人维护原发器状态历史记录来简化原发器代码。但是如果客户端过于频繁地创建备忘录,程序将消耗大量内存。负责人必须完整跟踪原发器的生命周期,这样才能销毁弃用的备忘录。

绝大部分动态编程语言(例如 PHP、Python 和 JavaScript)不能确保备忘录中的状态不被修改。

与其他模式的关系

详见上文与备忘录相关的内容。

观察者

观察者模式允许你定义一种订阅机制,可在对象事件发生时通知多个 “观察” 该对象的其他对象。

观察者模式包含了发布者、订阅者、具体订阅者、客户端。

  • 发布者 (Publisher) 会向其他对象发送值得关注的事件。事件会在发布者自身状态改变或执行特定行为后发生。发布者中包含一个允许新订阅者加入和当前订阅者离开列表的订阅构架。

    当新事件发生时,发送者会遍历订阅列表并调用每个订阅者对象的通知方法。该方法是在订阅者接口中声明的。

  • 订阅者 (Subscriber) 接口声明了通知接口。在绝大多数情况下,该接口仅包含一个 update 更新方法。该方法可以拥有多个参数,使发布者能在更新时传递事件的详细信息。

  • 具体订阅者 (Concrete Subscribers) 可以执行一些操作来回应发布者的通知。所有具体订阅者类都实现了同样的接口,因此发布者不需要与具体类相耦合。

    订阅者通常需要一些上下文信息来正确地处理更新。因此,发布者通常会将一些上下文数据作为通知方法的参数进行传递。发布者也可将自身作为参数进行传递,使订阅者直接获取所需的数据。

  • 客户端 (Client) 会分别创建发布者和订阅者对象,然后为订阅者注册发布者更新。

观察者模式的应用场景

  • 当一个对象状态的改变需要改变其他对象,或实际对象是事先未知的或动态变化的时,可使用观察者模式。
  • 当应用中的一些对象必须观察其他对象时,可使用该模式。但仅能在有限时间内或特定情况下使用。

观察者模式遵循开闭原则,无需修改发布者代码就能引入新的订阅者类(如果是发布者接口则可轻松引入发布者类)。可以在运行时建立对象之间的联系。但是订阅者的通知顺序是随机的。

与其他模式的关系

  • 责任链模式 命令模式 中介者模式 观察者模式 用于处理请求发送者和接收者之间的不同连接方式:
    • 责任链按照顺序将请求动态传递给一系列的潜在接收者,直至其中一名接收者对请求进行处理。
    • 命令在发送者和请求者之间建立单向连接。
    • 中介者清除了发送者和请求者之间的直接连接,强制它们通过一个中介对象进行间接沟通。
    • 观察者允许接收者动态地订阅或取消接收请求。

状态

状态模式让你能在一个对象的内部状态变化时改变其行为,使其看上去就像改变了自身所属的类一样。

状态模式包含了上下文、状态、具体状态。

  • 上下文 (Context) 保存了对于一个具体状态对象的引用,并会将所有与该状态相关的工作委派给它。上下文通过状态接口与状态对象交互,且会提供一个设置器用于传递新的状态对象。

  • 状态 (State) 接口会声明特定于状态的方法。这些方法应能被其他所有具体状态所理解,因为你不希望某些状态所拥有的方法永远不会被调用。

  • 具体状态 (Concrete States) 会自行实现特定于状态的方法。为了避免多个状态中包含相似代码,你可以提供一个封装有部分通用行为的中间抽象类。

    状态对象可存储对于上下文对象的反向引用。状态可以通过该引用从上下文处获取所需信息,并且能触发状态转移。

    上下文和具体状态都可以设置上下文的下个状态,并可通过替换连接到上下文的状态对象来完成实际的状态转换。

状态模式的应用场景

  • 如果对象需要根据自身当前状态进行不同行为,同时状态的数量非常多且与状态相关的代码会频繁变更的话,可使用状态模式。
  • 如果某个类需要根据成员变量的当前值改变自身行为,从而需要使用大量的条件语句时,可使用该模式。
  • 当相似状态和基于条件的状态机转换中存在许多重复代码时,可使用状态模式。

如果状态机只有很少的几个状态,或者很少发生改变,那么应用该模式可能会显得小题大作。

与其他模式的关系

  • 状态 可被视为策略 的扩展。两者都基于组合机制:它们都通过将部分工作委派给 “帮手” 对象来改变其在不同情景下的行为。策略使得这些对象相互之间完全独立,它们不知道其他对象的存在。但状态模式没有限制具体状态之间的依赖,且允许它们自行改变在不同情景下的状态。

策略

策略模式能让你定义一系列算法,并将每种算法分别放入独立的类中,以使算法的对象能够相互替换。

策略模式包含了上下文、策略、具体策略、客户端。

  • 上下文 (Context) 维护指向具体策略的引用,且仅通过策略接口与该对象进行交流。

  • 策略 (Strategy) 接口是所有具体策略的通用接口,它声明了一个上下文用于执行策略的方法。

  • 具体策略 (Concrete Strategies) 实现了上下文所用算法的各种不同变体。

    当上下文需要运行算法时,它会在其已连接的策略对象上调用执行方法。上下文不清楚其所涉及的策略类型与算法的执行方式。

  • 客户端 (Client) 会创建一个特定策略对象并将其传递给上下文。上下文则会提供一个设置器以便客户端在运行时替换相关联的策略。

策略模式的应用场景

  • 当你想使用对象中各种不同的算法变体,并希望能在运行时切换算法时,可使用策略模式。
  • 当你有许多仅在执行某些行为时略有不同的相似类时,可使用策略模式。
  • 如果算法在上下文的逻辑中不是特别重要,使用该模式能将类的业务逻辑与其算法实现细节隔离开来。
  • 当类中使用了复杂条件运算符以在同一算法的不同变体中切换时,可使用该模式。

策略模式可以在运行时切换对象内的算法,将算法的实现和使用算法的代码隔离开来。使用组合代替继承。策略模式遵循开闭原则,无需对上下文进行修改就能够引入新的策略。

但是如果你的算法极少发生改变,那么没有任何理由引入新的类和接口。使用该模式只会让程序过于复杂。客户端必须知晓策略间的不同 —— 它需要选择合适的策略。

许多现代编程语言支持函数类型功能,允许你在一组匿名函数中实现不同版本的算法。这样,你使用这些函数的方式就和使用策略对象时完全相同,无需借助额外的类和接口来保持代码简洁。

与其他模式的关系

  • 模板方法模式 基于继承机制:它允许你通过扩展子类中的部分内容来改变部分算法。策略 基于组合机制:你可以通过对相应行为提供不同的策略来改变对象的部分行为。模板方法在类层次上运作,因此它是静态的。策略在对象层次上运作,因此允许在运行时切换行为。

模板方法

模板方法模式在超类中定义了一个算法的框架,允许子类在不修改结构的情况下重写算法的特定步骤。

模板方法模式包含了抽象类、具体类。

  • 抽象类 (Abstract­Class) 会声明作为算法步骤的方法,以及依次调用它们的实际模板方法。算法步骤可以被声明为抽象类型,也可以提供一些默认实现。
  • 具体类 (Concrete­Class) 可以重写所有步骤,但不能重写模板方法自身。

模板方法的应用场景

  • 当你只希望客户端扩展某个特定算法步骤,而不是整个算法或其结构时,可使用模板方法模式。
  • 当多个类的算法除一些细微不同之外几乎完全一样时,你可使用该模式。但其后果就是,只要算法发生变化,你就可能需要修改所有的类。

模板方法模式可仅允许客户端重写一个大型算法中的特定部分,使得算法其他部分修改对其所造成的影响减小,将重复代码提取到一个超类中。但是部分客户端可能会受到算法框架的限制。通过子类抑制默认步骤实现可能会导致违反里氏替换原则。模板方法中的步骤越多,其维护工作就可能会越困难。

与其他模式的关系

详见上文与模板方法相关的内容。

访问者

访问者模式能将算法与其所作用的对象隔离开来。

访问者模式包含了访问者、具体访问者、元素、具体元素、客户端。

  • 访问者 (Visitor) 接口声明了一系列以对象结构的具体元素为参数的访问者方法。如果编程语言支持重载,这些方法的名称可以是相同的,但是其参数一定是不同的。
  • 具体访问者 (Concrete Visitor) 会为不同的具体元素类实现相同行为的几个不同版本。
  • 元素 (Element) 接口声明了一个方法来 “接收” 访问者。该方法必须有一个参数被声明为访问者接口类型。
  • 具体元素 (Concrete Element) 必须实现接收方法。该方法的目的是根据当前元素类将其调用重定向到相应访问者的方法。请注意,即使元素基类实现了该方法,所有子类都必须对其进行重写并调用访问者对象中的合适方法。
  • 客户端 (Client) 通常会作为集合或其他复杂对象(例如一个组合 树)的代表。客户端通常不知晓所有的具体元素类,因为它们会通过抽象接口与集合中的对象进行交互。

访问者模式的应用场景

  • 如果你需要对一个复杂对象结构(例如对象树)中的所有元素执行某些操作,可使用访问者模式。
  • 可使用访问者模式来清理辅助行为的业务逻辑。
  • 当某个行为仅在类层次结构中的一些类中有意义,而在其他类中没有意义时,可使用该模式。

访问者模式遵循开闭原则和单一职责原则。访问者对象可以在与各种对象交互时收集一些有用的信息。当你想要遍历一些复杂的对象结构(例如对象树),并在结构中的每个对象上应用访问者时,这些信息可能会有所帮助。

但是每次在元素层次结构中添加或移除一个类时,你都要更新所有的访问者。在访问者同某个元素进行交互时,它们可能没有访问元素私有成员变量和方法的必要权限。

与其他模式的关系

详见上文与访问者相关的内容。

在实际项目中应用设计模式

如果你还未开发过中大型项目,那么设计模式可能并不适合你直接学习。

设计模式的学习与应用依赖于大量的项目编码实践。如何在恰当的情况下使用合适的设计模式,需要大量的历史经验积累。实际上,在一个个项目规范开发的流程中,你可能已经无意识地使用到了某几种比较常用的设计模式。

设计模式本身就是一种在代码层面提出的思想,是为了更好地服务于代码的可维护性和稳健性,它并不是一种针对业务需求的指导思想。如果要在项目中强行套用设计模式,那么很可能会造成过度设计或矫枉过正。

如果你已经开发过了大大小小的项目,那么设计模式可能就是对于你过去有意或无意的编码思维进行了整体的总结。通过学习这些设计模式,你能对自己的编码思维有更清晰的认知,并利用设计模式去优化已有项目架构中的不足之处。

参考资料

本文主要内容参考自重构大师 refactoringguru

其他参考资料

什么是 SOLID 原则? | ExplainThis

design pattern | sourcemaking.com

设计模式 | 程序员进阶

  • 标题: 浅析设计模式
  • 作者: Entropy Tree
  • 创建于 : 2024-07-09 17:41:37
  • 更新于 : 2024-07-09 17:41:37
  • 链接: https://www.entropy-tree.top/2024/07/09/design-pattern/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论