软件设计的好坏评判标准之一是复用性。
变化与稳定
实际开发中最大的困难就是需求的多变,而变化是软件复用的天敌。面向对象的设计最大的优势是“抵御变化”,注意不是“消灭变化”,而是把变化像小兔子一样锁在笼子里限制起来。 设计模式是软件设计中常见问题的典型解决方案。它们就像能根据需求进行调整的预制蓝图,可用于解决代码中反复出现的设计问题。[1] 设计模式与方法或库的使用方式不同,你很难直接在自己的程序中套用某个设计模式。 模式并不是一段特定的代码,而是解决特定问题的一般性理念。 你可以根据模式来实现符合自己程序实际所需的解决方案,所以最佳的实践设计模式的方法是重构到模式(Refactory to Patterns)。当然如果你非常熟悉各种模式的应用场景,也可以一上来就按照适当的设计模式去开发。
1994年,GoF在他们的书中总结了23种设计模式,此后人们又陆续发现了几十种面向对象的设计模式,“模式方法” 开始在其他程序开发领域中流行起来。 如今,在面向对象设计领域之外,人们也提出了许多其它的模式。设计模式是针对软件设计中常见问题的工具箱,其中的工具就是各种经过实践验证的解决方案。无疑这些设计模式提高了在团队中的沟通效率,我们只要说在这种情况下,你使用工厂模式就可以了,不需要过多的解释。 但是模式不是金科玉律,千万不要为了用模式而去套模式。在实际项目中应该实事求是的对项目进行调整。况且,模式也是会过时。以前广泛使用的模式可能在新一代的语言中内化为语言的设计理念。用设计模式的时候最重要的是分清工程里哪些是变化的,哪些是不变的。把变化的那部分用设计模式的方案来改造一下。试想两个极端:假如我们的工程里面所有模块都是变化的,那用什么设计模式都救不了你。再譬如,我们的工程里所有模块都是稳定的,那你也没有必要去用设计模式了,直接写代码就完事了。[2] 因此,分清变与不变最重要,设计模式主要着力点就是那些变化的地方。
八大设计原则
比模式更重要的是设计原则,下面介绍一下八大原则: [2]
- 依赖倒置原则(DIP)
- 高层模块(稳定)不应该依赖低层模块(变化),二者都应该依赖抽象(稳定)。
- 抽象(稳定)不应该依赖于实现细节(变化),实现细节应该依赖于抽象(稳定)。
- 开放封闭原则(OCP)
- 对扩展开放,对更改封闭。
- 类模块应该是可扩展的,但是不可修改。
- 单一职责原则(SRP)
- 一个类应该仅有一个引起它变化的原因。
- 变化的方向隐含着类的责任。
- 里氏替换原则(LSP)
- 子类必须能够替换它们的基类(IS-A)。
- 继承表达类型抽象。
- 接口隔离原则(ISP)
- 不应该强迫客户程序依赖它们不用的方法。
- 接口应该小而完备。
- 优先使用对象组合,而不是类继承
- 类继承通常为“白箱复用”,对象组合通常为“黑箱复用”。
- 继承在某种程度上破坏了封装性,子类父类耦合度高,而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低。
- 封装变化点
- 使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合。
- 针对接口编程,而不是针对实现编程
- 不将变量类型声明为某个特定的具体类,而是声明为某个接口。
- 客户程序无需获知对象的具体类型,只需要知道对象所具有的接口。
- 减少系统中各部分的依赖关系,从而实现“高内聚、松耦合”的类型设计方案。
二十三个设计模式
模式名称 | 应用场景 |
---|---|
模板方法模式 | 常用于框架设计,例如 Windows 的 MFC |
策略模式 | 代码中如果if else 很多的情况下考虑用这个模式 |
观察者模式 | 如果一个事件发生后,需要很多对象都能对此作出响应,则考虑观察者模式 |
装饰模式 | 动态(组合)地给一个对象增加一些额外的职责 |
桥模式 | 将抽象部分(业务功能)与实现部分(平台实现)分离,使它们都可以独立地变化 |
工厂方法 | 提供一种 “封装机制” 来避免客户程序和这种 “具体对象创建工作” 的紧耦合 |
抽象工厂 | 提供一个接口,让该接口负责创建一系列 “相关或者相互依赖的对象”,无需指定它们具体的类 |
构建者模式 | Builder 模式主要用于 “分步骤构建一个复杂的对象”。在这其中 “分步骤” 是一个稳定的算法,而复杂对象的各个部分则经常变化 |
单件模式 | 软件系统中,有些特殊的类必须保证它们在系统中只存在一个实例,才能确保它们的逻辑正确性,以及良好的效率 |
享元模式 | 运用共享技术有效的支持大量细粒度的对象 |
门面模式 | 为子系统中一组接口提供一致(稳定)的界面,facade模式定义了一个高层接口,这个接口使得这个子系统更容易的复用 |
代理模式 | 为其他对象提供一种代理以控制(隔离)对这个对象的访问 |
适配器模式 | 将一个类的接口转换成客户希望的另一个接口,一般用在新老模块共同复用的情况 |
中介模式 | 多个对象相互关联交互,对象之间维持一个复杂的引用关系。这种情况下,使用一种中介对象来管理对象的关联关系,避免紧耦合关系 |
状态模式 | 出现需要状态机的场景,此模式与策略模式有点相似 |
备忘录 | 此模式把对象的内部状态存储下来,由于现代语言都提供序列化的特性,此模式已过时 |
组合模式 | 将对象组合成树形结构以表示 “部分/整体” 的层次结构,使得用户对单个对象和组合对象的使用具有一致性。 |
迭代器 | 顺序访问聚合对象中的各个元素,而又不暴露该对象的内部表示。此模式现在已经被泛型编程中的迭代器取代了。 |
职责链模式 | 当某种请求发生时,需要多个模块对此请求作出处理,把各个模块串成链表,依次处理该请求 |
命令模式 | 将一个请求(行为)封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销操作。 |
访问器模式 | 某些类层次结构中需要常常要增加新的行为方法,如果直接在基类中修改,会给子类增加严重负担。 运行时透明的为各层次结构上的各个类添加新操作 |
解释器模式 | 某一个特定领域的问题比较复杂,将特定领域的问题表达为某个语法规则下的句子,然后构建一个解释器来解释句子,从而达到解决问题的目的 |
重构技法
- 静态转动态
- 早绑定转晚绑定
- 继承转组合
- 编译时依赖转运行时依赖
- 紧耦合转松耦合
什么时候不用模式
- 代码可读性很差时(这时应该首先改善代码的可读性,用模式救不了你,运用设计模式是在可读性问题解决后的事情)
- 需求理解很浅时
- 变化没有显现时 (不要提前过度设计,等到变化的方向明确时,设计模式才不会用错)
- 不是系统的关键依赖点
- 项目没有复用价值时 (一般外包公司的项目都是这种类型,所以外包公司的开发人员不关心设计模式)
- 项目即将发布时 (设计模式是为了更好的维护代码,没有必要在发布前还引入未知bug)
经验之谈
- 不要为了模式而模式
- 关注抽象类和接口
- 理清变化点和稳定点
- 审视依赖关系
- 要有 Framework 和 Application 的区分思维
- 良好的设计是演化的结果
最重要的事情
你可以忘记所有的设计模式,但是设计原则必须牢记于心。 所有的模式都是对着这八条原则,在不同的场景下推演出来的。 所有的模式实际上全部是依靠多态这个特性来实现的。 后面几个章节将介绍一下 C 语言开发中常用的设计模式,注意不是 C++ 语言。C 语言同样可以做到面向对象设计。只不过需要开发者自己做一些基础设施。