Skip to content

设计模式简介

在1994年,由Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides四⼈合著出版了⼀本名为DesignPatterns-Elements of Reusable Object-Oriented Software(中⽂译名:设计模式-可复⽤的⾯向对象软件元素)的书,该书⾸次提到了软件开发中设计模式的概念。四位作者合称GOF(四⼈帮,全拼Gang of Four)

设计模式的出现并不是说我们要写的代码一定要遵循设计模式所要求的方方面面,这是不现实同时也是不可能的。设计模式的出现,其实只是强调好的代码所具备的一些特征(六大设计原则),这些特征对于项目开发是具备积极效应的,但不是说我们每实现一个类就一定要全部满足设计模式的要求,如果真的存在完全满足设计模式的要求,反而可能存在过度设计的嫌疑。

同时,23种设计模式,其实都是严格依循设计模式六大原则进行设计,只是不同的模式在不同的场景中会更加适用。

设计模式的理解应该重于意而不是形,真正编码时,经常使用的是某种设计模式的变形体,真正切合项目的模式才是正确的模式。

基础概念

设计模式(Design pattern) 是⼀套被反复使⽤、多数⼈知晓的、经过分类编⽬的、代码设计经验的总结。使⽤设计模式是为了可重⽤代码、让代码更容易被他⼈理解、保证代码可靠性。毫⽆疑问,设计模式于⼰于他⼈于系统都是多赢的,设计模式使代码编制真正⼯程化,设计模式是软件⼯程的基⽯,如同⼤厦的⼀块块砖⽯⼀样。项⽬中合理的运⽤设计模式可以完美的解决很多问题,每种模式在现在中都有相应的原理来与之对应,每⼀个模式描述了⼀个在我们周围不断重复发⽣的问题,以及该问题的核⼼解决⽅案,这也是它能被⼴泛应⽤的原因。

设计模式的分类

分类有两个维度:

  1. 模式的目的
  2. 模式的作用范围。

根据目的分类

  • 创建型模式:对象实例化的模式,创建型模式⽤于解耦对象的实例化过程。
  • 结构型模式:把类或对象结合在⼀起形成⼀个更⼤的结构。
  • ⾏为型模式:类和对象如何交互,及划分责任和算法。

根据作用范围分类

  1. 类模式:用于处理类与子类之间的关系,这些关系通过继承来建立,是静态的,在编译时刻便确定下来了。
  2. 对象模式:用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。
范围\目的创建型模式结构型模式行为型模式
类模式工厂方法模式适配器模式(类)模板方法模式,解释器模式
对象模式单例模式,原型模式,抽象工厂模式,建造者模式适配器模式(对象),代理模式,桥接模式,装饰模式,外观模式,享元模式,组合模式策略模式,命令模式,职责链模式,状态模式,观察者模式,中介者模式,迭代器模式,访问者模式,备忘录模式

其中适配器模式分为 类适配器模式 和 对象适配器模式。

设计原则

开闭原则(Open Closed Principle,OCP) 由勃兰特·梅耶(Bertrand Meyer)提出,他在1988年的著作《⾯向对象软件构造》(Object Oriented Software Construction)中提出:软件实体应当对扩展开放,对修改关闭(Software entities should be open for extension,but closed for modification)

⾥⽒替换原则(Liskov Substitution Principle,LSP) 由麻省理⼯学院计算机科学实验室的⾥斯科夫(Liskov)⼥⼠在1987年的“⾯向对象技术的⾼峰会议”(OOPSLA)上发表的⼀篇⽂章《数据抽象和层次》(Data Abstraction and Hierarchy)⾥提出来的,她提出:继承必须确保超类所拥有的性质在⼦类中仍然成⽴(Inheritance should ensure that any property proved about supertype objects also holds for subtype objects)。

任何基类可以出现的地⽅,⼦类⼀定可以出现,只有当派⽣类可以替换掉基类,软件功能不受影响时,基类才能真正被复⽤, ⽽派⽣类也能够在基类的基础上增加新的⾏为。

依赖倒置原则(Dependence Inversion Principle,DIP) 是Object Mentor公司总裁罗伯特·⻢丁(Robert C.Martin)于1996年在C++ Report上发表的⽂章。依赖倒置原则的原始定义为:⾼层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象(High level modules shouldnot depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details. Details should depend upon abstractions)。其核⼼思想是:要⾯向接⼝编程,不要⾯向实现编程。

⾼层不应该依赖低层,要⾯向接⼝编程。在⾼层和底层之间插⼊⼀层抽象层,屏蔽底层细节,向上层提供统⼀的接⼝

单⼀职责原则(Single Responsibility Principle,SRP) ⼜称单⼀功能原则,由罗伯特·C.⻢丁(Robert C. Martin)于《敏捷软件开发:原则、模式和实践》⼀书中提出的。这⾥的职责是指类变化的原因,单⼀职责原则规定⼀个类应该有且仅有⼀个引起它变化的原因。

接⼝隔离原则(Interface Segregation Principle,ISP) 要求程序员尽量将臃肿庞⼤的接⼝拆分成更⼩的和更具体的接⼝,让接⼝中只包含客户端感兴趣的⽅法。2002年罗伯特·C.⻢丁给“接⼝隔离原则”的定义是:客户端不应该被迫依赖于它不使⽤的⽅法(Clients should not be forced to depend on methods they do not use)。该原则还有另外⼀个定义:⼀个类对另⼀个类的依赖应该建⽴在最⼩的接⼝上(The dependency of one class to another one should depend on the smallest possible interface)。

迪⽶特法则(Law of Demeter,LoD) ⼜叫作最少知识原则(Least Knowledge Principle,LKP),产⽣于1987年美国东北⼤学(Northeastern University)的⼀个名为迪⽶特(Demeter)的研究项⽬,由伊恩·荷兰(Ian Holland)提出,被UML创始者之⼀的布奇(Booch)普及,后来⼜因为在经典著作《程序员修炼之道》(The Pragmatic Programmer)提及⽽⼴为⼈知。

不该知道的不要知道,⼀个类应该保持对其它对象最少的了解,降低耦合度

合成复⽤原则(Composite Reuse Principle,CRP) ⼜叫组合/聚合复⽤原则(Composition/Aggregate Reuse Principle,CARP)。它要求在软件复⽤时,要尽量先使⽤组合或者聚合等关联关系来实现,其次才考虑使⽤继承关系来实现。

下面我们对最常使用的设计原则进行阐述

单一职责原则

C++面向对象三大特性之一的封装指的就是将单一事物抽象出来组合成一个类,所以我们在设计类的时候每个类中处理的是单一事物而不是某些事物的集合。

设计模式中所谓的单一职责原则,就是对一个类而言,应该仅有一个引起它变化的原因,其实就是将这个类所承担的职责单一化(就跟海贼王中的能力者一样,每个人只能吃一颗恶魔果实,拥有某一种能力【黑胡子这个Bug除外】)。

如果一个类承担的职责过多,就等于把这些职责耦合到了一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致设计变得脆弱,当变化发生时,设计会遭受到意想不到的破坏。

偷袭白胡子的这个男人被白胡子视为自己的儿子,他叫斯库亚德,本来是一起去救艾斯的【此时的他是一个单一职责的类】,后来被赤犬挑拨离间想到了自己的过去,并萌生了别的想法【这个类被追加了一些其他的职责】,最终背叛并刺穿了白胡子的身体【这个类没能完成开始时的预期任务,就此废掉了】。由此可见,让一个类承担太多的职责绝非好事。

软件设计真正要做的事情就是,发现根据需求发现职责,并把这些职责进行分离,添加新的类,给当前类减负,越是这样项目才越容易维护。

开放封闭原则

开放 – 封闭原则说的是软件实体(类、模块、函数等)可以扩展,但是不可以修改。也就是说对于扩展是开放的,对于修改是封闭的。

该原则是程序设计的一种理想模式,在很多情况下无法做到完全的封闭。但是作为设计人员,应该能够对自己设计的模块在哪些位置产生何种变化了然于胸,因此需要在这些位置创建抽象类来隔离以后发生的这些同类变化(其实就是对多态的应用,创建新的子类并重写父类虚函数,用以更新处理动作)。

此处的抽象类,其实并不等价与C++中完全意义上是抽象类(需要有纯虚函数),这里所说的抽象类只需要包含虚函数(纯虚函或非纯虚函数)能够实现多态即可。

草帽团船长路飞从出海到现在一共召集了9个伙伴,这些伙伴在船上的职责都是不一样的,有音乐家、船工、舵手、航海士、剑士、考古学家、狙击手、厨师、船医,作为船长没有要求自己学习这些船员的技能【对自己来说是封闭的】,而是提出了伙伴的概念【这就是一个可变的抽象】,最终找到了优秀的伙伴加入【对外是开放的,每个伙伴都是这个抽象的具体实现,但他们的技能又有所不同】,事实证明这样做是对的,如果反其道而行之,不仅违背了开放封闭原则,也违背了单一职责原则。

开放 – 封闭原则是面向对象设计的核心所在,这样可以给我们设计出的程序带来巨大的好处,使其可维护性、可扩展性、可复用性、灵活性更好。

依赖倒转原则

关于依赖倒转原则,对应的是两条非常抽象的描述:

  • 高层模块不应该依赖低层模块,两个都应该依赖抽象。
  • 抽象不应该依赖细节,细节应该依赖抽象。

先用人话解释一下这两句话中的一些抽象概念:

  • 高层模块:可以理解为上层应用,就是业务层的实现
  • 低层模块:可以理解为底层接口,比如封装好的API、动态库等
  • 抽象:指的就是抽象类或者接口,在C++中没有接口,只有抽象类

先举一个高层模块依赖低层模块的例子

大聪明的项目组接了一个新项目,低层使用的是MySql的数据库接口,高层基于这套接口对数据库表进行了添删查改,实现了对业务层数据的处理。而后由于某些原因,要存储到数据库的数据量暴增,所以更换了Oracle数据库,由于低层的数据库接口变了,高层代码的数据库操作部分是直接调用了低层的接口,因此也需要进行对应的修改,无法实现对高层代码的直接复用,大聪明欲哭无泪。

通过上面的例子可以得知,当依赖的低层模块变了就会牵一发而动全身,如果这样设计项目架构,对于程序猿来说,其工作量无疑是很重的。

如果要搞明白这个案例的解决方案以及抽象和细节之间的依赖关系,需要先了解另一个原则 — 里氏代换原则。

里氏代换原则

所谓的里氏代换原则就是子类类型必须能够替换掉它们的父类类型。

关于这个原理的应用其实也很常见,比如在Qt中,所有窗口类型的类的构造函数都有一个QWidget*类型的参数(QWidget 类是所有窗口的基类),通过这个参数指定当前窗口的父对象。虽然参数是窗口类的基类类型,但是我们在给其指定实参的大多数时候,指定的都是子类的对象,其实也就是相当于使用子类类型替换掉了它们的父类类型。

这个原则的要满足的第一个条件就是继承,其次还要求子类继承的所有父类的属性和方法对于子类来说都是合理的。关于这个是否合理下面举个栗子:

比如,对于哺乳动物来说都是胎生,但是有一种特殊的存在就是鸭嘴兽,它虽然是哺乳动物,但是是卵生。

如果我们设计了两个类:哺乳动物类和鸭嘴兽类,此时能够让鸭嘴兽类继承哺乳动物类吗?答案肯定是否定的,因为如果我们这么做了,鸭嘴兽就继承了胎生属性,这个属性和它自身的情况是不匹配的。如果想要遵循里氏代换原则,我们就不能让着两个类有继承关系。

如果我们创建了其它 的胎生的哺乳动物类,那么它们是可以继承哺乳动物这个类的,在实际应用中就可以使用子类替换掉父类,同时功能也不会受到影响,父类实现了复用,子类也能在父类的基础上增加新的行为,这个就是里氏代换原则。

上面在讲依赖倒转原则的时候说过,抽象不应该依赖细节,细节应该依赖抽象。也就意味着我们应该对细节进行封装,在C++中就是将其放到一个抽象类中(C++中没有接口,不能像Java一样封装成接口),每个细节就相当于上面例子中的哺乳动物的一个特性,这样一来这个抽象的哺乳动物类就成了项目架构中高层和低层的桥梁,将二者整合到一起。

  • 抽象类中提供的接口是固定不变的
  • 低层模块是抽象类的子类,继承了抽象类的接口,并且可以重写这些接口的行为
  • 高层模块想要实现某些功能,调用的是抽象类中的函数接口,并且是通过抽象类的父类指针引用其子类的实例对象(用子类类型替换父类类型),这样就实现了多态。

基于依赖倒转原则将项目的结构换成上图的这种模式之后,低层模块发生变化,对应高层模块是没有任何影响的,这样程序猿的工作量降低了,代码也更容易维护(说白了,依赖倒转原则就是对多态的典型应用)。

23种模式的功能

  1. 单例模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例。
  2. 原型模式:将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。
  3. 工厂方法模式:定义一个用于创建产品的接口,由子类决定生产什么产品。
  4. 抽象工厂模式:提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。
  5. 建造者模式:将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。
  6. 代理模式:为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
  7. 适配器模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
  8. 桥接模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
  9. 装饰模式:动态的给对象增加一些职责,即增加其额外的功能。
  10. 外观模式:为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。
  11. 享元模式:运用共享技术来有效地支持大量细粒度对象的复用。
  12. 组合模式:将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。
  13. 模板方法模式:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
  14. 策略模式:定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的改变不会影响使用算法的客户。
  15. 命令模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。
  16. 职责链模式:把请求从链中的一个对象传到下一个对象,直到请求被响应为止。通过这种方式去除对象之间的耦合。
  17. 状态模式:允许一个对象在其内部状态发生改变时改变其行为能力。
  18. 观察者模式:多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。
  19. 中介者模式:定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解。
  20. 迭代器模式:提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
  21. 访问者模式:在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。
  22. 备忘录模式:在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。
  23. 解释器模式:提供如何定义语言的文法,以及对语言句子的解释方法,即解释器。