[全程学习笔记]-软件设计模式

设计模式概述

设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结, 使用设计模式是为了可复用代码, 让代码更容易被他人理解、保证代码可靠性等.

设计模式四要素:

  • 模式名称
  • 问题
  • 解决方案
  • 效果

设计模式分类

根据目的分为

  • 创建型模式: 用于对象创建
  • 结构型模式: 用于处理类和对象的组合
  • 行为型模式: 用于描述类和对象的交互和职责

根据处理对象分为

  • 类模式: 处理类和子类的关系, 通常通过继承建立, 在编译时确定, 是静态的
  • 对象模式: 处理对象间的关系, 在运行时变化, 是动态的

类间关系

依赖(dependency)关系(虚线)

一个类A使用到了另一个类B, 但这个关系是偶然的, 临时的, 非常弱的, 类B的变化会影响到A

A偶然知道了B

在代码层面, 表现为A在方法中使用到了B类, 如局部变量, 方法中的参数, 对静态方法的调用等

// 依赖关系的例子, A依赖B
class A {
  // A在方法中调用了B
  public void func1(B b){
    ...
  }
  // A的方法中有B作为局部变量
  public void func2() {
    B b = new B();
    ...
  }
}
class B {...}

image-20241028091602221

自身依赖: 包含对自身类的局部变量, 方法参数等. 常见于链表

双向依赖: AB互相依赖, 形成依赖环. 这种依赖会形成强耦合.

关联(association)关系(实线)

两个类AB, A长期与B存在固定的对应关系. 比依赖更强的关系. 比如我和我的朋友, 公司和员工, 师傅和徒弟. 具有长期性和平等性.

在代码层面, 通常作为属性存在

// 关联关系的例子, A长期知道B(非偶然)
class A {
  private B b;
  public void func1() {
    ...
  }
}
class B {...}

关联关系也可以分为单向关联, 自身关联和双向关联

image-20241028093251503

聚合/聚集(aggregaion)关系(空心棱形)

当对象B被加入A中, 成为A的组成部分时, AB之间为聚合关系. 聚合关系是关联关系的一种特例.

聚合强调整体与部分, 拥有的关系. 整体与部分是可分离的, 可以有各自的生命周期. 部分可以属于多个整体对象.

例如: 自行车和车把, 车轮, 摇铃. 计算机和主板, CPU, 内存, 硬盘. 课题组和科研人员.

在代码层面, 聚合关系和关联关系是一致的. 但在逻辑层面, 关联关系是相同层次, 聚合是整体——部分层次.

// 聚合关系的例子
// 聚合关系在代码上和关联关系一致
class A{
  private B b; 
}
class B{...}

image-20241028184120722

组合/合成(composition)关系(实心棱形)

组合也是关联关系的一种特例, 体现一种比聚合关系更强的包含关系, 又称强聚合.

此时, 整体与部分是不可分的, 整体负责部分的生存与消亡.

// 组合关系的例子, A控制B的生命周期
class A {
  private final B b;
  public A() {
    this.b = new B();
  }
}
// B的生命周期完全由A控制
class B {...}

image-20241028184638212

泛化(generalization)关系(空心三角实线)

泛化指一般与特殊, 一般与具体之间的关系.

比如狗是对动物的具体描述.

在代码层面, 泛化是通过继承实现的.

// 泛化的例子
class B {...}
class A extends B {
  ...
}

image-20241028185013016

实现(realization)关系(空心三角虚线)

实现是类与接口的关系, 表示类是接口所有特征和行为的实现.

泛化和实现的区别在于子类是否继承了父类的实现, 如果继承了则为泛化, 反之为实现.

实现在代码上主要体现在接口和实现的关键字.

// 实现的例子
interface A {
    // 不能修改, 隐式是 public static final 
    int value = 1; 
    // 只有方法声明, 没有实现用来继承
    void doSomething();  
}
class B implements A {
    @Override
    public void doSomething() {
        // 必须提供实现
    }
}

image-20241028185208134

上述几种关系的强度依次为:

泛化/实现 > 组合 > 聚合 > 关联 > 依赖

面向对象设计原则

面向对象的七大原则不是互相孤立的, 彼此间互相关联.

大体上可以做出这样的分类:

  • 设计目标: 开闭原则, 里氏替换原则, 迪米特原则
  • 设计方法: 单一职责原则, 接口分离原则, 依赖倒置原则, 组合/聚合复用原则

开闭原则

Open-Closed Principle, OCP

软件实体(模块, 类, 方法等)应对扩展开放, 对修改关闭.

实现方法:

  • 将不变的部分抽象成不变的接口, 用接口应对未来的扩展
  • 接口的最小功能设计原则: 原有的接口要不可以应对未来的扩展, 要不可以通过定义新的接口来实现
  • 模块之间调用通过接口进行

接口可以复用, 但接口的实现不一定能被复用. 接口是稳定的, 关闭的, 但接口的实现是可变的, 开放的. 可以通过对接口的不同实现和类的继承为系统增加新的或改变原有的功能, 实现软件系统的柔性扩展.

系统是否有良好的接口(抽象)设计是判断软件系统是否满足开闭原则的重要标准. 现在多把开闭原则等同于面向接口的软件设计.

image-20241028202657337

  • Client对于Server提供的接口是修改关闭的

    Client依赖的AbsServer接口一旦定义就不会轻易修改

  • Client对于Server的新接口是扩展开放的

可以创建新的类来实现AbsServer接口

以上两点说明Client的稳定性高, 不因Server实现的变化而改变, 体现了面向接口编程的原则, 降低了耦合度

开闭原则有相对性, 软件系统的构建会经历不断重构,100%满足开闭原则是非常困难的. 但在设计过程中, 可以尽量接近满足.

里氏替换原则

Liskov Substitution Principle, LSP

所有引用基类的地方必须能透明的使用其派生类的对象

  • 不应该在代码中用if-else判断派生类类型
  • 派生类应当可以替换所有基类对象出现的地方还能正常工作

一个违反了LSP的例子:

image-20241028205119439

如果业务代码中有

void g(Rectangle& r) {
  r.setHeight(5);
  r.setWidth(4);
}

则对于扩展类Square, 函数g的执行是不符合预期的. 这违反了LSP.

要改正这个设计, 可以考虑两种解决方案:

  • 创建新的抽象类C, 作为两个具体类的基类, 将具体类的共同行为移动到C中
  • 放弃继承, 改为关联

比如本例中, 可以构造一个抽象的四边形类.

一个一般性的指导原则是, 尽量从抽象类继承. 在系统的继承树中, 叶子结点最好是具体类, 树枝节点最好是抽象类和接口.

在进行设计的时候, 尽量从抽象类继承, 而不从具体类继承.

迪米特原则

最少知道原则, Law of Demeter, LoD

  • 一个软件的实体应该尽可能的少与其他实体相互作用(只和“朋友”通信)
  • 一个软件实体对其他实体的知识越少越好

上述原则中的朋友可以是

  • 对象本身(this)
  • 以参数形式传入的对象(依赖)
  • 直接引用的对象(关联)
  • 聚集中的元素
  • 当前对象创建的对象(组合)

需要注意:

  • 朋友也是有距离的, 类要尽量羞涩, 多使用private和protected

单一职责原则

Single Responsibility, SRP

永远不要让一个类存在多个改变的理由, 即, 如果要改变一个类, 改变的理由只能有一个.

SRP提倡一个类的属性和方法应该一起修改, 或者一起保持不变, 不应该存在这样的情况: 提出一个需求, 只修改一部分代码的内容.

来看一个好的设计实例, 某个订单类的实现:

// 好的SRP设计示例
class Order {
    private String orderId;
    private List<OrderItem> items;
    private double totalAmount;

    public void addItem(OrderItem item) {
        ...
    }

    public void removeItem(OrderItem item) {
        ..
    }

    private void recalculateTotal() {
        ...
    }
}

这个设计是好的, 因为

  • 这些方法和属性服务于同一个目的: 管理订单的基本信息和条目
  • 他们通常因同一个原因改变(比如订单业务的规则变化)

一个违反SRP的例子

class Order {
    private String orderId;
    private List<OrderItem> items;
    private double totalAmount;

    // 订单核心功能
    public void addItem(OrderItem item) { ... }
    public void removeItem(OrderItem item) { ... }

    // 违反SRP的部分:订单持久化
    public void saveToDatabase() {
        // 使用特定数据库API保存订单
    }

    // 违反SRP的部分:订单展示
    public String generateOrderHTML() {
        // 生成订单的HTML表示
    }
}

在上例中, 数据库的改变, 网页的改变都会修改类的部分功能, 这违反了SRP.

接口分离原则

Interface Segregation Principle, SRP

  • 接口的设计应该遵循最小原则
  • 一个接口a继承自b, 如果a继承来的方法中有不需要的部分, 则称接口a被b污染了, 应该重新设计

image-20241029183618928

CommonDoor不得不实现一个用不到的alarm方法, 这违反ISP.

单一职责原则和接口隔离原则的共同点

  • 都是为了提高内聚性, 降低耦合性, 体现封装思想
  • 表现出来都是约束接口到最小

依赖倒置原则

Dependency Inversion Principle, DIP

  • 高层模块不应该依赖底层模块, 二者都应该依赖于抽象

  • 抽象不应该依赖细节, 细节应该依赖抽象

这样的设计是错误的, 对底层的修改会影响上层

image-20241029185037987

应该在高层模块与低层模块之间引入一个抽象接口层

image-20241029185112402

启发1: 依赖于抽象

  • 任何对象都不应该持有一个具体类的指针或者引用

  • 任何类都不应该从具体类派生

启发2: 设计接口, 而非设计实现

  • 使用继承避免对具体类的直接绑定

启发3: 避免传递依赖

  • 避免高层依赖底层
  • 使用抽象类/接口来消除传递依赖

组合/聚合复用原则

Composite/Aggregate Reuse Principle, CARP, CRP

尽量使用组合/聚合而非继承来达到复用的目的

使用继承的前提:

  • 派生类是基类的一个特殊种类, 而不是一个角色, 即, 不是has a, 而是is a.
  • 永远不会出现派生类之间的类型转换. 如果不确定是否需要转换, 就不要继承.
  • 派生类是基类的扩展, 而不是替换和注销. 如果派生类需要大量替换, 则不应该设计.
  • 只有分类学角度上有意义时, 才使用继承.

image-20241112230446477
image-20241114222647000
image-20241114222827760

创建型模式

创建型模式关注的是对象的创建, 类的实例化进行抽象和封装, 分离了对象创建和对象使用.

  • 客户不知道对象的具体类是什么, 除非看源代码
  • 隐藏了对象实例是如何被创建和组织的
  • 需要new运算符的时候, 就可以考虑创建型模式

简单工厂模式

Simple Factory Pattern, 静态工厂方法模式, Static Factory Pattern

动机与定义

考虑一个简单的场景: 有一个按钮基类, 继承出多种不同形状的按钮, 比如圆形, 矩形等. 子类在继承之后会修改部分属性,

我们希望在使用按钮时, 不需要知道具体按钮类的名字, 只需要知道一个参数, 并提供一个调用方便的方法, 将这一个参数传入方法, 即可返回一个相应的按钮对象.

在简单工厂模式中, 可以根据参数的不同返回不同类的实例. 即, 专门定义一个类, 用以创建其他类的实例, 所有被创建的实例通常有共同的父亲.

image-20241030170029216

模式结构

  • Factory: 工厂角色, 负责实现创建所有实例的内部逻辑
  • Product: 抽象产品角色, 是所有产品的父类
  • ConcreteProduct: 具体的创建目标

简单工厂模式的要点在于: 当你需要什么, 就传入一个参数来获取想要的对象, 你无需知道其他细节.

模式分析

优点

  • 分离对象的创建和主要业务处理降低了耦合度
  • 工厂方法是静态方法, 使用很方便

缺点

  • 最大的问题是工厂类的职责相对过重
  • 增加新的产品需要修改判断逻辑, 这违背开闭原则
  • 增加了系统中类的个数
  • 静态工厂方法导致工厂角色无法形成基于继承的等级结构

适用范围

  • 工厂类负责创建的对象较少
  • 客户端只知道传入工厂类的参数, 不关心如何创建对象

模式扩展

简单工厂模式的简化

在有些情况下, 工厂类可以由抽象产品角色扮演. 即, 把静态工厂方法写进抽象产品中.

image-20241113223810239


工厂方法模式

Factory Method Pattern, 虚拟构造器模式, Virtual Constructor, 多态工厂模式, Polymorphic Factory, 工厂模式, Factory Pattern

在简单工厂模式中, 只提供了一个工厂类, 这个类知道每一个产品对象的创建细节, 并决定何时实例化.

简单工厂模式最大的缺点是当前有新产品加入系统中时, 必须修改工厂类, 加入必要的处理逻辑, 这违背了开闭原则.

模式动机与定义

image-20241101141339371

为了解决上面的问题, 对该系统进行修改:

不再使用统一的按钮工厂来负责产品的创建, 而是将具体按钮的创建过程交给专门的工厂子类取完成.

我们定义一个抽象的按钮工厂类, 再定义具体的工厂类类完成圆形, 矩形, 菱形按钮的创建. 这些具体的类实现了抽象工厂类中定义的方法, 从而使得这种结构可以在不修改具体工厂类的情况下引进新的产品. 这更加符合开闭原则.

image-20241101142338814

在工厂方法模式中, 工厂父类负责定义创建产品对象的公共接口, 而工厂子类负责生产具体的产品对象.

模式结构

image-20241101143304527

  • Product: 抽象产品, 定义产品的接口, 工厂方法模式所创建对象的超类型
  • ConcreteProduct: 具体产品, 实现了抽象产品的接口, 由专门的具体工厂构建
  • Factory: 抽象工厂, 声明了工厂方法, 返回一个产品, 与具体应用无关
  • ConcreteFactory: 具体工厂, 实现了抽象工厂的方法, 返回具体产品实例

模式分析

工厂方法模式是简单工厂模式的推广, 保留了优点, 克服了缺点.

核心的工厂类不再负责具体产品的创建, 而是通过继承交给子类去做. 这使得工厂方法模式可以再不修改工厂角色的情况下引进新产品. 这符合开闭原则.

优点

  • 工厂方法对客户透明, 客户无需关心细节, 甚至无需知道类名
  • 系统加入新产品时, 无需修改代码, 完全符合开闭原则

缺点

  • 在添加新产品时, 编写新的具体类和具体工厂类, 增加了系统复杂度
  • 引入的抽象层增加了理解难度, 可能还需用到DOM, 反射等技术, 增加了实现难度

适用环境

  • 一个类不知道他所需要的对象类(名)
  • 一个类通过子类指定创建哪个对象
  • 将创建对象的任务委托给多个工厂子类中的某一个

工厂方法的本质: 延迟到子类来选择实现.

模式扩展

DOM和反射

DOM是一个与平台和语言无关的接口,允许程序动态访问和更新文档的内容、结构和样式

// 从XML配置中读取类名
String className = doc.getElementsByTagName("class").item(0).getTextContent();

反射是Java的特性, 允许在运行时检查和操作类、接口、方法和字段

String className = "Dog";
// clazz是根据字符串生成的一个类的设计图
// 本质上是JVM中, Dog类的运行时表示
Class<?> clazz = Class.forName(className); 
// 相当于new Dog();
return clazz.newInstance();

使用多个工厂方法

在抽象工厂角色中可以定义多个工厂方法, 从而使具体工厂实现. 这些工厂方法包含不同业务逻辑以满足不同需求

产品对象的重复使用

工厂对象可以将创建过的产品保存起来, 在客户请求来到时, 在集合中先搜索, 如果有满足要求的, 就直接返回.

多态性丧失和模式退化

如果工厂等级结构只有一个具体工厂类, 则可以省略抽象工厂.

当唯一的具体工厂可以创建所有对象, 且工厂方法是静态时, 工厂方法模式退化为简单工厂模式.


抽象工厂模式

Abstract Factory Pattern, Kit模式

动机与定义

在工厂方法模式中, 每个具体工厂对应一种具体的产品. 但是有时候我们需要一个工厂可以提供多个产品对象, 而不是单一的.

抽象工厂提供一个创建一系列相关对象的接口, 而无需指定具体的类.

模式结构与

这里引入两个概念:

  • 产品等级结构: 即产品的继承结构. 比如: 父类电视机, 继承出海尔电视机, TCL电视机, 海信电视机等等

  • 产品族: 在抽象工厂模式中, 产品族是指同一个工厂生产的, 位于不同产品等级结构的一组产品. 比如海尔电器工厂生产的海尔电冰箱, 海尔电视机等等

image-20241103223406159
image-20241103223848101

5191c5e6f923bf528918d38c9e81c96d.png

image-20241103225045552

  • AbstractFactory: 抽象工厂, 在一个抽象工厂中可以定义一组方法, 每一个方法对应一个产品顶级结构

  • ConcreteFactory: 具体工厂, 实现了抽象工厂声明的方法, 生成一组具体产品, 这些产品构成一个产品族

  • AbstractProduct: 抽象产品为每种产品声明接口, 定义共同业务方法

  • Product: 具体产品, 实现业务方法

模式分析

模式优点

  • 抽象工厂模式隔离了具体类的生成, 进一步降低了耦合度
  • 抽象工厂模式在产品族一侧是符合开闭原则的: 每增加一个产品族, 只需要实现抽象工厂类即可.

模式缺点

  • 在产品等级结构一侧, 即, 添加新的产品对象时, 难以抽象工厂, 需要改动所有类, 这一点不符合该闭原则, 被称作开闭原则的倾斜性

模式适用环境

  • 一个系统有多个产品族, 且同族的产品将在一起使用(比如组装电脑的例子, 华硕全家桶经常一起用)
  • 举例: 软件系统更换界面主题, 即
    • [主题A, 主题B, 主题C]\: 产品族
    • [按钮1, 按钮2, 按钮3]\: 产品等级结构

模式扩展

退化

当抽象工厂模式中的每一个具体工厂都只创建一个产品对象时, 即, 产品等级结构为一时, 抽象工厂模式退化为工厂方法模式;

当工厂方法模式中的抽象工厂与具体工厂合并, 即, 用一统一的工厂创建产品对象, 且创建方法为静态时, 工厂方法模式退化为简单工厂模式

抽象工厂模式的本质是选择产品簇的实现

  • 在工厂方法模式中, 虽然一个类可以有多个工厂方法, 但是这些方法一般没有联系(即使看起来有联系);
  • 在抽象工厂模式中, 着重产品簇的选择实现. 每一个产品簇内的产品经常一起出现;

建造者模式

定义和动机

在现实和软件系统中, 存在一些复杂的对象, 包含多个组成部分. 比如汽车, 包含车轮, 方向盘, 发动机等各种部件. 对于大部分用户, 无须知道装配细节, 也不会单独使用某个部件.

image-20241105155725541

在软件开发中, 有很多类似的复杂对象. 他们拥有一系列成员属性, 有些是引用类型. 这些属性可能有相互的制约条件, 比如完成性约束.

建造者返回客户端的是一个已经建造完毕的完成产品, 而用户无须关心对象所包含的属性和组装方式, 这就是建造者模式的动机.

建造者模式可以将部件和组装过程分开, 使得同样的构建过程可以有不同的表示.

建造者模式一步步创建一个复杂对象.

模式结构

image-20241105162943839

  • Builder: 抽象建造者, 为创建一个产品对象的各部件指定抽象接口
  • ContreteBuilder: 具体建造者, 实现上面的接口, 包括各个部件的构造和装配方法, 定义并明确复杂对象, 也可以提供一个方法返回创建好的复杂产品对象
  • Director: 指挥者负责安排复杂对象的建造次序. 也为了隔离客户与生产过程.
  • Product: 被构建的复杂对象

模式分析

模式优点

  • 客户不必清楚产品的内部组成细节, 降低了耦合度
  • 客户可以使用相同的创建过程可以创建不同的产品对象
  • 具体建造者之间相对独立
  • 可以精细的控制产品的创建过程
  • 增加新的具体建造者无需修改已有代码, 符合开闭原则

模式缺点

  • 建造者模式适合建造有相同点的产品, 如果差异过大则不合适
  • 如果产品内部变化复杂, 可能需要定义很多具体建造者

适用情况

  • 产品对象有复杂的内部结构
  • 产品对象有属性的相互依赖, 并且需要指定生成顺序
  • 对象的创建过程独立于创建该对象的类, 创建过程封装封装在指挥者, 而不是建造者.
  • 隔离复杂对象的创建和使用
  • 需要相同的创建过程可以创建不同的对象

建造者模式 vs 抽象工厂模式

  • 建造者模式返回一个组装好的产品, 而抽象工厂注重返回一系列的产品族, 产品等级结构
  • 建造者模式中, 客户端可以不直接调用建造者的方法, 而是通过指挥者, 注重一步一步的组装. 抽象工厂模式中, 客服实力化工厂类, 然后直接获取.
  • 抽象工厂模式: 汽车配件生产工厂, 生产一个产品族
  • 建造者模式: 汽车组装工厂, 注重组装, 返回一个完整汽车

建造者模式的本质是分离整体构建算法和部件构造

构造复杂对象有构建过程, 构造过程中又有具体的实现. 建造者模式用来分离这两部分. 这降低了耦合度, 让程序结构松散, 扩展容易, 复用性好.

模式扩展

建造者模式的简化

  • 省略抽象建造者角色: 如果系统中只需要一个具体建造者
  • 省略指挥者: 如果只有一个具体建造者, 且抽象建造者也被省略掉
  • 合并指挥者和抽象建造者: 简化结构, 不符合单一职责原则

原型模式

动机和定义

原型模式通过给出一个原型来指明要创建的对象的类型, 然后复制这个原型来创建更多同类型的对象. 这样操作更方便, 更节省资源.

对比拷贝构造

  • 原型对象在创建时, 不用关心这个对象本身的类型
  • 原型模式有助于符合里氏替换原则, 比如水果篮里放入水果副本

模式结构

image-20241105181945292

  • Prototype: 抽象原型类, 定义克隆自己的方法的接口
  • ConcretePrototype: 具体原型类, 实现具体的克隆方法, 返回一个自己的克隆对象
  • Client: 客户类, 只需要直接调用克隆方法来复制

模式分析

一个类包含一些成员对象, 在使用原型模式克隆对象时, 根据成员对象是否克隆, 可以分为深浅克隆

优点

  • 简化对象创建过程和效率
  • 扩展性较好, 客户端针对抽象原型类编程, 增加减少产品类对原系统没有影响
  • 优化了创建结构, 省略工厂类

缺点

  • 每一个类都需配备一个克隆方法
  • 多层深克隆比较复杂

模式适用环境

  • 创建新对象的成本较大
  • 系统要保存对象的状态
  • 规避工厂类层次

原型模式的本质是克隆生成对象

克隆是手段, 生成新对象是目的.

模式扩展

带原型管理器的原型模式

原型管理器是将多个原型对象存储在一个集合中供客户使用, 他是一个专门负责克隆对象的工厂. 如果需要某个对象的克隆, 他来通过复制集合中的原型对象来获得.

image-20241113234949151

相似对象的复制

很多时候, 希望得到的对象与原型并不是完全相同的. 可以通过原型模式先复制, 再修改.


单例模式

动机与定义

对于系统中的某些类来说, 只有一个实例很重要. 例如一个系统中只有一个文件系统, 一个任务管理器.

我们需要设计一种类, 让类自身负责保存他的唯一实例, 保证没有其他实例创建, 并提供访问方法.

单例模式的要点

  • 只有一个实例
  • 自行创建这个实例
  • 自行向系统提供这个实例

模式结构

image-20241105220338984

模式分析

  • 单例类的构造函数是私有的, 无法被new实例化
  • 单例类有一个静态的成员变量和一个静态的, 公有的工厂方法. 这个工厂方法负责检验实例存在性, 并实例化自己出存在静态成员变量中

模式优点

  • 提供对唯一实例的受控访问
  • 节约系统资源
  • 允许可变数目的实例

缺点

  • 单例模式中没有抽象, 这使得扩展困难
  • 单例类的职责过重, 违背单一职责原则
  • 滥用会带来负面问题

单例模式的本质: 控制实例数量

模式扩展

饿汉式单例: 立即实例化

image-20241105221338986

懒汉式单例: 访问时实例化

image-20241105221227541


结构型模式

Structural Pattern

  • 类结构模式: 关心多个类的组合关系, 一般只存在继承和实现
  • 对象结构模式: 关心类与对象的组合

适配器模式

Adapter Pattern, 包装器模式, Wrapper Pattern

image-20241112232655282

模式动机

  • 通常情况下, 客户端可以通过目标类的接口访问服务. 有时候虽然接口可以满足功能, 但不是客户所直接期望的.
  • 现有的接口需要转化为客户类期望的接口

定义一个包装类, 包装不兼容接口的对象, 对外提供客户类需要的接口, 对内对接服务类的接口.

客户类不直接访问适配者类.

模式结构

类适配器

image-20241105223206422

对象适配器

image-20241105223534875

  • Target: 用户所需的接口, 可以是抽象类, 接口, 也可以是具体类
  • Adapter: 适配器类, 对Adaptee和Target适配. 在对象适配器中, 他继承Target并关联一个Adaptee来产生关系.

模式分析

优点

  • 将目标类和适配者类解耦
  • 增加了类的透明性和复用性
  • 灵活性和扩展性非常好, 符合开闭原则

类适配器的优点

  • 由于适配器类是适配者的子类, 因此可以置换一些适配者的方法, 使得灵活性更强

类适配器的缺点

  • Java, C#等不支持多重继承等语言一次只能适配一个适配者类
  • 目标抽象类只能抽象, 不能具体, 使用有一定局限性
  • 不能将一个适配者类和他的子类都适配到目标接口

对象适配器的优点

  • 同一个适配器可以把适配者类和他的子类全部适配到目标接口

对象适配器的缺点

  • 想要置换适配者类的方法不容易

适配器的本质: 转换匹配, 复用功能

适用环境

  • 系统需要现有的类, 但是类的接口不符合要求
  • 想建立一个可重复使用的类(适配器类), 用于联系彼此之间没有太大关联的类(目标类和适配者类)
  • 为将来可能引进的类(适配者类的子类)一起工作

模式扩展

默认适配器模式(缺省适配器模式, 接口适配器模式, 单接口适配器模式)

当不需要全部实现某个接口提供的方法时, 可以先设计一个抽象类实现接口, 并为每个方法提供一个空的默认实现.

此时, 该抽象类的子类可以有选择的覆盖父亲的某些方法来实现需求.

他适用于一个接口不想使用其他所有方法的情况.

image-20241115155525624

双向适配器

目标类和适配者类需要相互引用

image-20241105225043083

智能适配器

在转换匹配过程中, 实现一些功能处理, 进一步的, 还可以按需复用不同适配者

适配多个适配者

在转换匹配过程中, 可以适配多个适配者, 即, 在实现目标接口时, 需要调用多个类的功能.


桥接模式

Bridge Pattern, 柄体模式, Handle & Body Pattern, 接口模式, Interface Pattern

动机与定义

考虑这样的一个实例:

我们有大中小3种型号的蜡笔, 每种大小的画笔都需要绘制12种不同的颜色, 总共36支.

如果使用毛笔, 只需要3种型号, 外加12个颜料盒即可. 也就是说, 如果增加一种新粗细, 蜡笔需要增加12支, 而毛笔只需要增加一支.

在蜡笔中, 颜色和型号两个不同的变化维度融合在一起, 无论是改变谁都会影响另一方.

在毛笔中, 这两个维度被分离了, 增加新的颜色或者型号对另一方没有影响.

桥接模式被用作处理类似的, 具有变化维度的情况. 桥接模式将抽象部分与实现部分分离, 使他们可以独立的变化. 他是对象结构型模式.

与多重继承方案不同, 桥接模式将两个独立变化的维度设计为两个独立的继承等级结构, 且在抽象层建立关联, 类似一条连接两个独立继承结构的桥, 易于扩展, 控制了类的个数.

模式结构

image-20241107224911718

  • Abstraction: 抽象类, 一般不是接口, 定义抽象类的接口. 启动定义了一个Implementor(实现类接口)类型的对象, 并可以维护该对象. 他与Implementor具有关联关系, 既可以包含抽象业务方法, 也可以包含具体业务方法.
  • RefinedAbstracion: 扩充抽象类, 扩充Abstraction定义的接口, 通常情况下是具体类. 他实现了Abstraction中的抽象, 也可以调用Implementor中定义的业务方法.
  • Implementor: 实现类接口, 这个接口不一定要与Abstration的接口完全一致. 通过关联关系, Abstraction中不仅拥有自己的方法, 还可以调用Implementor的方法
  • ConcreteImplementor: 具体实现类, 实现Implementor接口, 提供具体业务操作方法

在桥接模式中, 抽象类通常定义"是什么", 比如图形, 设备等. 而接口定义"怎么做", 比如绘制, 工作等.

模式分析

优点

  • 桥接模式使用了对象间的关联关系解耦了抽象和实现的绑定, 使得抽象和实现可以沿着个字的维度来变化.
  • 极大的减少了子类的个数, 比继承方案更好, 符合“单一职责原则”
  • 两个维度变化中任意扩展, 都不需要修改原有系统, 符合“开闭原则”

缺点

  • 增加了系统的理解与设计难度
  • 识别两个独立维度需要一定经验

桥接模式的本质: 分离抽象与实现

适用环境

  • 一个系统需要在抽象话和具体化之间增加更多灵活性, 避免在两个层次间简历静态继承关系

  • 一个类需要两个(或多个)变化维度, 并且每个维度独立扩展

  • 不希望适用继承, 或者因为继承导致系统类个数急剧增加

模式扩展

适配器模式可以和桥接模式连用

桥接模式用于初步设计, 而发现初步设计和已有类无法协同工作时, 采用适配器模式.

有时涉及的接口复杂, 设计初期也需要考虑适配器模式.

image-20241108002800953


组合模式

Composite Pattern, 整体-部分模式, Part-Whole Pattern

动机与定义

树形结构在软件中随处可见, 组合模式就是要用面向对象的方法来处理树形结构

树形结构中, 当容器对象的某一个方法被调用时, 将遍历整个树形结构, 递归调用方法. 由于容器对象和叶子对象在功能上有区别, 代码中必须区别对待, 这会让程序非常复杂

组合模式可以让客户端一致的处理整个树形结构, 也可以一致的处理叶子结点和容器节点.

模式结构

image-20241115163758749

  • Componet: 抽象组件, 接口或者抽象类, 为叶子组件和容器组件声明接口, 可以包含所有子类共有行为的声明和实现. 定义访问/管理子组件的方法, 如增删查改
  • Leaf: 叶子组件, 表示叶子结点对象, 没有子节点. 他实现了Component中定义的行为, 对于管理子组件的方法, 可以用异常等方法处理
  • Composite: 容器组件, 表示内节点对象, 他的子节点可以是容器, 也可以是叶子
  • Client: 客户类, 针对Component组件编程

模式分析

组合模式的关键是定义一个抽象组件类, 既可以代表叶子也可以代表容器. 对于客户端, 需要针对抽象组件类编程, 无需知道他表示的是叶子还是容器.

容器对象和抽象组件建立聚合关联关系, 在容器对象中可以包含叶子和容器, 以此递归组合, 形成树形结构.

需要注意的是, 在实现业务方法时候, 容器组件应该递归的调用孩子组件的业务方法

优点

  • 定义了包含基本对象和组合对象的类层次结构
  • 统一了组合对象和叶子对象
  • 简化了客户端调用
  • 容易扩展

缺点

  • 很难限制组件类型, 比如很难限制容器中只能包含特定类型的组件(只能是树枝, 只能是叶子等等)

适用环境

  • 想表示整体-部分的层次结构, 并且统一起来

组合模式的本质: 统一叶子对象和组合对象

模式扩展

透明组合模式

组合模式的标准形式, 即在抽象组件Component中声明了所有用于管理成员对象的方法.

缺点是不够安全

image-20241108161655720

安全组合模式

在Component中不声明管理孩子对象的方法, 而是在Composite类中做.

这种做法是安全的, 但是不够透明, 客户端无法针对抽象编程, 必须区别对待叶子和容器组件

image-20241108162454210

更复杂的组合模式, 可以对叶子和容器节点进行抽象

image-20241114203849597


装饰模式

Decorator Pattern, 油漆工模式

动机与定义

在软件设计中, 有两种方式可以实现给一个类或对象增加行为/功能

  • 继承: 静态的, 不能控制增加的方式和时机
  • 关联: 将一个对象嵌入另一个, 来扩展行为, 这种对象叫装饰器

装饰模式动态的给一个对象增加一些额外指责. 在装饰模式中, 为了让系统有更好的灵活和扩展性, 通常定义一个抽象的装饰类, 并引入另一个抽象组件类.

装饰模式通过无需定义子类的方式扩充功能, 符合合成复用原则

模式结构

image-20241108165116368

  • Component: 抽象组件, 是具体组件和抽象装饰类的共同父亲, 声明在具体组件中实现的业务方法.
  • ConcreteComponent: 具体组件, Component类的子类, 实现了业务方法, 装饰器为他增加额外的指责(方法)
  • Decorator: 抽象装饰类, Component类的子类, 用以增加职责. 他维护一个指向抽象组件对象的引用, 调用装饰之前组件对象的方法, 达到装饰的目的
  • ConcreteDecorator: 具体装饰类, 负责添加新的职责.

需要注意的是在Decorator中并未真正实现operation()方法, 而只是调用原有component对象的operation()方法.

Decorator没有真正实施装饰. 而是提供一个统一的接口, 将具体装饰过程交给子类完成

模式分析

具体组件类和装饰类实现了相同的抽象组件接口, 所以装饰模式以对客户透明的方式动态的给一个对象附加更多的责任.

客户端并不会发现对象在装饰前后有什么不同. 装饰模式在不需要创造更多子类的情况下, 将对象的功能加以扩展.

优点

  • 从为对象添加新功能来看, 装饰模式比继承更灵活
  • 简化高层定义, 更容易复用功能

缺点

  • 每个装饰器实现一个功能, 产生了很多多粒度的对象
  • 比继承更容易出错, 排错也很困难, 多次装饰的对象需要逐级排查

装饰模式的本质: 动态组合

模式扩展

  • 透明装饰模式: 要求客户端完全针对抽象编程(Component类), 不得声明具体组件和具体装饰类型.
  • 半透明装饰模式(多数情况): 运行用户在客户端声明具体组件和装饰对象, 调用新增的方法(addedBehavior())

需要注意

  • 尽可能的保持装饰类的接口与被装饰类的接口相同, 这样对于客户端而言, 可以一致对待. 也就是说, 尽可能的使用透明装饰模式
  • 尽可能的保持具体组件类的简洁
  • 如果只有一个具体组件类, 则装饰类可以作为该组件类的直接子类

简化的装饰模式

image-20241108174759125


外观模式

Facade Pattern, 门面模式

动机与定义

考虑一个客户买电脑的例子: 某客户需要去电脑城选配一套电脑,

  • 方案A: 自己亲自去每家器材商选购每种配件
  • 方案B: 找一家专业装机的公司, 由公司负责选配

显然B方便一些, 从抽象层面来说, 公司隐蔽了电脑城内部的复杂性.

外观模式为子系统的一组接口提供一个统一的入口. 他定义了一个高层接口, 这个接口使得这些子系统更加容易使用.

外观模式中, 一个子系统的外部与内部的通信通过统一的外观类进行.

外观模式是迪米特原则的具体实现, 通过引入新的外观角色降低系统复杂度和耦合度.

模式结构

image-20241108212922906
image-20241108213025391

  • Facade: 外观角色, 客户可以调用他的方法. 他将客户发来的请求委派到相应的子系统, 传递给相应的子系统处理.

  • SubSystem: 子系统角色, 可以不是单独的类, 而是类的集合. 子系统不知道外观的存在.

模式分析

和适配器模式的主要区别

  • 外观模式: 注重隐藏复杂性
  • 适配器模式: 注重转换现有类的接口来满足目标接口的要求

为了节约资源, 外观类经常被设计为单例

外观模式的本质是封装交互, 简化调用

优点

  • 对客户端屏蔽了子系统组件, 减少了客户端所需处理的对象数
  • 实现了客户与子系统的松耦合关系
  • 只是提供了统一入口, 不影响客户单独访问子系统

缺点

  • 设计不当会造成增加新的子系统需要修改外观类的源代码, 这违反开闭原则
  • 不能限制客户端直接访问子系统类, 如果做出限制反而减少了可变性

适用环境

  • 为一整个复杂子系统提供一个简单接口, 这个接口适合大部分人, 用户也可以选择绕过
  • 客户端与多个子系统存在很大依赖, 此时引入外观类可以给客户端和子系统解藕
  • 在层次化结构中, 可以用外观模式定义每一层的入口

模式扩展

  • 特殊情况下, 一个系统可以有多个外观类
  • 不要试图通过外观类为子系统增加新的行为
  • 特殊情况下, 可以使用抽象外观类

image-20241108214823814


代理模式

Proxy Pattern, Surrogate Pattern

动机与定义

由于某些原因, 客户端不想或不能直接访问一个对象, 此时可以通过代理模式的第三者进行访问

模式结构

image-20241108221044832

  • Subject: 抽象主题角色, 声明真实主题和代理主题的共同接口, 这样以来, 任何使用真正主题的地方都可以使用代理主题. 客户端针对抽象主题进行编程. 抽象主题可以是接口, 抽象类或具体类
  • RealSubject: 真实主题角色, 定义代理角色所代表的真实对象.
  • Proxy: 代理主题角色, 他包含对真实主题的引用. 此外, 可以在必要时负责创建/删除真实主题. 在代理模式中, 客户在调用真实主题操作之后, 代理还有其他操作要做.

模式分析

在实际开发中, 代理类的实现很复杂. 根据目的和实现方式可以分为

  • 远程代理: 为位于不同地址空间的远程对象提供一个本地的代理
  • 虚拟代理: 如果需要创建一个资源稍大的对象, 可以先创建一个资源较小的对象来表示, 真实对象在需要时才被创建
  • 保护代理: 控制一个对象的访问, 为不同的用户提供不同级别的使用权限
  • 缓冲代理: 提供Cache
  • 智能引用代理: 提供额外操作, 比如将调用次数记录下来等
  • 防火墙代理
  • 同步代理: 为多线程提供安全访问
  • 写入复制代理: 延迟复制操作直到粘贴

代理模式的本质: 控制对象访问

优点

  • 远程代理为不同地址空间对象的访问提供了本地实现
  • 虚拟代理用临时对象代替, 节省运行开销
  • 缓冲代理优化系统性能
  • 保护代理提供安全

缺点

  • 代理对象拖慢了处理速度
  • 有些代理模式实现非常复杂
  • 编译时确认了谁代理谁, 灵活性较差

模式扩展

  • Proxy类中封装了preRequest和postRequest方法
  • 标准代理模式中, 各角色在编译前就存在, 并且指定了代理对象, 执行效率高, 也叫做静态代理模式

行为型模式

Behavioral Pattern

概述

行为型模式是对不同对象间划分责任和算法的抽象. 不仅关注类和对象的结构, 且重点关注他们的相互作用

  • 类行为型模式: 类行为型模式使用继承关系在类间分配行为. 主要通过多态等几个方式来分配父类与子类的职责
  • 对象行为型模式: 对象行为型模式使用对象的聚合关联关系来分配行为. 根据“合成复用原则”, 尽量使用关联关系取代继承关系. 因此, 大部分行为型模式都是对象行为型模式

职责链模式

Chain of Responsibility Pattern

动机和定义

职责链模式用以处理请求链式传递的模式. 职责链可以是一条直线, 一个环或者树形. 链上每一个对象都是请求的处理者.

客户端无需关心请求的处理细节和传递, 只需将请求发送到链上即可. 将请求发送和处理解藕, 这就是职责链模式的动机.

模式结构

image-20241108224347584

  • Handler: 抽象处理者角色, 定义了一个处理请求的接口, 一般设计为抽象类, 还持有一个对下家的引用(可以设为protected).
  • ConcreteHandler: 具体处理者角色, 可以处理请求. 在处理之前需要判断是否有权限, 如果没有直接转发给下一个

模式分析

在职责链模式里, 很多对象由每一个对象对其下家的引用而连接起来形成一条链

发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求, 这使得系统可以在不影响客户端的情况下动态地重新组织链和分配责任

职责链模式并不创建职责链, 一般是在使用该职责链的客户端中创建职责链

优点

  • 职责链模式让一个对象无须知道请求的处理和转发细节. 接收者和发送者都没有对方明确信息
  • 只需要维护一个后继者的引用, 很方便
  • 可以动态组织链条和重新分配责任, 符合开闭原则

缺点

  • 由于没有明确接收者, 就不能保证一定有处理
  • 长职责链的效率较低, 有死循环风险

适用环境

  • 有多个对象处理同一个请求, 具体谁来处理在运行时刻决定
  • 客户端只需发送请求到链上, 不关心后续
  • 需要动态改变职责先后顺序

职责链模式的本质: 分离职责, 动态组合

模式扩展

  • 纯职责链: 要么承担全部责任, 要么直接转发, 且必须有对象处理请求
  • 不纯的职责链: 可以部分处理再转发, 可能存在事件未被处理

命令模式

Command Pattern, 动作(Action)模式, 事务(Transaction)模式

动机与定义

在生活中, 控制灯泡, 排气扇的开关和电器本身并没有直接关系. 一个开关在安装之后可能用来控制电灯, 也可能用来控制排气扇.

我们可以将开关理解成一个请求的发送者, 电灯是请求的接受者和处理者. 这两者并不存在直接耦合关系, 而是通过电线连接在一起. 使用不同的电线可以连接不同的请求接受者.

我们希望设计一种松耦合的方式来完全解除请求发送者与接收者的关系. 在命令模式中, 发送者和接收者没有直接引用关系, 发送请求的对象只知道如何发送, 不知道如何处理.

命令模式将请求封装为对象, 让我们可以用不同请求对客户参数化. 对请求排队或记录日志, 以及支持可撤销操作. 命令类是命令模式的核心.

命令模式是对象行为型模式.

模式结构

image-20241109095814047

  • Command: 抽象命令类, 一般是抽象类或者接口, 在其中声明了用于执行请求的execute()等方法
  • ConcreteCommand: 具体命令类, 对应具体的接收者对象, 将接受者对象的动作绑定其中. 在实现execute方法的时候, 将调用接受者的相关操作action
  • Invoker: 调用者, 即请求发送者. 调用者不需要确定接收者, 在运行时, 将具体命令对象注入其中, 再调用其execute方法, 实现间接调用接受者的相关操作
  • Receiver: 接收者执行具体业务处理

模式分析

命令模式的本质是对请求进行封装

一个请求对应一个命令, 分割发出/执行的职责.

一个请求对应一个操作: 发送者发送请求, 要求执行某操作. 接收者收到请求并执行该操作.

发送者不关心请求如何被接收, 是否被执行, 具体如何执行.

模式优点

  • 更松散的耦合, 将请求者和接收者完全解藕, 相同请求可以对应不同接收者, 相同接收者也可以处理不同请求者
  • 更动态的控制, 对请求的封装方便参数化, 队列化, 日志化
  • 更好的扩展性, 新命令很容易加入, 无须修改代码, 符合开闭原则

模式缺点

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

适用环境

  • 需要抽象出执行的动作, 并参数化
  • 命令的排列, 记录, 取消重做, 崩溃恢复, 事务系统

模式扩展

命令队列

命令队列的实现方法有多种, 其中最常用的一种方式是增加一个CommandQueue类,由该类来负责存储多个命令对象, 而不同的命令对象可以对应不同的请求接收者

命令队列类似批处理

请求日志

将请求的历史记录保存下来, 如果系统故障, 可以作为恢复机制

请求日志也可以用于实现批处理, 将命令队列中的所有命令对象都存储在一个日志文件中, 执行一个, 删除一个.

撤销操作

可以在命令类中增加一个逆向操作undo, 也可以选择保存对象的历史状态来实现撤销, 后者可以用备忘录模式实现

宏命令

宏命令又称为组合命令, 是命令模式和组合模式的联用

当调用宏命令的execute()方法时, 将递归调用它所包含的每个成员命令的execute()方法.

一个宏命令的成员可以是简单命令, 还可以继续是宏命令


迭代器模式

Iterator Pattern, 游标(cursor)模式

模式动机和定义

在现实生活中, 外面通过遥控器来操控电视机. 电视机可以看作一个存储电视频道的集合对象.

遥控器的主要操作有返回上一个, 跳转下一个, 跳转到指定频道. 用户不关心频道具体如何存储在电视机中.

在软件开发中, 有些类用来存储多个成员对象(元素), 这种类通常称为聚合类, 对应对象称为聚合对象.

聚合类有两个职责, 一是存储数据, 二是遍历数据. 从依赖性看, 前者是基本职责, 后者即是可变化的, 又是可分离的.

迭代器模式, 提供一种方法访问聚合对象, 而不暴露这个对象的内部表示. 迭代器模式是对象行为型模式.

模式结构

image-20241110012601251

  • Iterator: 抽象迭代器, 定义访问和遍历元素的接口
  • ConcreteIterator: 具体迭代器, 实现抽象的接口, 通过游标来记录当前位置, 通常是一个表示位置的非负数
  • Aggregate: 抽象聚合类, 用于存储和管理元素对象. 声明createIterator()方法用于创建迭代器对象, 充当工厂角色
  • ConcreteAggregate: 具体聚合类, 负责存储数据, 实现了createIterator()方法, 该方法返回一个具体迭代器

模式分析

迭代器模式的关键思想是把对聚合对象的遍历和访问分离出来, 放入单独的迭代器类中, 来简化聚合对象.

迭代器和聚合对象可以独立的变化, 加强灵活性.

优点

  • 支持不同方式遍历对象
  • 迭代器简化的聚合类
  • 抽象层让增加新的聚合类和迭代器类很方便

缺点

  • 类的个数成对增加
  • 抽象迭代器的设计难度较大

适用环境

  • 访问一个聚合对象的内部而不想暴露内部表示
  • 为聚合对象提供多种遍历方式
  • 为遍历不同的聚合结构提供统一的接口, 在接口的实现中为不同聚合结构实现不同方式, 但是客户可以用统一方法使用

迭代器模式的本质: 控制访问聚合对象中的元素

模式扩展

  • 内部迭代器自己完成迭代
  • 外部迭代器需要手动next()

中介者模式

动机与定义

在QQ聊天中, 有两种聊天方式: 用户与用户私聊, 或者通过QQ群聊天. 使用QQ群, 可以一次向多个用户发送相同的信息和文件而无需一一发送.

群的作用就是将发送的信息和文件转发给每一个用户.

在某些软件中, 类与对象之间的相互调用关系复杂, 此时很需要一个类似QQ群的中间类来协调这些类/对象之间的复杂关系, 以降低系统耦合度.

如果一个系统中对象间的联系呈网状, 多对多状, 将导致系统过度耦合. 这些对象会相互影响, 称为同事对象.

我们可以将对象间的交互行为分离出来, 集中封装在一个中介者对象中, 由中介者进行统一协调.这样一来, 多对多的关系就转化为了一对多关系, 网状结构转化为了星型结构.

中介者模式是对象行为型模式.

模式结构

image-20241110210538438

  • Mediator: 抽象中介者, 定义一个接口用以同事之间的通信
  • ConcreteMediator: 具体中介者, 抽象中介者的子类, 他维持各个同事对象的引用来协调各个同事对象
  • Colleague: 抽象同事类, 定义同事类共有的方法, 同时维持一个对抽象中介类的引用, 其子类通过该引用来与中介者通讯
  • ConcreteColleague: 具体同事类

模式分析

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

  • 在结构上中转
  • 在行为上协调

优点

  • 同事类之间的复杂关系被解藕, 基本上互不依赖, 这使得同事对象可以独立的变化和复用
  • 集中控制交互
  • 多对多的关系变成一对多的关系

缺点

  • 过度集中化

适用环境

  • 一组对象的通讯方式复杂, 相互依赖, 结构混乱
  • 一个对象引用很多对象, 且直接交互, 导致难以复用该对象

中介者模式的本质: 封装交互

模式扩展

中介者模式和外观模式的区别

  • 外观模式用来封装一个子系统内部的多个模块, 目的是为了对外提供简单的接口
  • 中介者模式用来封装内部多模块之间多向的交互, 目的是松散多模块的耦合

备忘录模式

动机与定义

为了防止软件误操作, 提供类似“后悔药”的机制, 让软件系统可以回到误操作前的状态, 我们需要保存用户每一次操作前的状态.

备忘录模式是一种给软件提供后悔药的机制, 通过他可以让系统恢复到某一特定的历史状态.

备忘录模式在不破坏封装的前提下, 捕获一个对象的内部状态, 并保存在外部. 这样可以在以后将对象恢复到这个状态.

备忘录模式是对象行为型模式.

模式结构

image-20241110220658703

  • Originator: 原发器, 一个具体业务类类, 可以创建一个备忘录, 并记录他当前状态, 也可以使用备忘录来恢复. 一半来说, 将需要维护撤销的类设计为原发器
  • Memento: 备忘录, 记录原发器的内部状态, 视情况决定保存哪些状态. 备忘录的设计一般参考原发器设计. 除了原发器与负责人类外, 备忘录对象不能提供其他类使用
  • Caretaker: 负责人又称管理者, 负责保存, 但是不能操作内容或者检查. 负责人类中可以存储备忘录对象, 但是不能修改, 也无需知道细节

模式分析

在开发中, 原发器和备忘录的关系是很特殊的, 他们要分享信息, 又不能让其他类知道

除了原发器类, 不允许其他任何类调用备忘录类的构造函数与相关方法

C++中, 可以使用friend

Java中, 可以将备忘录和原发器类定义在一个package中来实现封装

优点

  • 提供了状态恢复的实现机制
  • 实现了对信息的封装

缺点

  • 资源消耗过大

适用环境

  • 保存一个对象在某时刻的状态
  • 需要在某时刻会滚
  • 防止外界对象破坏历史状态的封装

备忘录模式的本质: 保存和恢复内部状态

模式扩展

多次撤销

在负责人类中定义一个集合来存储多个备忘录, 背个备忘录保存一个状态. 在撤销时, 可以对集合进行逆向遍历(undo), 还可以正向遍历(redo)

备忘录模式与命令模式可以组合使用

命令模式实现中, 在撤销和重做的时候, 可以直接用备忘录模式.

备忘录模式与原型模式可以组合使用

原发器对象创建备忘录的时候, 如果原发器中全部状态都需要保存, 最简单的方法是clone


观察者模式

Observer Pattern, 发布-订阅(Public-Subscrib)模式, 模型-视图(Model/View)模式, 源-监听器(Source/Litsener)模式, 从属者(Dependents)模式

模式动机与定义

考虑一个订报纸的例子, 现在有多个订阅者, 他们向报社订阅报纸, 报社按时出报纸并投送到订阅者手中

image-20241111211656328

当出版社对象出版新报纸的时候, 多个订阅者如何知道? 订阅者对象如何得到报纸内容?

这个问题可以抽象为: 当一个对象的状态发生改变, 如何让依赖他的所有对象得到通知, 并进行处理?

观察者模式定义对象间的一种一对多关系, 使得每当一个对象状态改变时, 相关依赖对象接到通知并被自动更新.

观察者模式是一种对象行为型模式.

观察者模式是使用频率最高的设计模式之一. 在观察者模式中, 发生改变的对象称作观察目标, 被通知的对象称为观察者.

一个观察目标可以对应多个观察者, 观察者之间没有联系.

模式结构

image-20241111212318420

  • Subject: 目标又称主题, 他是指被观察的对象. 在目标中, 会定义一个观察者集合. 一个观察目标可以接受任意数量的观察者. 同时, 定义通知方法notify(). 目标类可以是接口或者抽象类.
  • ConcreteSubject: 具体目标是目标类的子类. 通常包含经常发生该表的数据, 发生改变时, 会通知他的观察者. 如果无须扩展目标类, 则具体目标类可以省略
  • Observer: 抽象观察者, 将对目标的改变作出反应, 一般定义为接口, 通常只声明更新数据的方法update.
  • ConcreteObserver: 具体观察者, 其中维护一个指向具体目标对象的引用. 实现了update方法. 通常在实现时, 可以调用具体目标类的attach方法将自己添加到集合, detach方法将自己删除

模式分析

一个目标可以有任意个观察者, 一旦目标的状态发生改变, 所有的观察者都会被通知.

作为对通知的响应, 每个观察者都将监视状态并同步自己. 这种交互也被称作发布—订阅.

有些情况下, 具体观察者在update时候需要用到具体目标类的属性, 因此需要维护对具体目标类的关联或者依赖. 如不需要, 则可以省略.

优点

  • 实现了观察者和目标间的抽象耦合, 即目标只是知道观察者接口, 不知道具体的类, 从而实现具体类的解藕
  • 实现了动态联动
  • 支持广播通讯
  • 满足开闭原则

缺点

  • 每次都是广播通讯, 不管观察者需不需要, 都会被调用update方法. 如果观察者不需要, 要及时解注册.
  • 两个对象互相监视要注意死循环

观察者模式的本质是触发联动

适用环境

  • 一个对象的改变引起多个其他对象的改变, 而且不知道具体有多少, 具体是谁
  • 两个抽象类可以独立改变和复用, 并且有依赖关系
  • 触发链机制

模式扩展

观察者模式在Java语言中非常重要, 在java.util包中提供了Obserbable和Observer接口, 构成了JDK对观察者模式的支持.

我们可以直接使用Observer接口和Observable类来作为观察者模式的抽象层, 再自定义具体观察者类和具体观察目标类.

MVC模式

MVC模式包含三个角色, 模型(Model), 视图(View)和控制器(Controller). 观察者模式可以用来实现MVC模式.

其中模型是观察目标, 观察者是视图, 控制器是中介者

image-20241111224621730


状态模式

动机与定义

很多事物具有多种状态, 在不同状态下会具有不同的行为, 状态在特定条件下还会发生相互转换.

在UML中可以使用状态图描述变化,

image-20241112153723686

在软件系统中, 有些对象具有多种状态, 这些状态会相互转换, 且对象在不同状态下有不同的行为.

状态模式允许一个对象在其内部状态改变时改变行为. 对象看起来似乎修改了他的类.

状态模式是对象行为型模式.

状态模式将状态分离出来, 封装在状态类中, 使得状态可以灵活变化.

模式结构

image-20241112154045286

  • Context: 环境类, 又称上下文类, 他是拥有多种状态的对象. 内部维护一个抽象类State的引用, 定义当前状态.
  • State: 抽象状态类, 声明了不同状态对应的方法, 而在子类中实现. 相同的方法写在State中, 不相同的写在ConcreteState.
  • ConcreteState: 具体状态类, 每一个具体状态类实现一个具体状态相关的行为, 对应具体状态

模式分析

引入了一个抽象类表示状态, 环境类维持一个对抽象状态类的引用.

环境类实际上是真正拥有状态的对象, 我们只是将环境类中与状态有关的代码提取出来封装到专门的状态类中.

在实际使用时, State对Context可能也存在依赖或者关联关系.

通常有两种实现状态转换的方式:

  • 统一由环境类来负责状态之间, 此时他充当状态管理器角色. 提供changeState()方法, 判断属性并转换状态
  • 由具体状态类负责状态切换. 在业务方法中判断环境类的属性, 为他设置新的状态对象. 此时两者就存在依赖/关联关系

优点

  • 简化应用逻辑控制, 用单独的类封装状态处理.

  • 应用程序控制时只关心状态切换, 不关心状态对应的真正处理

  • 扩展新的状态很容易

缺点

  • 增加新状态需要修改负责状态转换的代码, 不符合开闭原则
  • 结构和实现比较复杂, 系统中类和对象较多

适用环境

  • 对象的行为依赖于状态, 状态的改变导致行为的变化
  • 代码中包含大量判断状态语句

状态模式的本质:根据状态来分离和选择行为

模式扩展

共享状态

在有些情况下多个环境对象需要共享同一个状态, 这时候需要将状态对象定义为环境的静态成员对象

简单状态模式

简单状态模式是指状态都相互独立, 无须进行转换的状态模式. 这种模式可以在客户端直接实例化状态类, 然后将状态对象设置到环境类中. 这种简单状态模式符合开闭原则.

可切换状态的状态模式

大多数的状态模式都是可切换的: 具体状态类内部需要调用环境类Context的setState()方法进行转换. 在具体状态类中调用到环境类的方法, 因此状态类与环境类通常还存在关联/依赖关系.

增加新的状态类可能需要修改其他状态类, 甚至环境类的代码, 否则系统无法切换到新状态.

状态模式和观察者模式的区别

两个模式都是在状态改变时出发行为, 但观察者模式的行为是固定的, 即, 通知所有观察者. 但状态模式根据状态选择不同的处理.


策略模式

Strategy Pattern, 政策(Policy)模式

动机与定义

在很多情况下, 实现某个目标的途径不止一条. 比如我们外出旅游, 可以选择多种出行方式, 骑车, 坐火车, 飞机等. 可以根据实际情况选择.

在软件系统中, 要实现排序功能, 有许多算法可以选择. 为了解决问题, 可以定义一些独立的类来封装不同的算法, 每个类封装一个具体算法. 在这里, 每一个封装算法的类称作策略(Strategy)

策略模式: 定义一系列算法, 将每个算法独立封装, 并可以互相替换. 策略模式让算法独立于客户变化.

策略模式是一种对象行为型模式.

策略模式完美支持开闭原则

模式结构

image-20241112201954829

  • Context: 环境类, 使用算法的角色. 在解决问题时可以采用多种策略. 维持一个对抽象策略类的引用.
  • Strategy: 抽象策略类为支持的算法声明抽象方法. 他可以是抽象类, 具体类或者接口
  • ConcreteStrategy: 具体策略类, 实现抽象策略类定义的抽象策略对象

模式分析

策略模式将算法的责任和算法本身分隔开, 委托给不同对象管理.

策略模式仅仅封装算法, 并不决定何时使用何种算法. 算法的调用由客户端决定. 这要求客户端需要理解具体算法的区别, 自己选择合适算法.

优点

  • 提供了管理算法族的办法, 用抽象策略类定义了算法族, 恰当的继承可以把公共代码移动到抽象策略类中, 避免重复
  • 避免多重条件选择语句
  • 分离了选择算法和执行算法
  • 提供了替换继承的办法, 如果不使用策略模式, 则需要继承环境类来提供算法, 这违背单一职责原则.
  • 完美支持开闭原则

缺点

  • 客户端必须知道所有策略类, 并自行决定选择
  • 系统产生很多具体策略类, 细小的变化也会产生新的策略类
  • 只适合扁平的算法结构, 算法之间地位平等, 如果需要嵌套多个算法, 可以考虑装饰模式

适用环境

  • 一个系统需要动态的在n种算法中选择一种
  • 多重条件选择语句

策略模式的本质: 分离算法, 选择实现

模式扩展

策略模式与状态模式的比较

如果系统中某个类的对象存在多种状态, 不同状态下行为有差异, 而且这些状态之间可以发生转换, 则状态模式

如果系统中某个类的某一行为存在多种实现方式, 而且这些实现方式可以互换时使用策略模式

使用策略模式时, 客户端需要知道所选的具体策略是哪一个.

而使用状态模式时, 客户端无须关心具体状态,环境类的状态会根据用户的操作自动转换.


模板方法模式

动机与定义

在生活中, 很多事情包含几个固定的实现步骤. 比如吃饭, 包含点单, 吃东西, 付款几个步骤. 在这三个步骤中, 点单买单大同小异, 吃什么有多种选择.

在软件开发中, 某个方法的实现需要多个步骤, 有些是固定的, 有些并不固定, 存在可变性.

可以将固定的方法称为基本方法(点单, 吃东西, 买单), 而调用基本方法, 同时定义基本方法的执行次序的方法称作模板方法(请客).

模板方法模式: 定义一个操作框架, 将一些步骤延迟到子类中. 模板方法使得子类可以不改变一个算法的结构即可重定义算法的特定步骤.

模板方法是一种类行为型模式.

模式结构

image-20241112212439048

  • AbstractClass: 抽象类, 定义一系列基本操作, 可以是具体的, 也可以是抽象的. 每一个基本操作对应算法的一个步骤. 其子类可以重新定义, 或实现这些步骤. 同时, 抽象类还实现一个模板方法, 用以定义算法框架
  • ConcreteClass: 具体类是抽象类的子类, 实现抽象操作, 也可以覆盖具体操作

模式分析

模板方法

模板方法是定义在抽象类的, 把基本操作方法组合在一起形成的一个总算法.

这个模板由子类不断加一修改完全继承下来.

模板方法有一个具体方法templateMethod(), 所以模板方法只能是抽象类, 不能是接口.

基本方法

基本方法是实现算法各个步骤的具体方法, 是模板方法的组成部分. 基本方法分为三种:

  • 抽象方法: 在抽象类声明, 在具体子类实现
  • 具体方法: 在抽象类或者具体类声明并原地实现
  • 钩子方法: 在抽象类或者具体类声明并原地实现, 其子类加以扩展. 通常在父类给出空实现, 并以空实现作为默认实现. 当然也可以提供非空的默认实现.

钩子方法

钩子方法有两类, 第一类钩子方法与具体步骤挂钩, 实现在不同条件下执行模板方法的不同步骤. 这类钩子方法通常返回bool类型, 名叫isXXX(). 用于判断, 如果满足则执行.

// 模板方法
public void TemplateMethod() {
  doSomething();
  doOtherThing();
  if (isPrint()) {
    // 通过钩子方法来确定某步骤是否执行
    Print(); 
  }
}

// 钩子方法
public bool isPrint() {
  return true;
}

第二类钩子方法是实现体为空的具体方法, 子类可以根据需要覆盖或者继承. 优点在于如果子类没有覆盖父类定义的钩子方法, 编译可以正常通过.

在模板方法模式中, 子类对象定义的方法会覆盖父类, 实现子类对父类行为的反向控制.

优点

  • 在父类形式化的定义算法, 由子类实现细节. 子类并不会改变模板方法的执行次序

  • 在类库设计中常用

  • 实现反向控制, 通过覆盖钩子方法来决定是否执行特定步骤

  • 符合开闭原则

缺点

  • 需要为每个基本方法的不同实现提供子类, 如果父类可变的基本方法太多, 则会导致类的个数增加. 此时可以结合桥接模式
  • 算法骨架不容易升级

适用环境

  • 对复杂的算法进行分割, 将不变的部分设计为模板方法和父类具体方法, 可以改变的细节交给子类.
  • 需要通过子类来决定父类算法某个步骤是否执行, 实现反向控制.

模板方法的本质: 固定算法骨架

模板方法很好的体现了开闭原则和里氏替换原则

模式扩展

好莱坞原则

在模板方法模式中, 子类不显式调用父类方法, 而是通过覆盖父类方法来实现具体业务逻辑. 父类来调用子类. 这种机制叫好莱坞原则

钩子方法

钩子方法使得子类可以控制父类的行为

模板方法模式和策略模式

模板方法封装算法的骨架, 这个骨架是静态的, 变化的是算法中具体步骤的实现.

策略模式将具体实现封装起来, 所有封装对象等级啊, 可以相互替换.

可以在模板方法中调用策略模式, 即, 在变化的算法步骤通过策略模式实现

也可以在策略模式使用模板方法

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注