一个公式总结设计模式

2,120 阅读8分钟

在编程之路的升级过程中总是反复会遇到设计模式(此处的设计模式是指面向对象的设计模式),大学的时候没把它学明白,到了工作的时候依旧没有学明白,虽然会用,但只是死记硬背,并没有理解究竟何为设计模式,设计模式就是代码的固定编程模板?如果你这么认为,说明你还对它一无所知。

在我看来,设计模式 = SOLID + ER,相比于记住每种设计模式的类结构图,我们更应该记住每种设计模式对应场景下有哪些角色,这些场景下的角色也是我认为设计模式不是伪命题的原因。设计模式总共有 23 个,如果只是死记硬背的话,不仅记忆时间长,还容易忘记。其实之所以是 23 个,是因为刚好总结的时候有 23 个设计模式能够对应覆盖日常开发时经常会遇到的 23 个场景。详细分析会在以下展开,但是在分析设计模式前,我想先分析一下 SOLID 原则。

SOLID 原则总结

为什么要先讲 SOLID 原则?一个原因是设计模式的实现是符合 SOLID 原则的,还有一个原因就是当多个知识点产生联系时可以加深知识点的记忆。

SOLID 原则的详细介绍

这里对 SOLID 的基础概念只做简单介绍:

  • 单一职责原则
    • 尽量让每个类只负责软件中的一个功能, 并将该功能完全封装(你也可称之为隐藏) 在该类中
  • 开闭原则
    • 对于扩展, 类应该是“开放” 的; 对于修改, 类则应是“封闭” 的
  • 里氏替换原则
    • 派生类(子类)对象可以在程序中代替其基类(超类)对象
  • 接口隔离原则
    • 根据接口隔离原则, 你必须将“臃肿” 的方法拆分为多个颗粒度更小的具体方法
  • 依赖倒置原则
    • 高层次的类不应该依赖于低层次的类

再结合面向对象的三大特性(即封装、继承和多态)来分析 SOLID 原则:

  • 类/接口、方法和代码块都基于封装、继承和多态
    • 里氏替换原则基于多态,多态基于继承
    • 依赖倒置原则基于继承,继承是为了扩展
      • 单一职责基于扩展,开闭原则基于扩展
        • 接口隔离原则是单一职责在接口中的实现

用一张图整理:

每个模式的结构特点

考虑到篇幅以及设计模式之间有相似性,以下不一一分析所有设计模式,只取常用的

创建型模式

  • 工厂方法模式
    • 定义
    • 结构:超类 A 有 1 个方法 z 并返回接口 B,超类 A 的子类 C 实现方法 z 并返回接口 B 的实现类 D
    • 意图:一个 A 的子类生产一个 B 的实现类
    • 满足单一职责、开闭原则
  • 抽象工厂模式
    • 定义
    • 结构:接口 A 有多个方法分别返回接口 B、接口 M 和接口 N,接口 A 的实现类 C 实现多个方法并返回接口 B 的实现类 D,接口 M 的实现类 E,接口 N 的实现类 F
    • 意图:一个 A 的实现类生产多个 B 的实现类
    • 满足单一职责、开闭原则
  • 生成器模式
    • 定义
    • 结构:接口 B (builder) 定义 N 个方法,类 A (生成器) 拥有接口 B 作为成员变量,并且有一个方法能够按某种顺序调用 B 的实现类中 N 的若干个方法
    • 意图:一个 A 以特定步骤生成一个 B 的实现类
    • 满足单一职责
  • 单例模式
    • 定义
    • 结构:类 A 有类 A 作为成员变量
    • 意图:一个 A 生产一个 A

以上设计模式的结构可以总结为:B 是 A 的成员变量,A 的子类中有一个方法调用 B 的子类的构造方法

结构型模式

  • 适配器模式
    • 定义
    • 结构:在类 A 的方法 fa() 中要使用类 B 的某些方法,只需用类 C 继承 A,把 B 作为成员变量,重写 fa() 时使用 B 的方法
    • 场景:A 包装 B,使调用 A 的一个方法时能调用 B 的一个方法
    • 满足单一职责、开闭原则
  • 桥接模式
    • 定义
    • 结构:在抽象类 A 子类的多个方法中分别使用接口 B 的的实现类中不同的方法,B 是 A 的成员变量
    • 场景:A 包装 B,A 中有多个方法能分别调用 B 中的不同方法
    • 满足单一职责、开闭原则
  • 组合模式
    • 定义
    • 结构:抽象类 A 的子类中把接口 B 数组作为成员变量,并且 A 的子类中有一个方法 fa() 会遍历接口 B 数组并调用 B 的同一个方法
    • 场景:A 包装多个 B,A 中有方法调用每个 B 的方法
    • 满足开闭原则
  • 装饰器模式
    • 定义
    • 结构:接口 A 的实现类 B 中把接口 A 的另一个实现类 C 作为成员变量,并且 B 有一个方法 f() 会调用 C 的 f()
    • 场景:A 有两个子类 B 和 C,B 包装 C
    • 满足单一职责
  • 代理模式
    • 定义
    • 结构:接口 A 的实现类 B 中把接口 A 的另一个实现类 C 作为成员变量,并且 B 有一个方法 f() 会调用 C 的 f()
    • 场景:A 有两个子类 B 和 C,B 代理 C
    • 满足开闭原则

行为模式

  • 责任链模式
    • 定义
    • 结构:单链表
    • 满足单一职责、开闭原则
  • 策略模式
    • 定义
    • 结构:类 A 有 接口 B 作为成员变量,并且 A 中有方法调用 B 中的方法
    • 场景:A 的方法 f(B b) 根据传入的 B 子类的类型调用对应的方法
    • 满足开闭原则
  • 模板方法模式
    • 定义
    • 结构:超类 A 有一个方法 a() 依次调用 A 中的 b()、c()、n() 方法,子类继承 A 后只需实重写 b() ... n() 中若干个方法即可
  • 观察者模式
    • 定义
    • 结构: 接口 B 类型的数组是类 A 的成员变量,A 中有方法 a() 一次性调用所有 B 数组里每个对象的同一个方法
    • 满足开闭原则

根据以上对不同设计模式的分析,总的来说可以把设计模式理解成:结构 + 场景。 其中结构基本上是符合 SOLID 原则的(单例模式除外),所以上面的结论转化为:SOLID + 场景 这里的 场景 理解为某个身份的人 A 想对某个身份的人 B 做什么事,因此可以把场景当成实体及其之间的关系(有点像数据库设计中的 ER图),所以可以再把上面的结论转化为:SOLID + ER,其中 E 表示实体代表的角色,R 代表实体角色之间的关系

设计模式之间的联系

将设计模式的结构可视化,得出下图: 往大了说,其实每个设计模式的结构无非是类 A 调用类 B 的方法(A 和 B 可以是抽象类或者是接口,A 可以等于 B,B 可以是多个不同的接口或类),于是可以得出一个结论:设计模式的意图都是类似 A 调用 B,即 SOLID + ER 中的 R 都是 A 调用 B 的关系。 于是,不同的设计模式之间的差别就在于 ER 的不同,可以总结为:

  • A 调用 B
    • A 生产 B 对应创建型模式
    • A 包装 B 对应结构型模式
    • A 执行 B 对应行为模式

总结

  • 设计模式 = SOLID + ER
    • 其中 E 表示实体代表的角色,E 根据场景不同而不同
    • R 代表实体角色之间的关系,R 都基于A 调用 B
    • 单例模式不符合 SOLID

在一开始总结出上述公式时,我开始怀疑设计模式是一个伪命题。但是当我开始思考为什么设计模式是 23 个的时候,我就明白了设计模式存在的意义——要满足 SLOID 的话就得适当地抽象出对象来给代码解耦,因为每个人的编程能力不同,这导致了能力稍微差点的人不能察觉到代码中可以提取的对象,而设计模式是几位行业大佬总结了多年代码经验所得出的,其凝聚了日常开发中最常见的场景,这些场景可以给我们提供参考

参考

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!