1 | 设计模式的诞生与发展
模式(Pattern
)起源于建筑业而非软件业,模式之父——美国加利福尼亚大学环境结构中心研究所所长 Christopher Alexander
博士用了大约 20
年的时间,对舒适住宅和周边环境进行了大量的调查和资料收集工作,发现人们对舒适住宅和城市环境存在一些共同的认同规律,并将其归纳为 253
个模式 ==》《A Pattern Language:Towns,Buildings,Construction
》(中文译本名为《建筑模式语言:城镇 · 建筑 · 构造》);
A pattern is a successful or efficient solution to a recurring problem within a context
.- 模式是在特定环境下人们解决某类重复出现问题的一套成功或有效的解决方案。
1.1 软件模式的概述
GoF(Gang of Four,GoF分别是 Erich Gamma,Richard Helm,Ralph Johoson 和 John Vlissides) 4 位著名的软件工程学者,他们在 1994 年归纳发表了 23 种在软件开发中使用频率较高的设计模式,旨在用模式来统一沟通方面面向对象方法在分析、设计和实现间的鸿沟。
GoF 将模式的概念引入到软件工程领域,这标志着软件模式的诞生。软件模式是将模式的概念应用于软件开发领域,即软件开发的总体指导思想或参照样板。软件模式并非仅限于设计模式,还包括架构模式、分析模式和过程模式等,实际上,在软件生存周期的每一个阶段都存在这一些被认同的模式。
软件模式基本结构:
1.2 软件模式的发展
软件模式的发展(不是重点),此处省略,有兴趣的同学自行百度科普下;
2 | 设计模式的定义与分类
2.1 设计模式的定义
设计模式
是在特定环境下为解决某一通用软件设计问题提供的一套定制的解决方案,该方案描述了对象和类之间的相互作用。Design patterns
are descriptions of communicating objects and classes that are customized to solve a general design problem in a particular context.
2.2 设计模式的基本要素
设计模式一般包含模式名称、问题、目的、解决方案、效果、实例代码和相关设计模式等基本要素,下面介绍 4 个最关键的要素:
- 【模式名称(Patten Name)】通过词汇来描述模式的问题、解决方案和效果,以便用户更好地理解模式并方便开发人员之间的交流,绝大多模式都是根据其功能或模式结构来命名的。
- 【问题(Problem)】描述了应该在何时使用模式,包含了原始模式设计中存在的问题以及问题存在的原因。
- 【解决方案(Solution)】描述了设计模式的组成成分,以及这些组成成分之间的相互关系,各自的职责和协作方式。
- 【效果(Consequences)】描述了模式的应用情况以及使用模式时应权衡的问题。效果主要包括模式的优缺点分析以及模式的适用环境,没有任何一个模式是 100% 完美的,在使用设计模式时需要进行合理的评价和选择。
2.3 设计模式的分类
设计模式一般有以下两种分类:根据目的分类 & 根据范围分类。
范围 \ 目的 | 创建型模式 | 结构型模式 | 行为型模式 |
类模式 | 工厂方法模式 | (类)适配器模式 | 解释器模式 模板方法模式 |
对象模式 | 抽象工厂模式 建造者模式 原型模式 单例模式 |
(对象)适配器模式 桥接模式 组合模式 装饰器模式 外观模式 享元模式 代理模式 |
职责链模式 命令模式 迭代器模式 中介者模式 备忘录模式 观察者模式 状态模式 策略模式 访问者模式 |
下面简单的对 GoF
的 23
种设计模式进行说明:
模式类别 | 模式名称 | 模式说明 |
创建型模式 (Creational Pattern) |
抽象工厂模式 (Abstract Factory Pattern) |
提供一个创建系列相关或相互依赖对 象的接口,而无须指定它们具体的类 |
建造者模式 (Builder Pattern) |
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示 | |
工厂方法模式 (Factory Method Pattern) |
定义一个用于创建对象的接口,但是让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类 | |
原型模式 (Prototype Pattern) |
使用原型实例指定待创建对象的类型,并且通过复制这个原型来创建新的对象 | |
单例模式 (Singleton Pattern) |
确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例 | |
结构型模式 (Structural Pattern) |
适配器模式 ( Adapter Pattern) |
将一个类的接口转换成客户希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作 |
桥接模式 ( Bridge Pattern) |
将抽象部分与它的实现部分解耦,使得两者都能够独立变化 | |
组合模式 (Composite Pattern) |
组合多个对象形成树形结构以表示具有部分一整体关系的层次结构。组合模式让客户端可以统一对待单个对象和组合对象 | |
装饰模式 (Decorator Pattern) |
动态地给一个对象增加些额外的职责。 就扩展功能而言,装饰模式提供了一种比使用子类更加灵活的替代方案 | |
外观模式 (Facade Pattern) |
为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这_子系统更加容易使用 | |
享元模式 (Flyweight Pattern) |
运用共享技术有效地支持大量细粒度对象的复用 | |
代理模式 (Proxy Pattern) |
给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问 | |
行为型模式 (Behavioral Pattern) |
职责链模式 (Chain of Responsibility Pattern) |
避免将一个请求的发送者与接收者耦合在一起,让多个对象都有机会处理请求;将接收请求的对象连接成一条链,并且沿着这条链传递请求,直到有一个对象能够处理它为止 |
命令模式 (Command Pattern) |
将一个请求封装为一个对象,从而可用不同的请求对客户进行参数化,对请求排队或者记录请求日志,以及支持可撤销的操作 | |
解释器模式 (Interpreter Pattern) |
给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子 | |
迭代器模式 (Iterator Pattern) |
提供一种方法顺序访问一个聚合对象中的各个元素,而又不用暴露该对象的内部表示 | |
中介者模式 (Mediator Pattern) |
定义一个对象来封装一系列对象的交互。 中介者模式使各对象之间不需要显式地相互引用,从而使其耦合松散,而且让你可以独立地改变它们之间的交互 | |
备忘录模式 ( Memento Pattern) |
在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态 | |
观察者模式 (Observer Pattern) |
定义对象之间的一种-对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新 | |
状态模式 (State Pattern) |
允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类 | |
策略模式 (Strategy Pattern) |
定义一系列算法,将每一个算法封装起来,并让它们可以相互替换,策略模式让算法可以独立于使用它的客户变化 | |
模板方法模式 (Template Method Pattern) |
定义一个操作中算法的框架,而将-些步骤延迟到子类中。模板方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步据 | |
访问者模式 ( Visitor Pattern) |
表示一个作用于某对象结构中的各个元素的操作。访问者模式让你可以在不改变各元索的类的前提下定义作用于这些元素的新操作 |
2.4 设计模式的优点
- 融合了众多专家的经验,并以一种标准的形式供广大开发人员所用,它提供了一套通用的设计词汇和一种通用的语言,以方便开发人员之间进行沟通和交流,使得设计方案更加通俗易懂。使用不同编程语言的开发和设计人员可以通过设计模式来交流系统设计方案,每一个设计模式都对应一个标准的解决方案,设计模式可以降低开发人员的理解系统的复杂度。
- 让人们可以更加简单方便地复用成功的设计和体系结构,可是新系统人员更加容易理解其设计思路。设计模式使得重用功能的设计更加容易,并避免导致不可重复的设计方案。
- 使得设计方案更加灵活,且易于修改。很多设计模式中广泛的使用了面向对象的设计原则,使得系统具有较好的可维护性。
- 提高软件系统的开发效率和开发质量,且在一定的程度上节约设计成本。
- 有助于初学者更深入的理解面向对象的思想,一方面可以帮助初学者更加方便的阅读和学习现有类库与其他系统中的源代码,另一方面还可以提高软的件设计水平和代码质量。
3 | 面向对象的设计原则
设计原则名称 | 定义 | 使用频率 |
单一职责原则 (Single Responsibility Principle,SRP) |
一个对象应该只包含单一的职责,并且该职责被完整地封装在一 个类中 | ★★★★ |
开闭原则 (Open- Closed Principle,OCP) |
软件实体应当对扩展开放,对修改关闭 | ★★★★★ |
里氏代换原则 (Liskov Substitution Principle,LSP) |
所有引用基类的地方必须能透明地使用其子类的对象 | ★★★★★ |
依赖倒转原则 (Dependence Inversion Principle,DIP) |
高层模块不应该依赖低层模块,它们都应该依赖抽象。 抽象不应该依赖于细节,细节应该依赖于抽象 | ★★★★★ |
接口隔离原则 (Interface Segregation Principle,ISP) |
客户端不应该依赖那些它不需要的接口 | ★★ |
合成复用原则 (Composite Reuse Principle,CRP) |
优先使用对象组合,而不是继承来达到复用的目的 | ★★★★ |
迪米特法则 (Law of Dermeter,LoD) |
每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位 | ★★★ |
3.1、开闭原则/OCP:
当应用的需求改变时,在不修改软件实体的源代码或者二进制代码的前提下,可以扩展模块的功能,使其满足新的需求。
- 作用:
- 方便测试;测试时只需要对扩展的代码进行测试。
- 提高代码的可复用性;粒度越小,被复用的可能性就越大。
- 提高软件的稳定性和延续性,易于扩展和维护。
- 实现方式:
通过“抽象约束. 封装变化”来实现开闭原则。通过接口或者抽象类为软件实体定义一个相对稳定的抽象层,而将相同的可变因素封装在相同的具体实现类。
3.2、里氏替换原则/LSP:
继承必须确保超类所拥有的性质在子类中仍然成立。子类可以扩展父类的功能,但不能改变父类原有的功能。
- 作用:
- 克服了继承中重写父类造成的可复用性变差的缺点。
- 保证了动作正确性。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
- 实现方式:
继承,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
3.3、依赖倒置原则/DIP:
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。其核心思想是:要面向接口编程,不要面向实现编程。
- 作用:
- 依赖倒置原则可以降低类间的耦合性。
- 依赖倒置原则可以提高系统的稳定性。
- 依赖倒置原则可以减少并行开发引起的风险。
- 依赖倒置原则可以提高代码的可读性和可维护性。
- 实现方式:
- 每个类尽量提供接口或抽象类,或者两者都具备。
- 变量的声明类型尽量是接口或者是抽象类。
- 任何类都不应该从具体类派生。
- 使用继承时尽量遵循里氏替换原则。
3.4、单一职责原则/SRP:
一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分。核心就是控制类的粒度大小. 将对象解耦. 提高其内聚性
- 作用:
- 降低类的复杂度。一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多。
- 提高类的可读性。复杂性降低,自然其可读性会提高。
- 提高系统的可维护性。可读性提高,那自然更容易维护了。
- 变更引起的风险降低。变更是必然的,如果单一职责原则遵守得好,当修改一个功能时,可以显著降低对其他功能的影响。
- 实现方式:
3.5、接口隔离原则/ISP:
客户端不应该被迫依赖于它不使用的方法,一个类对另一个类的依赖应该建立在最小的接口上
- 作用:
- 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
- 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
- 如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
- 使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
- 能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。
- 实现方式:
- 接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。
- 为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。
- 了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同深入了解业务逻辑。
- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
3.6、迪米特法则/Lod:
如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,通过第三方转发调用降低类之间的耦合度,提高模块的相对独立性
- 作用:
- 降低了类之间的耦合度,提高了模块的相对独立性。
- 由于亲合度降低,从而提高了类的可复用率和系统的扩展性。
- 实现方式:
3.7、合成复用原则/CRP:
在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
- 作用:
- 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
- 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
- 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
- 实现方式:
将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用。
总结
对于 7
种设计原则其各自的侧重点不同。其中,开闭原则是总纲,它告诉我们要对扩展开放,对修改关闭;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;单一职责原则告诉我们实现类(方法)要职责单一;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合度;合成复用原则告诉我们要优先使用组合或者聚合关系复用,少用继承关系复用。