设计模式基础


简介

设计模式(Design Pattern)代表了最佳的实践,在面向对象的编程中被很多老鸟们反复使用。使用设计模式有很多好处:

  • 可重用代码
  • 保证代码可靠性
  • 使代码更易被他人理解

毫无疑问,设计模式于己、于人、于系统都是多赢的。《设计模式》之于程序员,就好比《圣经》之于耶稣信徒一样,意义可想而知!

| 版权声明:一去、二三里,未经博主允许不得转载。

什么是 GoF

谈及设计模式,必然离不开 GoF:

GoF:Gang of Four,也称为“四人组”,即:EErich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 四人。

1994 年,这几位大牛合著出版了一本名为《Design Patterns: Elements of Reusable Object-Oriented Software》(即:《设计模式》)的书。该书首次提到了软件开发中设计模式的概念,将设计模式提升到理论高度,并将之规范化。书中提及了 23 种基本设计模式,时至今日,在可复用面向对象软件的发展过程中,新的设计模式仍然不断出现。

这就是传说中的“风尘四侠”,比起国外的其他技术大咖(不修边幅),看起来要好很多O(∩_∩)O。

设计模式的类型

根据《设计模式》参考书,共有 23 种设计模式,这些模式可以分为三类:

类型 描述
创建型模式(Creational Patterns) 用于构建对象,以便它们可以从实现系统中分离出来。
结构型模式(Structural Patterns) 用于在许多不同的对象之间形成大型对象结构。
行为型模式(Behavioral Patterns) 用于管理对象之间的算法、关系和职责。

设计模式是从众多优秀的软件系统中总结出的成功的、能够实现可维护性复用的设计方案,使用这些方案将避免一些重复性工作,高效设计出高质量的软件系统。总的来说,设计模式主要有以下优点:

  • 设计模式融合了众多专家的经验,以一种标准的形式供广大开发人员使用,通俗的设计词汇和通用的语言方便开发人员交流和学习;
  • 设计模式使人们可以更简单方便地复用成功的设计,使新开发者更容易理解设计思路;
  • 设计模式使设计方案更加灵活、易于修改;
  • 设计模式的使用将提高软件系统的开发效率和软件质量,节约开发成本;
  • 设计模式有助于初学者深入理解面向对象思想。

2.设计模式作用和分类

从上述设计模式的定义可以初步看出设计模式的作用,即:

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

  • 设计模式是从许多优秀的软件系统中总结出的成功的、能够实现可维护性复用的设计方案,可避免重复性工作
  • 设计模式提供了一套通用的设计词汇和一种通用的形式来方便开发人员之间沟通,使得设计方案更加通俗易懂
  • 大部分设计模式兼顾了系统的可重用性和可扩展性,这使得我们可以更好地重用一些已有的设计方案、功能模块甚至一个完整的软件系统,避免我们经常做一些重复的设计、编写一些重复的代码
  • 有助于初学者更加深入地理解面向对象思想

狭义的设计模式一般分为3大类共计23种(广义上说,随着软件技术的逐步发展,不断有新的设计模式被总结出来并应用到工程实践中),如下表。其中,创建型模式关注对象的创建过程,结构性模式关注如何将现有类或对象组织在一起形成更加强大的结构,行为型模式关注系统中对象之间的交互研究系统在运行时对象之间的相互通信与协作,进一步明确对象的职责

类型 种类
创建型模式 单例模式,简单工厂模式,抽象工厂模式,工厂方法模式,原型模式,建造者模式
结构型模式 适配器模式,桥接模式,组合模式,装饰模式,外观模式,享元模式,代理模式
行为型模式 职责链模式,命令模式,解释器模式,迭代器模式,中介者模式,备忘录模式,观察者模式,状态模式,策略模式,模板方法模式,访问者模式

UML 类图

UML 时序图

设计模式原则

1.单一职责原则

单一职责原则:

定义1:一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中

定义2:就一个类而言,应该仅有一个引起它变化的原因。

首先需要知道两个原则:高内聚低耦合

高内聚:内聚是对软件系统中元素职责相关性和集中度的度量。如果元素具有高度相关的职责,除了这些职责内的任务,没有其它过多的工作,那么该元素就具有高内聚性;反之则成为低内聚性。

低耦合:耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及通过接口的数据

单一职责原则用于控制类的力度大小。软件设计过程中,如果一个类承担的职责越多,那么它被复用的可能性越小。(为什么?想想,如果一个类有许多接口,另一个类想复用其中一两个接口,还不如重新实现!)。另一方面,如果一个类承担的职责越多,各个职责耦合在一起,修改其中一个职责可能“牵一发而动全身”。因此,应该将这些职责进行分离,不同的职责封装在不同的类中。

2.开闭原则

开闭原则:软件实体应对扩展开放,对修改关闭。

开闭原则指软件实体(一个软件模块、一个由不同类组成的局部结构或一个独立的类) 应该在不修改原有代码的基础上进行扩展。软件设计过程中,需求可能会随时变化,需要根据需求扩展已有的设计。如果原有的设计符合开闭原则,那么扩展起来就比较安全(不会影响原有功能,稳定)和方便(易于扩展)。开闭原则的关键在于抽象化。可以为系统定义一个相对较为稳定的抽象层,将不同的实现行为放到具体的实现层中完成。

举个例子:要设计一个计算器的类Computer,包含加、减两个功能,很自然的想法是在类Computer里声明并实现Add和Sub两个方法。那么如果要求再增加乘法功能,是不是要在Computer里增加Mul的方法呢?这就违背了开闭原则。

3.里氏代换原则

历史替换原则:所有引用基类的地方必须能透明地使用其子类的对象。

在软件中,如果用子类对象来替换基类对象,程序将不会产生任何异常和问题,反过来不成立。为什么?很好理解,子类继承自基类,基类有的成员方法和成员属性,子类全都有;而子类可以增加新的方法和属性,所以反过来不成立。

里氏代换原则的指导意义在于:尽可能地使用基类类型来对对象进行定义,而在运行时再确定子类类型,然后用子类对象替换父类对象设计时应将父类设计为抽象类或者接口,子类继承父类并实现在父类中声明的方法;运行时子类实例(对象)替换父类实例(对象),可以很方便地扩展系统功能。

4.依赖倒转原则

依赖倒转原则:高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。

什么是高层,什么是低层呢?它们指的是继承(派生)中的基类子类关系,基类或者越抽象的类,层次越高。简单说,依赖倒转原则要求针对接口编程,不要针对实现编程

依赖倒转原则要求再程序代码中传递参数时,或在关联关系中,尽量引用层次高的出现层类,即使用接口或抽象类来声明变量类型、参数类型声明、方法返回类型声明,以及数据类型转换等,而不要使用具体类来做这些事情。(其实这一点也符合里氏代换原则的指导意义,即对一个方法而言,返回基类的地方一定可以返回子类)。同样,依赖倒转原则设计的关键也在与抽象化设计。

5.接口隔离原则

接口隔离原则:客户端不应该依赖那些它不需要的接口。

当一个接口太大时,应该将它根据需要分割成多个更细小的接口,每个接口仅承担一个相对独立的角色或功能,使用该接口的客户端仅需知道与之相关的方法即可。 但是,接口不能过小,否则系统中接口太多,不利于维护。一般而言,在接口中仅包含为某一类用户定制的方法即可。

6.合成复用原则

合成复用原则:优先使用对象组合,而不是通过继承来达到复用的目的。

根据UML类图关系,合成复用原则指导在软件设计时,优先使用关联、聚合和组合关系,尽量少用泛化(继承)。对象组合可以使系统更加灵活(黑箱复用),降低类与类之间的耦合度,一个类的变化尽可能不影响其他类(父类和子类耦合度高不高?)。如果要使用继承,则需考虑里氏代换原则和依赖倒转原则。继承关系会破坏系统的封装性,会将基类的实现细节暴露给子类(白箱复用),如果基类发生改变,那么子类的实现也不得不改变。

7.迪米特法则

迪米特法则:每一个软件单位对其他单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位

迪米特法则要求一个软件实体应当尽可能少地与其他实体发生相互作用。如果一个系统负荷迪米特法则,那么当修改其中某一个模块时就会尽量少地影响其他模块。应用迪米特法则可以降低系统的耦合度。在类的设计上应该注意以下几点:在类的划分上应尽量创建松耦合的类,类之间的耦合度越低,越有利于复用;类的结构设计上,每一个类都应该降低其成员变量和成员函数的访问权限。