本文有什么用? ——设计模式速查表 ——个人理解阐述 ——相关优质参考资料汇总 本文主要为笔者学习和理解设计模式过程中的随记,那么最大的意义显然是对于我自身来说可以用来记录自己的理解;我发觉自己记录的过程实际在形式上体现为:记录课程中没有体现在纸面的要点;理解并用自己的话阐述概念;用自己的一句话概括这个概念等等。 除了这几个用处之外,本文的阐述和归纳也只是个人理解,虽然每个人的修辞和描述习惯都不同,但这些阐述和归纳也不失为一个额外角度,或许能够帮助切削出读者更加准确的理解;

其他:
模块化
任务
任务/资源池化
状态机
表驱动

1994.《设计模式:可复用面向对象设计模式》GOF,提出了23个设计模式;
如今认为设计模式并不依赖于面向对象、或具体某种语言的语法实现,使用struct和指针同样可以实现和应用设计模式;
“概念上,类或者面向对象,就是自带动作的数据;闭包就是自带数据的动作”
C眼光下的OOP:用struct嵌套实现集成和聚合,用指针成员实现组合;
“软件工程既要有底层思维—将复杂问题分解为多个简单问题以处理细节,也要有抽象思维—由于不能掌握全部的复杂对象,我们选择忽视其非本质细节而处理泛化和理想化的对象模型”

当我们提到“依赖”:一般指编译时的依赖,A依赖B即A的编译需要B代码存在,而不是运行时的依赖;
当我们提到“复用”:指代情况大多是编译单元层面的复用(我理解至少需要是一个函数,改得好的话可以有利于增量编译,节省一些测试用例);把其他地方的代码拷贝到这里,不是这里探讨的复用;
怎么理解设计模式,可以这样去提问:没有使用这种模式的时候会有什么问题?

让程序更能应对变化;
设计模式也可以帮助你更快地看懂接口;

设计原则

设计模式不是算法,大部分时候并不是必须要这样安排类的关系才算使用某某设计模式、也不必纠结某段代码使用的到底是什么设计模式;反而要强调的是:设计原则高于设计模式本身;

设计原则包含如下的几条:

DIP,依赖倒置原则

高层模块不应依赖于低层模块,二者都应该依赖于抽象;
抽象不应依赖于实现细节,实现细节应依赖于抽象;

OCP,开放封闭原则

类对象应该对扩展开放,对更改封闭;

SRP,单一职责原则

一个类应该仅有一个引起它变化的原因;
一个类只有一个责任,责任就是它可能变化的方向;

LSP,里氏替换原则

子类必须能够替换它们的基类;
继承表达类型抽象;

ISP,接口隔离原则

不应该强迫客户程序依赖它们不用的方法;
接口应该小而完备;

原则6,优先使用对象组合,而不是类继承

类继承通常是“白箱复用”,对象组合通常是“黑箱复用”

原则7,封装变化点

使用封装来创建分界层,使变化只发生在一侧;

原则8,针对接口编程,而不是针对实现编程

将变量类型声明为某个接口类型,而不是某个具体类型;
客户程序只需要知道对象所具有的接口;

“接口标准化是产业强盛的标志”

组件协作

Template Method,模板方法——我已经把初稿打好,要用的自己填充

动机:某项任务通常有稳定的整体操作结构,各个子步骤却有许多改变的需求;
描述:(在框架中)定义一个操作中的算法框架,而将一些具体步骤(的具体实现)延迟到子类;
CPP举例:在抽象类A中提供模板方法A.Run()作为稳定的算法框架,A.Run()调用虚函数A.vf1();在客户代码中,可以通过继承A和重写A.vf1(),以实现具体步骤细节;
C举例:可以用函数指针/回调函数实现;

PS:

  • 没有稳定就没有复用,也谈不上设计模式;没有变化也没必要复用,也谈不上设计模式了;
  • 设计模式的意义也在于让代码在变化和稳定中寻求平衡;实际上谈到的变化和稳定是相对的,变化也不是时时刻刻在变,稳定也不是永恒不变,但至少有一个变化频率的相对比较;
  • 从这个角度,当我们在看一个类图时,要有意识地注意其中哪些是相对稳定的,哪些是相对变化的;
  • 模板方法尤其体现了依赖倒置的做法;相对的做法往往是Application调用lib,而模板方法的视角下则是Frame调用application;

Strategy,策略模式——多一种情况就多一个条件分支,不如让多态去决定

动机:某个步骤不同情况下使用到的算法可能多种多样;
业务举例:订单计算税额时使用各国的税收计算方法;
CPP举例:策略类S,定义虚函数S.vf()作为接口供上下文调用;为每个具体情形实现一个类Cx,继承S并实现为Cx.vf();在不同的上下文中使用不同的具体策略类对象Cx/Cy/…,实现在运行时算法的切换;
PS:

  • 许多用来决定策略的条件判断,如if-else或switch,都可以用策略模式替代;
  • 如果策略对象Cx不需要归属于实例的变量(不需要归属于对象的闭包),那么各个上下文可以共享同一个策略对象;

Observer/Event,观察者模式

动机:某个对象的状态发生改变,所有的观察者都将得到通知;但是这种依赖关系的实现如果耦合度过高,将会使软件不能很好地抵御变化;

PS:

  • 谈一下C++的多继承:最佳实践其实和Java的继承模型(extends一个父类,implements多个接口)差不多——继承一个主要的基类,其他的都是抽象基类/接口

单一职责

以下两个模式,对于单一职责原则表现得尤为突出。

Decorator,装饰模式——不要哪里都用继承支持新功能,有时应该用接口修饰

动机:
业务举例:网络流和文件流都是流,还有一些需求,如加密文件流、加密网络流、缓存文件流、加密缓存文件流等等的实现,如果要用继承来实现,那类的数量和重复代码会阶乘级别地爆炸;这样的地方不应该用继承,而要用组合——在运行时组装,而没有在编译时组装;

Bridge,桥模式——一个类被多种责任拉扯,那就分一下工

对象创建

要通过“对象创建”模式,避免使用new带来的紧耦合(避免编译时依赖);
它是接口抽象之后的第一步,是面向接口编程的必然需求;

Factory Method,工厂方法——用new要依赖类型,我不如依赖工厂

  • 在依赖具体类型的地方声明类型为接口;
  • 通过调用工厂对象的方法返回使用的类型,而不是使用new;
  • 依赖的具体工厂类型可以通过构造函数传入;

Abstract Factory,抽象工厂

创建依赖的一系列对象(有相互关联的一些对象);
把有相互关联的对象创建方法放到一起;

Prototype,原型模式——已经准备好了一个对象,通过克隆它创建新对象

通过克隆自己(深拷贝)来创建对象;
其实就是工厂方法中的工厂和具体对象合并了;
和工厂有什么区别呢?——如果要创建的对象特别复杂(带有较为复杂的状态/拥有众多稳定连接/持有较多数据)时使用原型;

CPP:深拷贝通过拷贝构造函数实现;
其他高级语言可以使用序列化方法实现;

Builder,构建器——用来创建对象的“模板方法”,复杂的创建过程提取为构建器

动机:某个复杂对象的各个组成部分剧烈变化,而组成它的步骤相对稳定;——似乎是作为“构造函数”的模板方法;(不过从C++的视角,虚函数不能在构造函数中调用;其他语言如Java可以实现构造函数中的动态绑定)
CPP:通常实现为一个Init()函数;
PS:

  • 如果某个类的创建过程过于复杂,形成了一个“肥胖的类”,那么可以对其进行拆分,拆成行使类行为的本体,和用于初始化的CxxBuilder;(点题)

对象性能

Singleton,单例模式——用这个类的人不要给我创建出第二个对象

类的设计者如何保证客户代码能且只能创建一个实例;
CPP:可以将构造函数声明为private;

  • 可以实现为返回static的全局单例对象s;
  • 或者实现为返回static的成员对象ms,ms在且仅在为空时初始化;——注意getInstance的线程安全;
  • 加锁的版本;
  • 双检查锁的版本,锁后检查,锁前同样检查一次,避免给读者上锁;——但是由于指令重排,这里很有可能产生问题,让读的线程取到还没有构造完的instance指针;(C++11之后可以使用std::atomic_thread_fence内存屏障,避免指令重排)
  • 参见:C++ 并发与多线程学习笔记(五)单例设计模式 共享数据分析_一群坑货的博客-CSDN博客

    Flyweight,享元模式

    查询池子里的对象,如果已经有了,那么可以共享;

接口隔离

“加一层”,添加一层间接的,相对稳定的接口,来隔离变化;

Facade,门面模式——暴露出来的接口能不能简化?

使用一个门面类F作为子系统的统一对外接口,可以类比以太网网关、部门接口人;
CPP:可以实现为一个单例的门面类F,持有子系统内部模块对象的指针,外部通过调用F的函数影响内部;
补充:Facade思想让接口变得集中,但要避免“胖接口”问题;

Proxy,代理模式——我的接口不方便直接给你调用

为其他对象提供代理以控制/限制对本对象的访问;用来应对高性能/安全控制/分布式需求

Adapter,适配器——不能动的老接口怎么包装成目标接口

把老的接口转换成新的接口,或者包装成希望的接口;
插头转换器;

CPP举例:

  • 类适配器的一种形态:类适配器A多重继承INew和类IOld,在实现的INew.f中调用IOld.f以实现使用新接口;——但是这种“多继承”的方案,让A绑定了一种接口;
  • 对象适配器的一种形态:对象适配器A继承和重载INew,通过组合一个被适配接口对象old,在实现的INew.f中调用IOld.f;
  • 标准库的容器适配器,std::stack,std::queue

Mediator,中介模式——多方相互依赖太复杂,交给专门的中介吧

用中介对象来封装一系列的对象交互,中介使得各对象不需要显式的相互引用(编译时的依赖);
举例:界面控件-数据模型;希望更改界面时DM更改,DM变化时界面更改,这样往往会产生复杂的依赖;避免“相互交互关联的”对象紧耦合;
这种做法类似于计算机网络早期发展过程中,全联通网络转变成星形网络;

辨析:

  • 门面模式侧重于系统外和系统内的隔离;侧重于系统对外暴露接口的(单向)关联;
  • 中介模式侧重于同一系统内的多个对象的解耦;侧重于对象间相互的(双向)关联;

    状态变化

    State,状态模式——多一个状态就多一个条件分支,不如用多态实现状态机

  • “虚函数在这里的作用,其实像是一个运行时的if-else/switch”

  • 如果只把这个模式理解成“状态机”,就属于盲人摸象了

辨析:

  • 状态模式和策略模式:不要纠结于一段代码使用的设计模式究竟叫什么名字

Memento,备忘录模式

在不破坏封装性的前提下,记录一个对象的内部状态,并在对象之外保存这个状态,以备以后将对象恢复到保存的状态;

数据结构

Composite,组合模式——

使得对单个对象和组合对象的处理具有一致性——对树或者节点或者叶子节点都可以调用相同的接口

PS:

  • 应该在树节点还是子节点中提供插入和删除呢?
  • 组合模式将一对多的关系也转化为一对一关系——利用多态,避免了对单个对象(叶子)和组合对象(树)的讨论。

Iterator,迭代器模式——

迭代抽象:提供一种方法给客户代码以顺序访问聚合对象中的内容,而不暴露内部结构;
迭代多态:为便利不同的集合结构提供一个统一的接口(都是iterator),从而支持同样的算法使用相同的操作在不同聚合结构上进行操作;

PS:

  • C++的视角下,面向对象的迭代器的实现已经过时了,因为每次都要通过虚函数进行调用,大数据量的迭代性能很差;——更推荐使用泛型编程实现的迭代器,(编译时多态)
  • 其他语言没有上述问题(因为没有支持编译时多态的模板(?))
  • 迭代器的设计和使用要注意一个问题:编译的同时更改迭代器所在的集合结构,代码的实际行为是否支持;

Chain of Responsibility,责任链模式

动机:一个请求可能被多个对象处理,但是每个请求只能有一个接受者,如果被显式指定则可能会带来发送者和接受者的紧耦合。——让发送者不需要指定具体的接受者,让请求的接受者自己在运行时决定处理请求
请求沿着责任链(链表)传播
每个handler在handle时判断自己是否能处理,可以调用process以处理请求,也可以传递给下一个链上的handler

行为变化

Command,命令模式

Visitor,

领域问题

Interpretor