目录
1. 设计模式简介
2. 面向对象设计原则
3. 模板方法
4. 策略模式
5. 观察者模式
6. 装饰模式
7. 桥模式
1. 设计模式简介
1.1 课程目标
- 理解松耦合设计思想
- 掌握面向对象设计原则
- 掌握重构技法改善设计
- 掌握 GOF 核心设计模式
1.2 什么是设计模式
每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动。
—— Christopher Alexander
一般而言,一个模式有四个基本要素:
- 模式名称(pattern name)
一个助记名,它用一两个词来描述模式的问题、解决方案和效果。 - 问题(problem)
描述了应该在何时使用模式。 - 解决方案(solution)
描述了设计的组成成分,他们之间的相互关系及各自的职责和协作方式。 - 效果(consequences)
描述了模式应用的效果及使用模式应权衡的问题。
软件设计复杂的根本原因:变化!
软件设计的目标:复用!
1.3 GOF-23 模式分类
-
从目的来看:
- 创建型(Creational)模式
将对象的部分创建工作延迟到子类或者其他对象,从而对应需求变化为对象创建时具体类型实现引来的冲击。 - 结构型(Structural)模式
通过类继承或者对象组合的方式来获得更灵活的结构,从而应对需求变化为对象的结构带来的冲击。 - 行为型(Behavioral)模式
通过类继承或者对象组合来划分类与对象间的职责,从而应对需求变化为多个交互的对象带来的冲击。
- 创建型(Creational)模式
-
从范围来看
- 类模式处理与子类的静态关系。
- 对象模式处理对象间的动态关系。
1.4 使用设计模式的方法
2. 面向对象设计原则
变化是复用的天敌!
面向对象设计最大的优势:抵御变化!
2.1 面向对象的原则
-
依赖倒置原则(DIP)
- 高层模块(稳定的)不应该依赖于低层模块(容易变化的),二者都应该依赖于抽象(稳定的)。
- 抽象(稳定的)不应该依赖于实现细节(容易变化的),实现细节应该依赖于抽象(稳定的)。
-
开放封闭原则(OCP)
- 对扩展开放,对更改封闭。
- 类模块应该是可扩展的,但是不可修改。
-
单一职责原则(SRP)
- 一个类应该仅有一个引起它变化的原因。
- 变化的方向隐含着类的责任。
-
Liskov 替换原则(LSP)
- 子类必须能够替换他们的基类(is-a)。
- 继承表达类型抽象。
-
接口隔离原则(ISP)
- 不应该强迫客户程序依赖他们不用的方法。
- 接口应该小而完备。
-
优先使用对象组合,而不是类继承
- 类继承通常为「白箱复用」,对象组合通常为「黑箱复用」。
- 继承在某种程度上破坏了封装性,子类父类耦合度高。
- 而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低。
-
封装变化点
- 使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合
-
针对接口编程,而不是针对实现编程
- 不将变量类型声明为某个特定的具体类,而是声明为某个接口。
- 客户程序无需获知对象的具体类型,只需要知道对象所具有的接口。
- 减少系统中个部分的依赖关系,从而实现「高内聚、松耦合」的类型设计方案。
产业强盛的标志:接口标准化!
2.2 将设计原则提升为设计经验
「组件协作」模式
3. 模板方法(template method)
笔记
既然用了继承,并且肯定这个继承有意义,就应该要成为子类的模板,所有重复的代码都应该要上升到父类去,而不是让每个子类都去重复。
当我们要完成在某一细节层次一致的一个过程或一系列步骤,但其个别步骤在更详细的层次上实现可能不同时,我们通常考虑用模板方法模式来处理。
模板方法模式是通过把不变行为搬移到基类,去除子类中的重复代码来体现它的优势。相当于提供了一个很好的代码复用平台。
当不变的和可变的行为在方法的子类实现中混合在一起的时候,不变的行为就会在子类中重复出现。我们通过模板方法模式把这些行为搬移到单一的地方,这样就帮助子类摆脱重复的不变行为的纠缠。
对于模板方法来说,有一个前提,就是 Template Method()必须要是稳定的。如果 Template Method()不稳定,那么没有一个稳定的软件的骨架,就不存在这样一种设计模式。假定,所有方式都是稳定,那么其实也没有必要使用该设计模式。
结构
要点总结
Template Method 模式是一种非常基础性的设计模式,在面向对象的系统中有着大量的应用。它用最简洁的机制(虚函数的多态性)为很多应用程序的框架提供了灵活的扩展点,是代码复用方面的基本实现结构。
除了可以灵活对应子步骤的变化外,「不要调用我,让我来调用你」的反向控制结构是 Template Method 的典型应用。
在具体实现方面,被 Template Method 调用的虚方法可以具有实现,也可以没有任何实现(抽象方法、纯虚方法),一般推荐将他们设置为 protected 方法。
4. 策略模式(strategy)
定义一系列算法,把他们一个个封装起来,并且是他们可以互相替换(变化)。该模式使得算法可独立于使用它的客户程序(稳定的)而变化(扩展,子类化)。
——《设计模式》GoF
动机
在软件构建的过程中,某些对象使用的算法可能多种多样,经常改变,如果将这些算法都编码到对象中,将会使对象变得异常复杂;而且有时候支持不使用的算法也是一种性能负担.
如何在运行时根据需要透明地更改对象的算法?将算法与对象本身解耦,从而避免上述问题?
笔记
所谓的「复用」指的是二进制层面(编译单位的复用),而不是代码的「复制+粘贴」。
面向对象的编程,并不是类越多越好,类的划分是为了封装,但分类的基础是抽象,具有相同属性和功能的对象的抽象的集合才是类。
策略模式是一种定义一系列算法的方法,从概念上来看,所有这些算法完成的都是相同的工作,只是实现不同,它可以以相同的方式调用所有的算法,减少了各种算法类与使用算法类之间的耦合。
策略模式的 Strategy 类层次为 Context 定义了一系列的可供重用的算法或行为。继承有助于析取处这些算法中的公共功能。
策略模式的另外一个优点是简化了单元测试,因为每个算法都有自己的类,可以通过自己的接口单独测试。
当不同的行为堆砌在一个类中时,就很难避免使用条件语句来选择合适的行为。将这些行为封装在一个个独立的 Strategy 类中,可以在使用这些行为的类中消除条件语句。
策略模式就是用来封装变化的,所以可以用它来封装几乎任何类型的规则,只要在分析过程中需要在不同时间应用不同的业务规则,就可以考虑使用策略模式处理这种变化的可能性。
在基本的策略模式中,选择所用具体实现的职责由客户端对象承担,并转给策略模式的 Context 对象。
任何需求的变更都是需要成本的。
结构
要点总结
Strategy 及其子类为组件提供了一系列可重用的算法,从而可以使得类型在运行时方便的根据需要在各个算法之间进行切换。
Strategy 模式提供了用条件判断语句以外的另一种选择,消除条件判断语句,就是在解耦合。含有许多条件判断语句的代码通常都需要 Strategy 模式。尤其是条件判断语句在未来会有增加可能性的时候,应该优先考虑 Strategy 模式。
如果 Strategy 对象没有实例变量,那么各个上下文可以共享同一个 Strategy 对象,从而节省对象的开销。
5. 观察者模式(Observer)
动机
在软件构建的过程中,我们需要为某些对象建立一种「通知依赖关系」—— 一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使得软件不能很好地抵御变化。
使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系,从而实现软件体系结构的松耦合。
笔记
多继承:继承一个基类,其它都是接口,比较合理;同时继承多个基类会有耦合问题,不太好。
将一个系统分割成一系列相互协作的类有一个很不好的副作用,那就是需要维护相关对象间的一致性。我们不希望为了维持一致性而使各类紧密耦合,这样会给维护、扩展和重用都带来不便。而观察者模式的关键对象是主题 Subject 和观察者 Observer,一个 Subject 可以有任意数目的依赖它的 Observer,一旦 Subject 的状态发生了改变,所有的 Observer 都可以得到通知。Subject 发出通知时并不需要知道谁是它的观察者,也就是说,具体观察者是谁,它根本不需要知道。而任何一个具体观察者不知道也不需要知道其他观察者的存在。
结构
要点总结
使用面向对象的抽象,Observer
模式使得我们可以独立地改变目标与观察者,从而使二者之间的依赖关系达到松耦合。目标发送通知时,无需指定观察者,通知(可以携带通知信息作为参数)会自动传播。
观察者自己决定是否需要订阅通知,目标对象对此一无所知。
Observer 模式是基于事件的 UI 框架中非常常用的设计模式,也是
MVC 模式的一个重要组成部分。
「单一职责」模式
- 在软件组件的设计中,如果责任划分的不清晰,使用继承得到的结果往往是随着需求的变化,子类急剧膨胀,同时充斥着重复代码,这时候的关键是划清责任。
- 典型模式
- Decorator
- Bridge
6. 装饰模式(Decorator)
动态(组合)地给一个对象增加一些额外的职责。就增加功能而言,Decorator 模式比生成子类(继承)更为灵活(消除重复代码 & 减少子类个数)。
——《设计模式》GoF
动机
在某些情况下,我们可能会「过度地使用继承来扩展对象的功能」,由于继承为类型引入的静态特质,使得这种扩展方式缺乏灵活性;并且随着子类的增多(扩展功能的增多),各种子类的组合(扩展功能的组合)会导致更多子类的膨胀。
如何使「对象功能的扩展」能够根据需要来动态地实现?同时避免「扩展功能的增多」带来的子类膨胀问题?从而使得任何「功能扩展变化」所导致的影响将为最低?
笔记
编译时复用,运行时利用多态支持变化。
装饰模式把每个要装饰的功能放在单独的类中,并让这个类包装它所要装饰的对象,因此,当需要执行特殊行为时,客户代码就可以在运行时根据需要有选择地、按顺序地使用装饰功能包装对象了。
装饰模式把类中的装饰功能从类中搬移去除,这样可以简化原有的类,有效地把类的核心职责和装饰功能区分开了,而且可以去除相关类中重复的装饰逻辑。
结构
要点总结
通过采用组合而非继承的手法,Decorator 模式实现了在运动时动态扩展对象功能的能力,而且可以根据需要扩展多个功能。避免了使用集成带来的「灵活性差」和「多子类衍生问题」。
Decorator 类在接口上变现为 is-a Component 的继承关系,即
Decorator 类继承了 Component 类所具有的接口。但在实现上又表现为 has-a Component 的组合关系,即 Decorator 类又使用了另外一个 Component 类。Decorator 模式的目的并非解决「多子类衍生的多继承」问题,Decorator 模式应用的要点在于解决「主体类在多个方向上的扩展功能」——是为「装饰」的含义。
7. 桥模式(Bridge)
将抽象部分(业务功能)与实现部分(平台实现)分离,使它们都可以独立地变化。
——《设计模式》GoF
动机
由于某些类型的固有的实现逻辑,使得它们具有两个变化的维度,乃至多个维度的变化。
如何应对这种「多维度的变化」?如何利用面向对象技术来使得类型可以轻松地沿着两个乃至多个方向变化,而不用引入额外的复杂度?
笔记
对象的继承关系是在编译时就定义好了,所以无法在运行时改变从父类继承的实现。子类的实现与它的父类有非常紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化。当你需要复用子类时,如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性。
实现系统可能有多角度分类,每一种分类都有可能变化,那么就把这种多角度分离出来让它们独立变化,减少它们之间的耦合。
只要真正深入地理解了设计原则,很多设计模式其实就是原则的应用而已,或许在不知不觉中就在使用设计模式了。
结构
要点总结
Bridge 模式使用“对象间的组合关系”解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。所谓抽象和实现沿着各自维度的变化,即“子类化”它们。
Bridge 模式有时候类似于多继承方案,但是多继承方案往往违背单一职责原则(即一个类只有一个变化的原因),复用性比较差。Bridge 模式是比多继承方案更好的解决方法。
Bridge 模式的应用一般在“两个非常强的变化维度”,有时一个类也有多于两个的变化维度,这时可以使用 Bridge 的扩展模式。