面向对象的设计模式(一)

设计模式中文版封面.jpg

软件设计的好坏评判标准之一是复用性

变化与稳定

实际开发中最大的困难就是需求的多变,而变化是软件复用的天敌。面向对象的设计最大的优势是“抵御变化”,注意不是“消灭变化”,而是把变化像小兔子一样锁在笼子里限制起来。 设计模式是软件设计中常见问题的典型解决方案。它们就像能根据需求进行调整的预制蓝图,可用于解决代码中反复出现的设计问题。[1] 设计模式与方法或库的使用方式不同,你很难直接在自己的程序中套用某个设计模式。 模式并不是一段特定的代码,而是解决特定问题的一般性理念。 你可以根据模式来实现符合自己程序实际所需的解决方案,所以最佳的实践设计模式的方法是重构到模式(Refactory to Patterns)。当然如果你非常熟悉各种模式的应用场景,也可以一上来就按照适当的设计模式去开发。

1994年,GoF在他们的书中总结了23种设计模式,此后人们又陆续发现了几十种面向对象的设计模式,“模式方法” 开始在其他程序开发领域中流行起来。 如今,在面向对象设计领域之外,人们也提出了许多其它的模式。设计模式是针对软件设计中常见问题的工具箱,其中的工具就是各种经过实践验证的解决方案。无疑这些设计模式提高了在团队中的沟通效率,我们只要说在这种情况下,你使用工厂模式就可以了,不需要过多的解释。 但是模式不是金科玉律,千万不要为了用模式而去套模式。在实际项目中应该实事求是的对项目进行调整。况且,模式也是会过时。以前广泛使用的模式可能在新一代的语言中内化为语言的设计理念。用设计模式的时候最重要的是分清工程里哪些是变化的,哪些是不变的。把变化的那部分用设计模式的方案来改造一下。试想两个极端:假如我们的工程里面所有模块都是变化的,那用什么设计模式都救不了你。再譬如,我们的工程里所有模块都是稳定的,那你也没有必要去用设计模式了,直接写代码就完事了。[2] 因此,分清变与不变最重要,设计模式主要着力点就是那些变化的地方。

八大设计原则

比模式更重要的是设计原则,下面介绍一下八大原则: [2]

  1. 依赖倒置原则(DIP)
    • 高层模块(稳定)不应该依赖低层模块(变化),二者都应该依赖抽象(稳定)。
    • 抽象(稳定)不应该依赖于实现细节(变化),实现细节应该依赖于抽象(稳定)。
  2. 开放封闭原则(OCP)
    • 对扩展开放,对更改封闭。
    • 类模块应该是可扩展的,但是不可修改。
  3. 单一职责原则(SRP)
    • 一个类应该仅有一个引起它变化的原因。
    • 变化的方向隐含着类的责任。
  4. 里氏替换原则(LSP)
    • 子类必须能够替换它们的基类(IS-A)。
    • 继承表达类型抽象。
  5. 接口隔离原则(ISP)
    • 不应该强迫客户程序依赖它们不用的方法。
    • 接口应该小而完备。
  6. 优先使用对象组合,而不是类继承
    • 类继承通常为“白箱复用”,对象组合通常为“黑箱复用”。
    • 继承在某种程度上破坏了封装性,子类父类耦合度高,而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低。
  7. 封装变化点
    • 使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合。
  8. 针对接口编程,而不是针对实现编程
    • 不将变量类型声明为某个特定的具体类,而是声明为某个接口。
    • 客户程序无需获知对象的具体类型,只需要知道对象所具有的接口。
    • 减少系统中各部分的依赖关系,从而实现“高内聚、松耦合”的类型设计方案。

二十三个设计模式

模式名称 应用场景
模板方法模式 常用于框架设计,例如 Windows 的 MFC
策略模式 代码中如果if else 很多的情况下考虑用这个模式
观察者模式 如果一个事件发生后,需要很多对象都能对此作出响应,则考虑观察者模式
装饰模式 动态(组合)地给一个对象增加一些额外的职责
桥模式 将抽象部分(业务功能)与实现部分(平台实现)分离,使它们都可以独立地变化
工厂方法 提供一种 “封装机制” 来避免客户程序和这种 “具体对象创建工作” 的紧耦合
抽象工厂 提供一个接口,让该接口负责创建一系列 “相关或者相互依赖的对象”,无需指定它们具体的类
构建者模式 Builder 模式主要用于 “分步骤构建一个复杂的对象”。在这其中 “分步骤” 是一个稳定的算法,而复杂对象的各个部分则经常变化
单件模式 软件系统中,有些特殊的类必须保证它们在系统中只存在一个实例,才能确保它们的逻辑正确性,以及良好的效率
享元模式 运用共享技术有效的支持大量细粒度的对象
门面模式 为子系统中一组接口提供一致(稳定)的界面,facade模式定义了一个高层接口,这个接口使得这个子系统更容易的复用
代理模式 为其他对象提供一种代理以控制(隔离)对这个对象的访问
适配器模式 将一个类的接口转换成客户希望的另一个接口,一般用在新老模块共同复用的情况
中介模式 多个对象相互关联交互,对象之间维持一个复杂的引用关系。这种情况下,使用一种中介对象来管理对象的关联关系,避免紧耦合关系
状态模式 出现需要状态机的场景,此模式与策略模式有点相似
备忘录 此模式把对象的内部状态存储下来,由于现代语言都提供序列化的特性,此模式已过时
组合模式 将对象组合成树形结构以表示 “部分/整体” 的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
迭代器 顺序访问聚合对象中的各个元素,而又不暴露该对象的内部表示。此模式现在已经被泛型编程中的迭代器取代了。
职责链模式 当某种请求发生时,需要多个模块对此请求作出处理,把各个模块串成链表,依次处理该请求
命令模式 将一个请求(行为)封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销操作。
访问器模式 某些类层次结构中需要常常要增加新的行为方法,如果直接在基类中修改,会给子类增加严重负担。 运行时透明的为各层次结构上的各个类添加新操作
解释器模式 某一个特定领域的问题比较复杂,将特定领域的问题表达为某个语法规则下的句子,然后构建一个解释器来解释句子,从而达到解决问题的目的

重构技法

  • 静态转动态
  • 早绑定转晚绑定
  • 继承转组合
  • 编译时依赖转运行时依赖
  • 紧耦合转松耦合

什么时候不用模式

  • 代码可读性很差时(这时应该首先改善代码的可读性,用模式救不了你,运用设计模式是在可读性问题解决后的事情)
  • 需求理解很浅时
  • 变化没有显现时 (不要提前过度设计,等到变化的方向明确时,设计模式才不会用错)
  • 不是系统的关键依赖点
  • 项目没有复用价值时 (一般外包公司的项目都是这种类型,所以外包公司的开发人员不关心设计模式)
  • 项目即将发布时 (设计模式是为了更好的维护代码,没有必要在发布前还引入未知bug)

经验之谈

  • 不要为了模式而模式
  • 关注抽象类和接口
  • 理清变化点和稳定点
  • 审视依赖关系
  • 要有 Framework 和 Application 的区分思维
  • 良好的设计是演化的结果

最重要的事情

你可以忘记所有的设计模式,但是设计原则必须牢记于心。 所有的模式都是对着这八条原则,在不同的场景下推演出来的。 所有的模式实际上全部是依靠多态这个特性来实现的。 后面几个章节将介绍一下 C 语言开发中常用的设计模式,注意不是 C++ 语言。C 语言同样可以做到面向对象设计。只不过需要开发者自己做一些基础设施。


  1. https://refactoringguru.cn/design-patterns/ 

  2. 李建忠《 C++ 设计模式》课件 

updatedupdated2021-11-292021-11-29