使用设计模式的目的
在软件的开发和维护中,需要不断地更新产品的源代码以适应新的市场环境和客户需求。在源代码中使用设计模式可以十分有效地提高旧代码的复用率、降低维护成本。
旧代码的复用率是指更新软件后,编译、部署二进制文件时,未改变的部分所占的比重,即未修改的源文件个数占比。而不是重复代码的占比。
通常,一个软件产品中既有稳定(软件更新时不改变)的部分,又有变化(软件更新时可能会改变)的部分。使用设计模式的目的就是要隔离这两个部分,把变化装在笼子里,当软件更新的时候,只需扩展或修改变化部分的源代码,提高复用率、降低成本。因此在使用设计模式之前,要明确软件中哪些是稳定的,哪些是变化的。
因此,在两种极端情况下,不需要在软件中使用设计模式:
(1)
要求一个软件只需完成相应的功能即可,不考虑后续的更新和维护,那么该软件就是绝对稳定的,不存在变化的部分。
(2)
软件是绝对变化的,每一版千差万别,直接重写,不存在稳定的部分。
设计原则
明确几个概念的含义:
稳定等于抽象等于接口
变化等于具体等于实现等于细节
设计模式围绕以下几个原则:
(1)
依赖倒置
稳定模块不能依赖变化模块,两者都要依赖稳定模块。
当变化模块发生改变时,稳定模块不需要修改。
(2)
开放封闭
代码对扩展开放,对修改封闭。
当更新软件时,可以增加新的源文件,但不能修改已有的源文件。
也可以理解为不能修改稳定部分的源文件
(3)
单一职责
每个类的功能单一,仅有一个使其变化的原因(只存在一个变化的方向)。
(4)
里氏(Liskov)替换
子类能够替换父类,是is a
的关系,不能在子类中删除父类的方法。
(5)
接口隔离
类的接口少而完备,不依赖多余的方法。
(6)
优先使用组合而不是继承
子类与父类的耦合度很高,非必要不要用继承,而应该使用组合类对象或基类指针。
(7)
封装变化点
创建对象之间的分界层,一侧是稳定的,一侧是变化的
(8)
针对接口,而不是实现编程
接口是稳定的,面向接口编程能提高代码复用率。
(9)
关键技法
静态绑定转动态绑定
早绑定转晚绑定
继承转组合
编译时依赖转运行时依赖
紧耦合转松耦合
常用设计模式介绍
明确几个概念的含义:
抽象基类或基类是稳定的。
子类是变化的(通过虚函数)。
设计模式就是在提取抽象,剥离稳定和变化。
C++中,虚函数是变化方法,其它的是稳定方法。
虚函数所实现的运行时多态是剥离稳定和变化的关键。
(1)
模板方法
某个任务的执行流程是稳定的,但流程中调用的方法是变化的,将它们的实现延迟到子类。
将该流程写到基类,流程中变化的方法作为虚函数,可在子类中重写。
方法延迟到子类:将方法在基类中声明为纯虚函数,使用多态调用它在子类中的实现。
(2)
观察者/事件模式
多个观察者对象观察某个其他对象,当某个事件发生的时候,被观察的对象使用统一的接口、不同的实现方式通知它们。
这里的接口是观察者基类中的稳定接口,每个观察者在各自的子类中有自己的实现。
(3)
装饰模式
为了不破坏类的单一职责,在给某个类增加多个变化方向的扩展时,让扩展类和主体类都去继承某个公共基类。扩展类中包含一个基类指针(组合),指向其他主体类或扩展类。
基类指针能很好地实现多态。
(4)
桥接模式
将抽象部分与它的实现部分分离开来,使他们都可以独立变化。接口部分包含一个指向实现部分的指针(组合)。
相当于是将某个原始类按照变化方向,分割成了两个不同的类继承体系。
(5)
工厂模式
避免在主流程中使用new
或别的和具体类相关的操作创建对象。使用统一的工厂基类指针和方法创建对象。
尽量多使用稳定、抽象的接口(和具体类无关)。
(6)
构建器模式
某个类对象的创建过程比较复杂,但创建流程固定或变化有规律,将该创建流程提取出一个类XXXBuilder
,使用该类创建对象。
(7)
接口隔离
提供稳定的接口(抽象),系统/类/对象间使用该稳定接口进行交互。
门面模式:系统间的接口隔离。
代理模式:两个对象间的接口隔离。
适配器模式:新接口和老接口的隔离。
中介者模式:系统内大量对象间的接口隔离。(参考交换机的原理)
(8)
访问者模式
两次多态,visitor
类为每个目标子类定义一个方法。目标子类个数等于visitor
类中的方法数。
可使用访问者模式或状态模式实现有限状态机。
总结
想设计出优秀的软件,要做到:
(1)
寻找变化点,分离稳定部分和变化部分。
(2)
什么时候使用设计模式比理解具体设计模式的结构更重要。
(3)
不要误用、滥用设计模式。
(4)
XXX模式是具体,设计原则是抽象,要依赖抽象而非具体。不要拘泥于某个具体的设计模式。
推荐阅读:
《设计模式:可复用面向对象软件的基础》
《重构:改善既有代码的设计》
《重构与模式》