《23个设计模式》

课件&代码见:https://github.com/rhyspang/CPP-Design-Patterns

 
 

C++设计模式

C++设计模式

什么是设计模式

“每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动”。 ——Christopher Alexander

如何解决复杂性?

  • 分解
    • 人们面对复杂性有一个常见的做法:即分而治之,将大问题分解为多个小问题,将复杂问题分解为多个简单问题。
  • 抽象
    • 更高层次来讲,人们处理复杂性有一个通用的技术,即抽象。由于不能掌握全部的复杂对象,我们选择忽视它的非本质细节,而去处理泛化和理想化了的对象模型。

面向对象设计原则

1 依赖倒置原则(DIP)

  • 高层模块(稳定)不应该依赖于低层模块(变化),二者都应该依赖于抽象(稳定) 。
  • 抽象(稳定)不应该依赖于实现细节(变化) ,实现细节应该依赖于抽象(稳定)。

A依赖B,A在编译的时候B需要存在

2 开放封闭原则(OCP)

  • 对扩展开放,对更改封闭。
  • 类模块应该是可扩展的,但是不可修改。

3 单一职责原则(SRP)

  • 一个类应该仅有一个引起它变化的原因。
  • 变化的方向隐含着类的责任。

4 Liskov 替换原则(LSP)

  • 子类必须能够替换它们的基类(IS-A)。
  • 继承表达类型抽象。

5 接口隔离原则(ISP)

  • 不应该强迫客户程序依赖它们不用的方法。
  • 接口应该小而完备。

6 优先使用对象组合,而不是类继承

  • 类继承通常为“白箱复用”,对象组合通常为“黑箱复用” 。
  • 继承在某种程度上破坏了封装性,子类父类耦合度高。
  • 而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低。

7 封装变化点

  • 使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合。

8 针对接口编程,而不是针对实现编程

  • 不将变量类型声明为某个特定的具体类,而是声明为某个接口。
  • 客户程序无需获知对象的具体类型,只需要知道对象所具有的接口。
  • 减少系统中各部分的依赖关系,从而实现“高内聚、松耦合”的类型设计方案。

GOF-23模式分类

  • 从目的上看
    • 创建型(Creational)模式:将对象的部分创建工作延迟到子类或者其他对象,从而应对需求变化为创建对象时具体类型实现引来的冲击。
    • 结构性(Structural)模式:通过类继承或者对象组合获得更灵活的结构,从而应对需求变化为为对象的结构带来的冲击。
    • 行为型(Behavioral)模式:通过类继承或者对象组合来或分类与对象间的职责,从而应对需求变化为多个交互的对象带来的冲击。
  • 从范围来看:
    • 类模式处理类与子类的静态关系。
    • 对象爱嗯模式处理对象间的动态关系。

从封装变化角度对模式分类

从封装变化角度对模式分类

  • 组件协作:
    • Template Method
    • Observer / Event
    • Strategy
  • 单一职责:
    • Decorator
    • Bridge
  • 对象创建:
    • Factory Method
    • Abstract Factory
    • Prototype
    • Builder
  • 对象性能:
    • Singleton
    • Flyweight
  • 接口隔离:
    • Façade
    • Proxy
    • Mediator
    • Adapter
  • 状态变化:
    • Memento
    • State
  • 数据结构:
    • Composite
    • Iterator
    • Chain of Resposibility
  • 行为变化:
    • Command
    • Visitor
  • 领域问题:
    • Interpreter

重构获得模式 Refactoring to Patterns

  • 面向对象设计模式是“好的面向对象设计”,所谓“好的面向对象设计”指是那些可以满足 “应对变化,提高复用”的设计 。
  • 现代软件设计的特征是“需求的频繁变化”。设计模式的要点是“寻找变化点,然后在变化点处应用设计模式,从而来更好地应对需求的变化”.“什么时候、什么地点应用设计模式”比“理解设计模式结构本身”更为重要。
  • 设计模式的应用不宜先入为主,一上来就使用设计模式是对设计模式的最大误用。没有一步到位的设计模式。敏捷软件开发实践提倡的“Refactoring to Patterns”,重构获得模式,是目前普遍公认的最好的使用设计模式的方法。

重构关键技法

  • 静态 -> 动态
  • 早绑定 -> 晚绑定
  • 继承 -> 组合
  • 编译时依赖 -> 运行时依赖
  • 紧耦合 -> 松耦合

“组件协作”模式

现代软件专业分工之后的第一个结果是“框架与应用程序的划分”,“组件协作”模式通过晚期绑定,来实现框架与应用程序之间的松耦合,是二者之间协作时常用的模式。

经典模式:
• Template Method
• Observer / Event
• Strategy

Template Method模板方法

动机

  • 在软件构建过程中,对于某一项任务,它常常有稳定的整体操作结构,但各个子步骤却有很多改变的需求,或者由于固有的原因(比如框架与应用之间的关系)而无法和任务的整体结构同时实现。
  • 如何在确定稳定操作结构的前提下,来灵活应对各个子步骤的变化或者晚期实现需求

[代码实例](https://github.com/yangmiemie99/NoteBook/tree/master/CPP-Design-Patterns/03_课件_Template Method模板方法/代码)

在template1中

Library开发人员:

  • 开发1,3,5三个步骤

Application开发人员:

  • 开发2,4两个步骤
  • 程序主流程

在template2中:

Library开发人员:

  • 开发1,3,5三个步骤
  • 程序主流程

Application开发人员:

  • 开发2,4两个步骤

开发人员不需要知道主流程了

早绑定和晚绑定

Library早写出的东西调用晚写Application出来的东西。

模式定义

定义一个操作中的算法的骨架 (稳定),而将一些步骤延迟
(变化)到子类中。Template Method使得子类可以不改变(复用)一个算法的结构即可重定义(override 重写)该算法的某些特定步骤。

在面向对象的语言里,我们提到延迟到子类一般指,定义一个虚函数,让子类实现(重写)这个虚函数。

适合使用模板方法必须有一个稳定的骨架结构可以被重用。哪些是稳定的?哪些是变化的?相对性

要点总结

  • Template Method 模式是一种非常基础性的设计模式,在面向对象系统中有着大量的应用。她用最简单的机制(虚函数的多态型)为很多应用程序框架提供了灵活的扩展点(继承+多态),是代码复用的基本实现结构。

  • 除了可以灵活应对子步骤的变化外,“不要调用我,让我调用你”的反向控制结构是Template Method的典型应用。

  • 在具体实现方面,被Template Method调用虚方法可以具有实现,也可以没有任何实现(抽象方法,纯虚方法),但一般推荐为将他们设置为protected方法。

策略模式Strategy

动机

  • 在软件构建过程中,某些对象使用的算法可能多种多样,经常改变,如果将这些算法都编码到对象中,会将这些对象变得异常复杂;而且有时候支持不使用的算法也是一个性能负担。(不必要的代码占用代码段)
  • 如何在运行时根据需要透明地更改对象的算法?将算法与对象本身解耦,从而避免上述问题。

策略代码实例

不同国家的税务算法不同,用枚举实现,更改需求带来业务代码改变。

模式定义

定义一系列算法,把他们一个个封装起来,并且使他们可以互相替换(变化)。该模式使得算法可以独立于使用它的客户程序(稳定)而变化(扩展,子类化)。用扩展的方式面对未来需求的变化。

上层稳定,下层变化

要点总结

  • Strategy及其子类为组件提供了一系列可重用的算法,从而可以使得类型在运行时方便地根据需要在各个算法之间进行切换。
  • Strategy模式提供了用条件判断语句以外的另一种选择,消除条件判断语句,就是在解耦合。含有许多条件判断语句的代码通常都需要Strategy模式。
  • 如果Strategy对象没有实例变量,那么各个上下文可以共享同一个Strategy对象,从而节省对象开销。

观察者模式Observer

动机

  • 在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系” ——一个对象(目标对象)的状态发生改变,所有的依赖对 象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化。
  • 使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。

观察者代码

实例1违反了依赖倒置原则,进度通知依赖一个控件(变化),我们都可能不是一个WinForm程序,实现细节变更。

模式定义

定义对象间的一种一对多(变化)的依赖关系,一遍当一个对象(Subject)的状态发生改变的时候,所有依赖它的对象都得到通知并自动更新。

要点总结

  • 使用面向对象的抽象,Observer模式使得我们可以独立地改变目标与观察者,从而使得二者之间的依赖关系达致松耦合。
  • 目标发送通知时,无需指定观察者,通知(可以学带通知信息作为参数)会自动传播。
  • 观察者自己决定是否需要订阅通知,目标对象对此一无所知。
  • Observer模式是基于事件的UI框架中非常常用的设计模式,也是MVC模式的一个重要组成部分。

“单一职责”模式

在软件组件的设计中,如果责任划分的不清晰,使用继承得到的结果往往是随着需求的变化,子类急剧膨胀,同时充斥着重复的代码,这个时候的关键是划分责任。

典型模式,责任上表现突出

  • Decorator
  • Bridge

装饰模式Decorator

动机

  • 在某些情况下我们可能会“过度地使用继承来扩展对象的功能”,由于继承为类型引入的静态特质,使得这种扩展方式缺乏灵活性; 并且随着子类的增多(扩展功能的增多),各种子类的组合(扩展功能的组合)会导致更多子类的膨胀。
  • 如何使“对象功能的扩展”能够根据需要来动态地实现?同时避免“扩展功能的增多”带来的子类膨胀问题?从而使得任何“功能扩展变化”所导致的影响将为最低?

装饰模式代码

示例1内部继承结构

只有三个子类,两种操作,通过不同组合派生出9个孙子类。代码中存在大量重复。

实例2:用组合代替继承,运行时编译

实例3:将共同的部分提出,类关系图如下,子类大大减少

模式定义

动态(组合)地给一个对象增加一些额外的职责。就增加功能而言,Decorator模式比生成子类(继承)更为灵活(消除重复代码&减少子类的个数)。

要点总结

  • 通过采用组合而非继承的手法,Decorator模式实现了在运行时动态扩展对象功能的能力,而且可以根据需要扩展多个功能。避免了使用继承带来“灵活性差”和“多子类衍生问题”。
  • decorator类在接口上表现为is a Component的继承关系,即Decorator类继承了Component类所具有的接口。但在实现上又表现为has a Component的组合关系,即Decorate类又使用了另外一个Component类。
  • Decorator模式的目的并非解决“多子类衍生的多继承”问题,Decorator模式应用的要点在于解决“主体类在多个方向上的扩展功能”——是为“装饰”的含义。

桥模式Bridge

动机

  • 由于某些类型的固有的实现逻辑,使得它们具有两个变化的维度,乃至多个纬度的变化。
  • 如何应对这种“多维度的变化”?如何利用面向对象技术来使得类型可以轻松地沿着两个乃至多个方向变化,而不引入额外的复杂度?

桥模式示例代码

模式定义

将抽象部分(业务功能)与实现部分(平台实现)分离,使他们都可以独立变化。

要点总结

  • Bridge模式使用“对象间的组合关系”解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。所谓抽象和实现沿着各自纬度的变化,即“子类化”它们。
  • Bridge模式有时候类似于多继承方案,但是多继承方案往往违背单一职责原则(即一个类只有一个变化的原因),复用性比较差。Bridge模式是比多继承方案更好的解决方法。
  • Bridge模式的应用一般在“两个非常强的变化维度”,有时一个类也有多于两个的变化维度,这时可以使用Bridge的扩展模式。

“对象创建”模式

  • 通过“对象创建”模式绕开new,来避免对象创建(new)过程中所导致的紧耦合(依赖具体类),从而支持对象创建的稳定。它是接口抽象之后的第一步工作。
  • 典型模式
    • Factory Method
    • Abstract Factory
    • Prototype
    • Builder

工厂方法Factory Method

动机

  • 在软件系统中,经常面临创建对象的工作,由于需求的变化,需要创建的对象的具体类型经常变化。
  • 如何应对这种变化?如何绕过常规的对象创建方法(new),提供一种“封装机制”来避免可客户程序和这种“具体对象创建工作”的紧耦合?

工厂方法示例

模式定义

定义一个用于创建对象的接口,让子类决定实例化 哪一个类。Factory Method使一个类的实例化延迟(目的:解耦合,手段:虚函数)到子类

要点总结

  • Factory Method模式用于隔离对象的使用者和具体类型之间的耦合关系。面对一个经常变化的具体类型,紧耦合关系(new)会导致软件的脆弱。
  • Factory Method模式通过面向对象的手法,将所要创建的具体对象工作延迟到 子类,从而实现一种扩展(而非更改)的策略,较好的解决了这种紧耦合的关系。
  • Factory Method模式解决了“单个对象”的需求变化。缺点在于要求创建方法/参数相同。

抽象工厂Abstract Factory

动机

  • 在软件系统中,经常面临着“一系列相互依赖的对象”的创建工作;同时,由于需求的变化。往往存在更多系列对象的创建工作。
  • 如何应对这种变化?如何绕过常规的对象创建方法(new),提供一种“封装机制”来避免可客户程序和这种“具体对象创建工作”的紧耦合?

抽象工厂示例

模式定义

提供一个接口,让该接口负责一系列“相关或者相互依赖的对象”无需指定他们的具体类。

要点总结

  • 如果没有用应对“多系列构造”的需求变化,则没有必要使用抽象工厂模式,这时候使用简单工厂完全可以。
  • “系列对象”指的是在某一特定系列下的对象之间有相互依赖或作用的关系。不同系列的对象之间不能相互依赖。
  • 抽象工工厂模式主要应对“新系列”的需求变动。其缺点在于难以应对“新对象”的需求变动。

原型模式Prototype

将上述工厂模式代码的抽象类和工厂基类合二为一

动机

  • 在软件系统中,经常面临这“某些结构复杂的对象”的创建工作;由于需求的变化,这些对象经常面临着剧烈的变化,但是它们却拥有比较稳定一致的接口。
  • 如何应对这种变化?如何向“客户程序(使用这些对象的程序)”隔离出“这些易变对象”,从而使得依赖这些”易变对象“的客户程序不随着需求改变而改变。

应用的比较少

原型模式示例

模式定义

使用原型实例指定创建对象的种类,然后通过拷贝这些原型来创建新的对象。

要点总结

  • Prototype模式同样用于隔离对象的使用者和具体类型(易变类)之间的耦合关系,它同样要求这些“易变类”拥有稳定的接口。
  • Prototype模式对于“如何创建易变类的实体对象“采用”原型克隆“的方法来做, 它使得我们可以非常灵活地动态创建”拥有某些稳定接口“的新对象——所需工作仅仅是注册一个新类的对象(即原型), 然后在任何需要的地方Clone。
  • Prototype模式中的Clone方法可以利用某些框架中的序列化来实现深拷贝。(Java/C#)

构建器Builder

动机(Motivation)

  • 在软件系统中,有时候面临着“一个复杂对象”的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这 个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。
  • 如何应对这种变化?如何提供一种“封装机制”来隔离出“复杂对象的各个部分”的变化,从而保持系统中的“稳定构建算法”不随着需求改变而改变?

构建器示例

模式定义

将一个复杂对象的构造与其表示相分离,使得同样的构建过程(稳定)可以创建不同的表示(变化)。

要点总结

  • Builder 模式主要用于“分步骤构建一个复杂的对象”。在这其中“分步骤”是一个稳定的算法,而复杂对象的各个部分则经常变化。
  • 变化点在哪里,封装哪里—— Builder模式主要在于应对“复杂对象各个部分”的频繁需求变动。其缺点在于难以应对“分步骤构建算法”的需求变动。
  • 在Builder模式中,要注意不同语言中构造器内调用虚函数的差别(C++(构造函数中不可以调用虚函数) vs. C#)。

“对象性能”模式

  • 面向对象很好的解决了“抽象”的问题,但是不可避免的要付出一定的代价。对于通常情况来讲,面向对象的成本大都可以忽略不计。但是某些情况下,面向对象带来的成本必须谨慎处理。
  • 典型模式
    • Singleton
    • Flyweight

单件模式Singleton

动机(Motivation)

  • 在软件系统中,经常有这样一些特殊的类,必须保证它们在系统中只存在一个实例,才能确保它们的逻辑正确性、以及良好的效率。
  • 如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例?
  • 这应该是类设计者的责任,而不是使用者的责任。

示例代码

模式定义

保证一个类仅有一个实例,并提供一个该实例的全局访问点。 ——《设计模式》GoF

要点总结

  • Singleton模式中的实例构造器可以设置为protected以允许子类派生。
  • Singleton模式一般不要支持拷贝构造函数和Clone接口,因为这有可能导致多个对象实例,与Singleton模式的初中违背。
  • 如何实现多线程环境下安全的Singleton?注意对双检查锁的正确实现。

享元模式flyweight

动机

  • 在软件系统中采用纯粹对象方案的问题在于大量细颗粒读的对象会很快的充斥系统中,从而带来很高的运行代价——主要指内存需求方面的代价。
  • 如何在避免大量细颗粒度对象问题的同时,让外部客户程序依然能够透明的使用面向对象的方式来操作。

模式定义

运用共享技术有效地支持大量细颗粒读的对象。

享元模式 示例

要点总结

  • 面向对象和好地解决了抽象性的问题,但是作为一个运行在机器中的程序实体,我们需要考虑对象的代价问题。Flyweight主要解决了面向对象的代价问题,一般不触及面向对象的抽象性问题。
  • Flyweight采用对象共享的做法来降低系统中对象的个数。从而降低细颗粒对象给系统带来的内存压力。在具体实现方面,要注意对象状态的处理。
  • 对象的数量太大从而导致对象内存开销加大——什么样的数量才算大?这需要我们仔细的根据具体应用情况进行评估,而不能凭空臆断。

“接口隔离模式”

  • 在组件构建过程中,某些接口直接的依赖常常会带来很多问题、甚至无法实现。采用添加一层间接(稳定)接口,来隔离本来互相紧密关联的接口是一种常见的解决方案。
  • 典型模式
    • Facade 门面模式
    • Proxy 代理模式
    • Adapter 适配器
    • Mediator 中介者

Facade 门面模式

系统间耦合的复杂度

动机

  • 上述A方案的问题在于组件的客户和组件中各种复杂的子系统有了过多的耦合,随着外部客户程序和各子系统的演化,这种过多的耦合面临很多变化的挑战。
  • 如何简化外部客户程序和系统间的交互接口?如何将外部客户程序的演化内部子系统的变化之间的依赖相互解耦

模式定义

为子系统中的一组接口提供一个一致(稳定)的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用(复用)。

结构

要点总结

  • 从客户程序的角度来看,Facade模式简化了整个组件系统的接口,对于组件内部与外部客户程序来说,达到了一种“解耦”的效果——内部子系统的任何变化不会影响到Facade接口的变化。
  • Facade设计模式更注重从架构的层次去看整个系统,而不是单个类的层次。Facade很多时候是一种架构设计模式。
  • Facade设计模式并非是一个集装箱,可以任意地放进任何多个对象。Facade设计模式中组件的内部应该是“相互耦合关系比较大的一系列组件”,而不是一个简单的功能集合。

Proxy 代理模式

动机

  • 在面向对象系统中,有些对象由于某些原因(比如对象创建的开销很大,或者某些操作需要安全控制,或者需要进程外的访问等)直接访问会给使用者或者系统结构带来很多麻烦。
  • 如何在不失透明操作(一致性)对象的同时来管理/控制这些对象特有的复杂性?增加一层间接层是软件开发中常见的解决方式。

模式定义

为其他对象提供一种代理以控制(隔离,使用接口)对这个对象的访问。

代理模式代码示例

具体到设计会比较复杂。

要点总结

  • “增加一层间接层”是软件系统中对许多复杂问题的一种常见解决方法。在面向对象系统中,直接使用某些对象会带来很多问题,作为间接层proxy对象便是解决这一问题的常用手段。
  • 具体proxy设计模式的实现方法,实现粒度相差很大,很有可能对单个对象左细颗粒度控制,如copy-on-write技术,有些可能对组件模块提供抽象代理层(分布式架构,需要网络访问等等),在架构层次对对象左proxy。
  • proxy并不要求保持接口完整的一致性,只要能够间接控制,有时候损及一些透明性也是可以接受的。

Adapter适配器

动机

  • 在软件先系统中,由于应用环境的变化,常常需要将“一些现存的对象”放在新的环境中应用,但是新环境要求的接口是这些现存对象所不满足的。
  • 如何应对这种“迁移的变化”?如何既能利用现有对象的良好实现,又能满足新的应用环境索要求的接口。

模式定义

将一个类的接口转化成客户希望的另一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

adaptee现存对象。adapter适配器用组合的方式包含现存对象,继承自Target代表与Target有相同的接口。

适配器模式示例

模式定义

将一个类的接口转化成客户希望的另一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

要点总结

  • adapter模式主要应用于“希望复用一些现存的类,但是接口又与复用环境要求不一致的情况“,在遗留代码复用、类库迁移等方买你非常有用。
  • GoF23定义了良好总Adapter模式的实现结构:对象适配器和类适配器。但是类适配器采用”多继承“的实现方式,一般不推荐使用。对象适配器采用”对象组合的方式“,更符合松耦合的精神。
  • Adapter模式可以实现的非常灵活,不必拘泥 于Gof23定义的两种结构。例如,完全可以将Adapter模式中的”现存对象“(deque,queue)作为新的接口方法参数,达到适配的目的。

Mediator 中介者

动机

  • 在软件构建过程中,经常会出现多个对象相互关联交互的情况,对象之间常常会维持一种复杂的引用关系,如果遇到一些需求更改,这些直接的引用关系面临不断的变化。
  • 在这种情况下,我们可以使用一个”中介对象”来管理对象间的关系,避免相互交互的对象之间的紧耦合关系,从而更好的抵御变化。

模式定义

用一个中介对象来封装(封装变化)一系列的对象交互。中介者使各对象不需要显示的相互引用(编译时依赖 -》 运行时依赖),从而使其耦合松散(管理变化),而且可以独立地改变他们之间的交互。

结构

concreteCollegue本身是相互依赖的,经过中介,不相互依赖了。Meidator和Colleague相互依赖。

要点总结

  • 将多个对象间复杂的关联关系解耦,Mediator模式将多个对象间的控制逻辑进行集中管理,变“多个对象互相关联”为“多个对象和一个中介者关联”
  • 随着控制逻辑的复杂化,Mediator具体对象的实现可能相当复杂。这时候可以对Mediator独享进行分解处理。
  • Facade模式是解耦系统间(单向)的对象关联关系;Mediator模式是解耦系统内各个对象之间(双向)的关联关系。

“状态变化”模式

  • 在组件构建过程中,某些对象的状态经常面临变化,如何对这些变化进行有效的管理?同时又维持高层模块的稳定?“状态变化”模式这一问题提供了一种解决方案。
  • State
  • Memento

State状态模式

动机

  • 在软件构建过程中,某些对象的状态如果改变,其行为也会随之而发生变化,比如文档处于只读状态,其支持的行为和读写状态支持的行为就可能完全不同。
  • 如何在运行时根据对象的状态来透明地更改对象的行为?而不会为对象操作和状态转化之间引入紧耦合?

状态模式代码示例

模式定义

允许一个对象在其内部状态改变时改变他的行为。从而使得对象看起来似乎修改了其行为。

要点总结

  • State模式将所有与一个特定状态相关的行为都放入一个State的子类对象中,在对象状态切换时,切换相应的对象;但同时维持State的接口,这样实现了具体操作与状态转换之间的解耦合。
  • 为不同的状态引入不同的对象使得状态转换变得更加明确,而且可以保证不会出现状态不一致的情况,因为转换是原子性的——即要么彻底转换过来。要么不转化。
  • 如果State对象没有实例变量,那么上下文可以共享同一个State对象,从而节省对象开销。

Memento备忘录

动机

  • 在软件构建的过程中,某些对象的状态在转换的过程中,可能由于某种需要,要求程序能够回溯到对象之前处于某个点的状态。如果使用一些公有接口来让其他对象得到对象的状态,便会暴露对象的细节实现。
  • 如何实现对象状态的良好保存与修复?但同时又不会因此而破坏对本身的封装性

模式定义

在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先保存的状态。

备忘录示例

要点总结

  • 备忘录(Memento)存储原发器(Originator)对象的内部状态,在需要时恢复原发器状态。
  • 备忘录模式的核心是信息隐藏,即Originator需要向外接隐藏信息,保持其封装性。但同时又需要将状态保持到外界。
  • 由于相待语言进行时(C#,Java),都具有相当的对象序列化支持,因此往往采用效率更高、又较容易正确实现的序列化方案来支持Memento模式。

“数据结构”模式

  • 常常一些组件在内部具有特定的数据模式,如果让客户程序依赖这些特定的数据结构,将极大的破坏组件的复用。这时候,将这些特定的数据结构封装在内部,在外部提供统一的接口,来实现特定数据结构无关的访问,是一种行之有效的解决方案。
  • 典型模式
    • Composite
    • Iterator
    • Chain of Resposible

Composite组合模式

动机

  • 在软件在某些情况下,客户代码过多地依赖于对象容器复杂的内部实现结构,对象容器内部实现结构(而非抽象接口)的变化引起客户代码的频繁变化,带来了代码的维护性、扩展性等弊端。
  • 如何“客户代码与复杂的对象容器结构”解耦合?额昂对象容器实现自身的复杂结构,从而使客户代码就像处理简单对象一样来处理复杂的对象容器。

模式定义

将对象组合成树形结构以表示“部分-整体”的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性(稳定)。

组合模式代码示例

要点总结

  • 组合模式采用树形结构来实现更普遍存在的对象容器,从而将“一对多”的关系转化为“一对一”的关系,使得客户代码可以一致地(复用)处理对象和对象容器,无需关心处理的是单个的对象,还是组合的对象容器。

  • 将“客户代码与复杂的对象容器结构”解耦是Composite的核心思想,解耦之后,客户代码将与纯粹的抽象接口——而非对象容器内部实现结构——发生依赖,从而更能“应付变化”。

  • Composite模式在具体实现中,可以让父对象中的子对象反向追溯;如果父对象有频繁的遍历需求,可以使用缓存技巧来改善效率。

    核心:通过多态的递归调用来解耦内部和外部的依赖关系。

Iterator迭代器

动机

  • 在软件构建过程中,集合对象内部结构常常变化各异。但对于这些集合对象,我们希望在不暴露其在内部结构的同时,可以让外部客户代码透明地访问其中包含的元素;同时这种“透明遍历”也可以为“同一种算法在多种集合对象上进行操作“提供了可能。
  • 使用面向对象技术将这种遍历机制抽象为”迭代器对象“ 为”应对变化中的集合对象“提供了一种优雅的方式。

模式定义

提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露(稳定)该对象的内部表示。

迭代器代码示例

要点总结

  • 迭代抽象:访问一个对象的内容而无需暴露它的内部表示。
  • 迭代多态:为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作。
  • 迭代器的健壮性考虑:遍历的同时更改迭代器所在的集合结构,会导致问题。

Chain of Resposibility职责链

动机

  • 在软件构建过程中,一个请求可能被多个对象处理,但是每个请求在运行时只能有一个接受者,如果显示指定,将必不可少的带来请求发送者与接受者的紧耦合。
  • 如何使请求的发送者不需要指定具体的接受者?让请求的接受者如何自己在运行时句顶来处理请求,从而让两者解耦。

模式定义

使多个对象都有机会处理请求,从而避免请求的发送者和接收者直接的耦合关系。将这些对象连接成一条链,并沿着这条链传递请求,直到一个对象处理它为止。

职责链示例

要点总结

  • 职责链模式的应用场合在于“一个请求可能有多个接受者,但是最后真正接受者只有一个“,这时候请求发送者与接受者的耦合有可能出现”变化脆弱“的症状,职责链的目的就是将二者解耦合,从而更好的应对变化。
  • 应用了职责链模式后,对象的职责分配将更具灵活性。我们可以在运行时动态添加/修改请求的处理职责。
  • 如果请求传递到职责链的末尾仍然得不到出路,应该有一个合理的缺省机制。这也是每一个接受对象的责任,而不是发出请求的对象的责任。

”行为变化“模式

  • 在组件的构建过程中,组件的行为变化经常导致组件本身剧烈的变化。”行为变化“模式将组件的行为和组件本身进行解耦,从而支持组件行为的变化,实现两者之间的松耦合。
  • 经典模式
    • Command
    • Visitor

Command命令模式

动机

  • 在软件构建的过程中,”行为请求者”与“行为实现者“通常呈现一种紧耦合。在某些场合——比如需要对行为进行”记录、撤销/重(redo,undo)、事务“等处理,这种无法抵御变化的紧耦合是不适合的。
  • 在这种情况下,如何将”请求者“与”行为实现者“解耦?将一组行为抽象为对象,可以实现二者实现之间的松耦合。

模式定义

将一个请求(行为)封装成一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销操作。

行为模式实例

要点总结

  • 命令模式的根本目的在于将”行为请求者“与”行为实现者“解耦,在面向兑现嗯语言中,常见的实现手段是”将行为抽象为对象“。
  • 实现命令接口的具体命令对象ConcreteCommand有时候根据需要可能会保存一些额外的状态信息。通过使用Composite模式,可以将多个”命令“封装为一个”复合命令“,MacroCommand
  • Command模式与C++中函数对象有一些类似。但两者定义行为接口的规范有所区别:Command以面向对象中的”接口-实现“来定义行为接口规范,更严格,但性能有损失;C++函数对象以函数签名来定义行为接口规范,更灵活,性能更高。

Visitor访问器

动机

  • 在软件构建过程中,由于需求的改变,某些层次结构中常常需要增加新的行为(方法),如果直接在基类中做这样的更改,将会给子类带来很繁重的变更负担,甚至破坏原有的设计。
  • 如何在不更改类层次结构的前提下,在运行时根据需要透明地为层次结构上的各个类动态添加新的操作,从而避免上述问题。

首先Visitor有一个前提,我能预料到要为整个类层次添加新的操作。但我不知道要加什么操作,要加几个操作。

模式定义

表示一个作用于某对象结构中的各元素的操作。使得可以在不改变(稳定)各元素的类的前提下定义(扩展)作用域这些元素的新操作。

访问器模式示例

模式的缺点:visitor 稳定的前提是Element所有子类都确定。条件苛刻

要点总结

  • 访问器模式通过所谓的双重分发来实现不更改(不添加新的操作-编译时)Element类层次结构的前提下,在运行时透明地为类层次结构上的各个类动态添加新的操作(支持变化)。
  • 所谓双重分发即Visitor模式中间包括了两个多态的分发(注意其中的多态机制):第一个accept方法的多态辨析;第二个为visitElementX方法的多态辨析。
  • visitor模式最大的缺点在于扩展类层次结构(添加新的Element子类),会导致Visitor类的改变因此Visitor模式适用于Element类层次结构稳定,而其中操作经常面临频繁改动。

“领域规则”模式

  • 在特定领域中。某些变化虽然频繁,但可以抽象为某种规则。这时候,结合特定领域,将问题抽象为语法规则,从而给出在该领域下一般性解决方案。
  • 常见模式
    • 解析器Interpreter

解析器Interpreter

动机

  • 在软件构建过程中,如果某一特定领域的问题比较复杂,类似的结构不断重复出现,如果使用普通的编程方式来实现将面临非常频繁的变化。
  • 在这种情况下,将特定领域的问题表达为某种特定语法规则下的句子,然后构建一个解释器来解释这样的句子,从而达到解决问题的目的。

解析器模式示例

模式定义

给定一个语言,定义它文法的一种表示,并定义一种解释器,这个解释器使用该表示来解释语言中的句子。

要点总结

  • 解释器模式的应用场合是Interpreter模式应用中的难点,只有满足“业务规则频繁变化,且类似结构不断重复出现,并且容易抽象为语法规则“的问题才适合Interpreter模式。
  • 使用Interperter模式来表示文法规则,从而可以使用面向对象的技巧来方便地”扩展“文法。
  • Interpreter模式比较适合简单的文法表示,对于复杂的文法表示,Interpreter模式会产生比较大的类层次结构,需要求助于语法分析生成器这样的标准工具。

总结

一个目标

  • 管理变化提高复用

两个手段

  • 分解
  • 抽象

八大原则

  • 依赖倒置原则
  • 开放封闭原则
  • 单一职责原则
  • 里式替换原则
  • 接口隔离原则
  • 对象组合优于类继承
  • 封装变化点
  • 面向接口编程

重构技法

  • 静态 -> 动态
  • 早绑定 -> 晚绑定
  • 继承 -> 组合
  • 编译时依赖 -> 运行时依赖
  • 紧耦合 -> 松耦合

从封装变化角度对模式分类(将不常用圈出)

  • 组件协作:
    • Template Method
    • Observer / Event
    • Strategy
  • 单一职责:
    • Decorator
    • Bridge
  • 对象创建:
    • Factory Method
    • Abstract Factory
    • Prototype
    • Builder
  • 对象性能:
    • Singleton
    • Flyweight
  • 接口隔离:
    • Façade
    • Proxy
    • Mediator
    • Adapter
  • 状态变化:
    • Memento
    • State
  • 数据结构:
    • Composite
    • Iterator
    • Chain of Resposibility
  • 行为变化:
    • Command
    • Visitor
  • 领域问题:
    • Interpreter

关注稳定点和变化点

什么时候不用模式

  • 代码可读性很差时
  • 需求理解还很浅时
  • 变化还没有显现时
  • 不是系统的关键依赖点
  • 项目没有复用价值时
  • 项目将要发布的时候

经验之谈

  • 不要为了模式而模式
  • 关注抽象类&接口
  • 理清变化点和稳定点
  • 审视依赖关系
  • 要有Framework和Application的区隔思维(写Framework的时候要考虑为应用程序开发人员去留什么样的接口,留什么样的扩展点)
  • 良好的实际是演化的结果

设计模式成长之路

  • ”手中无剑,心中无剑“:见模式而不知
  • ”手中有剑,心中无剑“:可以识别模式,作为应用开发人员使用模式
  • ”手中有剑,心中有剑“:作为框架开发人员为应用设计某些模式
  • ”手中无剑,心中有剑“:忘掉模式,只有原则
 

设计模式简介

  每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。                                    

                                                                                      -- Christopher Alexander

  设计模式描述了软件设计过程中某一类常见问题的一般性的解决方案。面向对象设计模式描述了面向对象设计过程中,特定场景下,类(抽象类之间,抽象类和派生类)之间或者相互通信的对象之间常见的组织关系。

  对象是什么?

----从概念层面讲,对象是某种拥有责任的抽象。

----从规格层面讲,对象是一系列可以被其他对象使用的公共接口。

----从语言实现层面来看,对象封装了代码和数据

从设计原则到设计模式

针对接口编程,而不是针对实现编程

---- 客户无需知道所使用对象的特定类型,只需要知道对象拥有客户所期望的接口。

优先使用对象组合,而不是类继承

---- 类继承通常为“白箱复用”,对象组合通常为“黑箱复用”。继承在某种程度上破坏了封装性,子类和父类耦合度高;而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低。

封装变化点

---- 使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合。

  类设计的五项原则:

  • SRP,单一职责原则,一个类应该有且只有一个改变的理由。
  • OCP,开放封闭原则,应该能够不用修改原有类就能扩展一个类的行为。类模块应该是可扩展的,但是不可修改(对扩展开放,对修改封闭)。
  • LSP,Liskov替换原则,派生类要与其基类自相容。子类必须能够替换它们的基类。
  • DIP,依赖倒置原则,依赖于抽象而不是实现。高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖于实现细节,实现细节应该依赖于抽象。  
  • ISP,接口隔离原则,客户只要关注它们所需的接口。不应该强迫客户程序依赖于它们不用的方法。

 设计模式融合了上述设计原则,并提供了对常见设计问题的解决方案核心。

OO基础

抽象,封装,多态,继承

  其中,抽象和封装是OO的基本。抽象使实际概念得以升华并工程化,从而使设计者能够使用概念视角来考查和设计复杂系统。封装用于封装系统中的各种变化(点),如内部数据和子类差异等,从而简化系统编程。多态和继承则仅仅是实现抽象和封装的必要技术,并非OO本质。

OO原则(同于类设计的五项原则)

1) 封装变化:找出应用中可能的变化点,封装易变代码,隔离稳定代码。

2) 多用组合,少用继承

3) 针对接口编程,不针对实现编程

4) 为交互对象之间的松耦合设计而努力

5) 类应该对扩展开放,对修改关闭

6) 依赖抽象,不依赖具体类. 如工厂方法(Factory Method)

7) 只和朋友交谈(墨忒耳法则):在对象的方法内,只应该调用属于以下范围的方法:a) 该对象本身; b) 被当作方法的参数传递进来的对象; c) 此方法所创建或实例化的任何对象; d) 对象的任何组件(实例变量)。

8) 别找我,我会找你(依赖倒置原则)。父类对外提供公共接口,这些接口调用子类(底层)具体实现接口。这条原则需要通过抽象方法(钩子方法)实现,如模板方法,架构类。

9) 类应该只有一个改变的理由

模式与设计的关系

  每个模式都描述了某个特定场景中一个特定问题的约束因素/动机和关系,并为设计者提供一种解决这些问题的优雅方案。换句话说,模式仅仅是描述了特定场景下的关系和约束因素,正因如此,模式本身并不是最重要的,特定场景下的关系和约束因素才是最真实的,而模式仅仅是提供了一组描述这些关系的一组词汇,提供了一套解决这些关系的优雅方式而已。

  在软件设计中,模式是随特定场景下的关系和约束因素而定的。也就是说,对所要解决问题的分析和理解是使用模式的必要条件。只有清楚了特定场景下的关系和约束因素,才能很好地选择解决问题的方法和适用的模式。

  特定模式描述的是问题中概念(抽象类)之间的关系,比如所有的行为模式,bridge模式等;或者是抽象类和派生类之间的关系,比如Proxy,Composite,Decorator等;抑或是新类和原有类之间的关系,如Adaptor,Facade等。这些关系未必是显而易见的,它们都是问题分析的结果。没有从概念视角对问题的分析,这些关系是不能凸现出来的。因此,使用模式应该建立在共性与可变分析、分析矩阵等方法的基础上。在概念视角层次上的分析,往往考虑的是抽象类之间的关系,因此这时所采用的模式一般是行为模式、bridge模式等。描述抽象类和派生类之间关系的模式一般应该等到实现层次时才考虑引入。这种分层设计有助于简化设计过程,忽略不必要的次要因素,产生更好的高层设计。

  根据上述观点,虽然模式经常是组合使用,但模式的使用顺序是有先后之分的。在高层设计会采用一些高层的主模式,在代码实现级别上,也会再次引入其他合适的模式。个人觉得行为模式、Bridge、Facade等模式是最常用的主模式。

 模式与继承

(注:这里仅针对静态语言,在动态语言中,多态并非通过继承实现)

  封装/继承/多态都是面向对象的基本概念。前面已经讲过,封装变化是模式的核心思想之一。同时,从抽象类和派生类的角度看,继承和多态使得抽象类能够封装派生类的类型。显然,继承和多态使类封装变化成为可能。因此,在设计模式中,继承是随处可见的。

  从模式角度看,模式是面向对象复用的基础元素。采用模式可使设计的软件更加灵活,更易扩展,更易维护,这也正是OCP(开放封闭原则)所强调的。要实现这个目标,继承也是必不可少的。继承使派生类之间可互相替换,因此也就封装了抽象类背后的变化。

  一般来说,使用模式是有代价的。模式虽有其自身的优越性,但只有在问题本身有一定的复杂性时,采用模式来简化这些复杂性才是有意义的。然而,不能忽略的是:模式本身(所涉及的相互交互的类)是有一定复杂性的。每个模式中相互交互的类之间,可以说是紧密耦合在一起的。这些类基本上是不可分的,它们本身就是作为一个整体而存在。同时这些类背后的继承关系在使模式灵活的同时,也增加了复杂性。这都是使用模式时需要考虑的因素。切记:模式中的类是作为一个整体而存在,它们是紧密耦合的关系;模式描述了这些类之间的关系和约束因素,丰富设计词汇,使我们容易交流,但同时要清楚实现时是对这些类和这些交互关系的再现。

设计模式与封装变化

  设计模式可以确保系统能以特定方式变化(这很大程度是一种预测),从而帮助设计者避免重新设计系统。每一个设计模式允许系统结构的某个部分的变化独立于其他部分,这样产生的系统对于某一种特殊变化将更健壮。

  下面阐述一些导致重新设计的一般原因,以及解决这些问题的常用设计模式:

  1) 通过显式地指定一个类来创建对象。在创建对象时指定类名将使设计者受特定实现的约束, 而不是特定接口的约束。这会使未来的变化更复杂。要避免这种情况,应该间接地创建对象。

设计模式:Abstract Factory,Factory Method,Prototype。

  2) 对特殊操作的依赖。 当设计者为请求指定一个特殊操作时,完成该请求的方式就固定了。避免把请求代码写死,可在编译时刻或运行时刻方便地改变响应请求的方法。

设计模式:Chain of Responsibility,Command。

  3) 对硬件和软件平台的依赖。 外部的操作系统接口和应用编程接口(API)在不同的软硬件平台上是不同的。依赖于特定平台的软件将很难移植到其他平台上,甚至都很难跟上本地平台的更新。所以设计系统时限制其平台相关性就很重要了。

设计模式:Abstract Factory,Bridge。

  4) 对对象表示或实现的依赖。 知道对象怎样表示、保存、定位或实现的客户在对象发生变化时可能也需要变化。对客户隐藏这些信息能阻止连锁变化。

设计模式:Abstract Factory,Bridge,Memento,Proxy

  5) 算法依赖。 算法在开发和复用时常常被扩展、优化和替代。依赖于某个特定算法的对象在算法发生变化时不得不变化。因此有可能发生变化的算法应该被孤立起来。

设计模式:Builder,Iterator,Strategy,Template Method,Visitor

  6) 紧耦合的类很难独立地被复用,因为它们是互相依赖的。紧耦合产生单块的系统,要改变或删掉一个类,必须理解和改变其他类。这样的系统是一个很难学习、移植和维护的密集体。

  松散耦合提高了一个类本身被复用的可能性,并且系统更易于学习、移植、修改和扩展。设计模式使用抽象耦合和分层技术来提高系统的松散耦合性。

设计模式:Abstract Factory,Command,Facade,Mediator,Observer,Chain of Responsibility

  7) 通过生成子类来扩充功能。 通常很难通过定义子类来定制对象。每一个新类都有固定的实现开销(初始化、终止处理等)。定义子类还需要对父类有深入的了解。如,重定义一个操作可能需要重定义其他操作。一个被重定义的操作可能需要调用继承下来的操作。并且子类方法会导致类爆炸,因为即使对于一个简单的扩充,也不得不引入许多新的子类。(本质: 继承应该仅仅封装一个变化点)

  一般的对象组合技术和具体的委托技术,是继承之外组合对象行为的另一种灵活方法。新的功能可以通过以新的方式组合已有对象,而不是通过定义已存在类的子类的方式加到应用中去。另一方面,过多使用对象组合会使设计难于理解。许多设计模式产生的设计中,设计者可定义一个子类,且将它的实例和已存在实例进行组合来引入定制的功能。

设计模式:Bridge,Chain of Responsibility,Composite,Decorator,Observer,Strategy

  8) 不能方便地对类进行修改。 有时设计者不得不改变一个难以修改的类。也许你需要源代码而又没有(对于商业类库就有这种情况),或者可能对类的任何改变会要求修改许多已存在的其他子类。设计模式提供在这些情况下对类进行修改的方法。

设计模式:Adapter,Decorator,Visitor

  设计模式与其封装的变化点

设计模式

变化点

创建型

Abstract Factory

产品对象家族

Builder

组合创建组合(复杂) 对象

Factory Method

被实例化的子类(相关类)

Prototype

被实例化的类

Singleton

一个类的唯一实例

Object Factory

被实例化的类,Factory Method的变种

Object Pool

对象管理和重用,Factory Method的变种

Creation Method

类的构造函数,Factory Method的变种

结构型

Adapter

对象的接口(可变API)

Bridge

对象实现(实现逻辑)

Composite

对象的结构和组成

Decorator

对象的职责(职责组合)

Façade

子系统的接口(子系统的变化)

Flyweight

对象的存储

Proxy

对象的访问和对象的位置

行为型

Chain of Responsibility

满足某个请求的对象(请求的实际处理对象)

Command

何时、如何实现某个请求

Interpreter

一个语言的文法和解释

Iterator

如何遍历、访问一个聚合的各元素

Mediator

对象间的交互逻辑

Memento

对象信息的存储管理

Observer

对象间的依赖和通讯

State

对象状态

Strategy

算法

Template Method

算法某些步骤

Visitor

作用于对象上的操作

---------------------------------------------------

模式的分类

  模式依据其目的可分为创建型(Creational)、结构型(Structural)、或行为型(Behavioral)三种。创建型模式与对象的创建有关;结构型模式处理类或对象的组合;行为型模式描述类或对象之间的职责分配和交互。

  根据范围准则,模式可分为类模式和对象模式。类模式处理类和子类之间的关系,这些关系通过继承建立,是静态的,由编译时确定。对象模式处理对象间的关系,这些关系在运行时刻是可变化的,更具动态性。从某种意义上来说,几乎所有模式都使用继承机制,所以“类模式”专指那些集中于处理类间关系的模式,而大部分模式都属于对象模式的范畴。

  分类如下:

创建型

结构型

行为型

Factory Method

Adapter(类)

Interpreter;

Template Method

对象

Abstract Factory;

Builder;

Prototype;

Singleton

-----------------------

Object Factory

Object Pool

Creation Method

Adapter(对象);

Bridge;

Composite;

Decorator;

Façade;

Flyweight;

Proxy

Chain of Responsibility;

Command;

Iterator;

Mediator;

Memento;

Observer;

State;

Strategy;

Visitor

     说明:

  创建型类模式将对象的部分创建工作延迟到子类,而创建型对象模式则将它延迟到另一个对象中。结构型类模式使用继承机制来组合类,而结构型对象模式则描述了对象的组合方式。行为型类模式使用继承描述算法和控制流,而行为型对象模式则描述一组对象如何协作完成单个对象所无法完成的任务。

  还有其他组织模式的方式。有些模式经常会被绑在一起使用,例如,Composite常和Iterato r或Visitor一起使用;有些模式是可替代的,例如,Prototype常用来替代Abstract Factory;有些模式尽管使用意图不同,但产生的设计结果是很相似的,例如,Composite和Decorator的结构图是相似的。

创建型模式

  创建型模式抽象了实例化过程。它们帮助一个系统独立于如何创建、组合、管理和表示它的那些对象。一个类创建型模式使用继承改变被实例化的类,而一个对象创建型模式将实例化委托给另一个对象。

  随着系统演化得越来越依赖于对象复合而不是类继承,创建型模式变得更为重要。当这种情况发生时,重心从对一组固定行为的硬编码(hard-coding)转移为定义一个较小的基本行为集,这些行为可以被组合成任意数目的更复杂的行为。这样创建有特定行为的对象要求的不仅仅是实例化一个类,而可能是一组复杂的对象组合。

  在这些模式中有两个不断出现的主旋律。第一,它们都将关于该系统使用哪些具体的类的信息封装起来。第二,它们隐藏了这些类的实例是如何被创建和放在一起的。整个系统关于这些对象所知道的是由抽象类所定义的接口。因此,创建型模式在什么对象被创建,谁来创建,如何创建,以及何时创建这些方面给予设计者很大的灵活性。它们允许你用结构和功能差别很大的“产品对象”配置一个系统。配置可以是静态的(即在编译时指定),也可以是动态的(在运行时)。

Abstract Factory

意图: 提供一个创建一系列相关或相互依赖对象的接口,用于创建相关或依赖的对象家族,而无需明确指定具体类。

动机: 封装一系列相关或相互依赖对象的创建,隐藏了相关对象创建过程的选取组合问题,减少错误组合的可能性。

适用性: 常用于不同软件构建环境(平台,不同的软件需求配置等)下相关对象的创建。

结构: 一般使用继承来封装不同软件构建环境。父类定义所有的一系列创建接口,由具体类实现。本质上抽象工厂由多个工厂方法组成(每个子类均为对象工厂)。

优点: 减少对象错误组合使用的可能性。

Factory Method

意图:定义一个用于创建对象的接口,让子类决定将哪一个类实例化。FactoryMethod使一个类的实例化延迟到其子类。

动机: 让子类自己决定所需创建的对象,这里的对象一般并非子类本身,而是子类所需要的其他对象。本质:通过子类来创建对象,使客户仅依赖父类的抽象接口。

适用性: 常用于调用统一的抽象类接口,而让子类自己决定(实现)所需创建的对象,由此,封装了子类所创建对象的类型。

结构: 一般内嵌于具有继承关系的子类中,很少单独使用。抽象类提供创建接口,子类实现。典型例子: STL中迭代器的创建(end(),begin())。常用抽象接口:MakeObj();CreateObj()。

优点: 封装了子类所创建对象的类型。

组合模式: 经常用于TemplateMethod中的子类中,使创建过程成为TemplateMethod的一个步骤。

Note:实际中,设计者常常使用专门的工厂类负责相关对象的创建,这不同于FactoryMethod,但都属于对象工厂(负责创建对象的类)。

Builder

意图: 将一个复杂对象(一般内部含有多个其他对象)的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

动机: 封装复杂对象或具有多种组合的对象的创建过程。比如提供不同的接口表示同一个对象不同的构建过程。

适用性: 常用于封装具有可变构建过程的对象。

结构: 提供不同的接口封装不同的构建过程;提供较小粒度的接口,封装构建过程中对象间的交互关系,让客户通过提供的接口自己合成创建过程。

优点: 要么封装整个对象的构建,要么封装了创建过程中对象间的交互关系。

组合模式: 经常用于封装Composite的创建过程(提供小粒度的对外接口)。

Prototype

意图:用原型实例指定创建对象的种类,并且通过拷贝这个原型来创建新的对象。

动机: 拷贝已有对象(包括状态).

适用性: 常用于包含状态的对象拷贝,简化状态的同步过程。典型例子:CAD。

结构: 对象既有自拷贝的对外接口。注意区分浅拷贝和深拷贝,多线程也需要注意。

优点: 简化了对象状态的重新构建。向客户隐藏制造新实例的复杂性;提供让客户能够产生未知类型对象的途径;有时复制对象更高效。

组合模式: 有时与工厂Factory一同使用,通过单一的接口来创建所需的对象。(Map+clone)

Singleton

意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

动机: 保证一个类仅有一个实例。

适用性: 方便对象的全局访问;或在当对象因存在多个副本引发性能问题时,才考虑使用Singleton来限制对象实例化。

结构: 常常和静态变量等相关.

优点: 方便全局访问,限制对象实例化,解决系统的性能问题。

缺点: Singleton本质上是一个全局变量,全局变量并不是什么好东西,不好管理,且容易引发其他问题。一般并不推荐使用Singleton.

ObjectPool

意图: 在创建对象比较昂贵,或者所能创建对象(File/socket)数目有限制时,管理对象的重用。

动机: 封装对重用对象的管理。

适用性: 当对象的创建和/或管理必须遵循一组定义明确的规则集, 并且这些规则都与如何创建对象、创建多少对象和在已有对象完成当前任务时如何重用等相关时,使用Object Pool来创建和管理对象。

结构: 类似于简单的Factory,即使用单独的类。

优点: 封装对重用对象的管理。

CreationMethod

意图: 基于类的不同构造函数,提供多种不同的对象构建过程

动机: 使用不同的Creation函数(语义明确)来区别所创建对象的用途(意图)。

适用性: 类中存在多种不同的构造函数,并且每种构造函数所构建的类有不同的用途/意图。

结构: 一般Creation函数需要是static函数,根据所使用的语言而定。类的构造函数为私有。

优点: 根据不同的Creation函数名,可以很清楚的描述构建对象的用途。

变化: 有时直接把派生类的构建直接放入抽象类中,并使用Creation Method。

---------------------------------------------------

  结构型模式涉及到如何组合类和对象以获得更大的结构。结构型类模式采用继承机制来组合接口或实现。结构型对象模式不是对接口和实现进行组合,而是描述了如何对一些对象进行组合,从而实现新功能的一些方法。因为可在运行时改变对象组合关系,所以对象组合方式具有更大的灵活性,而这种机制用静态类组合是不可能实现的。

Adapter

意图:将一个类的接口转换成客户希望的另外一个接口(转换接口)。Adapter模式使得原本由于接口不兼容而不能一起工作的类可以一起工作。

动机: 重用已有的类。

适用性: 重用已有的类去完成正在编写的类中所需的功能。一般原有的类的功能应该和所需类的功能相近,或者更多。

结构: 使用对象组合。不一定就用于子类的编写,也可以是普通类的编写,因此继承关系并不是必须的。典型例子:ACE对系统调用函数的封装。

优点: 重用代码,简化类的编写和测试。

Facade

意图:为子系统中的一组接口提供一个一致的界面(一致的接口), Facade模式定义了一个高层接口,这个接口使得子系统更加容易使用(简化接口)。

动机: 封装复杂的子系统,使客户更容易使用。同时也封装了原系统变化所引起的客户代码变化。

适用性: 存在功能复杂或易变的子系统,使用Facade增加一个封装层,方便用户使用。

结构: 使用一个类为子系统提供一组易用的对外接口。

优点: 简化编程,隔离变化。

Bridge

意图:将抽象部分与它的实现部分分离,使它们都可以独立地变化。

动机: 分离对外接口和内部实现。本质上就是分离两个变化点,并单独封装其变化。

适用性: 用于具有两个变化点的抽象概念的分离。

结构: 使用两个抽象封装不同的变化点。

优点: 简化类之间的继承关系(理论上,一个抽象一个变化点)。隐藏实现的变化对客户的影响。适用于跨越多个平台的图形和窗口系统。

Composite

意图:将对象组合成树形结构以表示“部分-整体”的层次结构。Composite使得客户对单个对象和复合对象的使用具有一致性。

动机: 隐藏“一对多”的关系,让客户一致的对待不同的对象。

适用性: 常用于结构上具有显式树型结构的对象关系。当使用一致的方式对待列表中的每个对象的情况时,可以使用Composite来封装对对象列表的管理和遍历,简化客户代码对对象的管理,使客户能够使用一致的代码来对待所有对象。

结构: 常常在Composite的一个派生类中,使用列表来管理其他的派生对象;或者基于递归组合来组织可变数目的对象

优点: 封装了“一对多”的对象关系,使客户能够统一的对待所有的派生对象。省去客户自行管理对象的行为和代码(省略大量if语句)。

组合模式: 可以和Command对象一起使用,封装“一对多”的关系,当然如果Command对象需要考虑顺序或者其他情况的时候,应该使用Builder来构建。常常使用Builder来构建Composite,用Iterator来遍历对象。

Decorator

意图:动态地给一个对象添加一些额外的职责。就扩展功能而言,Decorator模式比生成子类方式(继承)更为灵活。

动机: 复用现有的类,在不破坏原来的条件下, 添加一些额外的职责/功能。

适用性: 存在一个具备核心功能的类(组件),但是同时需要一些附加的功能。使用Decorator可在不破坏原类的条件下,动态添加所需的附加功能。

结构: 如果需要动态的替换,继承关系是必须的,在继承关系下,只需在复用原类接口下改写部分接口即可。但是如果没有替换要求,没必要一定使用继承关系,只需简单的适配所需的接口即可。一般,每个装饰者都“有一个”(包装一个)组件,即保存某个组件的实例。如果需要多次修饰,那么可用多个装饰者包装核心组件(多层次继承)。

优点: 代码复用;容易在核心功能代码的基础上增加额外的功能。

缺点:装饰者会导致设计中出现许多小对象,如果过度使用,会使程序变得更加复杂。

Flyweight

意图:运用共享技术有效地支持大量细粒度的对象。让某个类的一个实例提供许多“虚拟实例”

动机: 对象共享,优化性能

适用性:当一个类有许多细小实例,且实例能被同一个方法控制,可使用绳量来存储细小对象,即将所有原来的实例状态存储在一个类中,统一管理。

优点:减少运行时对象实例个数,节约内存;将许多“虚拟”对象的状态集中管理。

Proxy

意图:为其他对象提供一个代理以控制对这个对象的访问。

动机: 对某个对象的访问有特殊的要求。比如需要跨越网络障碍等。

适用性:远程代理控制访问远程对象;虚拟代理控制访问创建开销大的资源;保护代理基于权限控制对资源的访问;缓存代理控制对缓存对象的访问。

结构:常用代理包装实际对象,在必要时可通过工厂来实例化对象。

优点: Proxy是一个重型模式(需要实现原对象的大部分接口),可使用Facade代替。

区别:1)装饰者为对象增加行为,代理是控制对象的访问。2)适配器会改变对象适配的接口,而代理则实现相同的接口。

-------------------下面两个可能谈不上模式,更像代码重构惯用法----------------------------

Compose Method

意图: 将一个比较复杂(逻辑或步骤多)的方法(接口实现)转化成为一系列意图自明的更小方法(接口)。

动机: 提供代码的可读性。

适用性: 所有的复杂的接口实现。该模式经常用于重构现有的代码。

结构: 用更小的接口封装代码行。

优点: 能够大大改善代码的可读性,如果接口名字取的好的话。

组合模式: 可与任何模式相结合,改善代码的质量。

Collecting Parameter

意图: 使用对象(结构体)在不同的地方进行参数收集。

动机: 所需的参数分散在系统的不同地方,使用该模式简化参数的收集和传递。

适用性: 用于类之间的数据收集和传递,也适用于类内部的参数收集和传递。

结构: 一般使用结构体来存放所需参数。

优点: 有利于接口之间和对象之间的参数传递。

组合模式: 常和ComposeMethod方法一起使用。

 行为模式涉及到算法和对象间职责的分配。行为模式不仅描述对象或类的模式,还描述它们之间的通信模式。这些模式刻画了在运行时难以跟踪的复杂的控制流。它们将你的注意力从控制流转移到对象间的联系方式上。

  行为型模式的三个典型特点:

封装变化

对象作为参数

对发送者和接收者解耦

Chain of Responsibility

意图:为解除请求的发送者和接收者之间耦合,而使多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它。

动机: 解除请求的发送者和接收者之间耦合,在对象间传递待处理的请求。

适用性:用于请求的跨层传递,解除请求发送者和请求的最终处理者之间的耦合。统一地处理客户请求。常用于窗口系统,处理鼠标或键盘事件,典型应用:wxWidget的事件系统。

结构: 客户请求一般采用Command封装,使易于传递。一般不同对象采用统一的接口来处理请求。如果请求处理者对象存放在列表中,一般要求使用继承实现。

优点: 易于请求的跨层传递;解除对象耦合(封装请求的真实处理者);统一请求处理(封装请求的变化)。

组合模式: 经常采用Command来封装不同类型的请求.

Command

意图:将一个请求封装为一个对象,从而可用不同的请求对客户进行参数化(传递请求);对请求排队或记录请求日志,以及支持可取消的操作。

动机: 封装请求的类型;让请求具有对象的特性(具有状态的实体),这样就可以传递、保存或者采用不同的方式来处理请求对象。

结构: 用继承来封装请求的类型;除了不一样的构造和初始化函数外,提供一致的核心接口。

优点: 让请求具有对象的特性,使客户能够采用不同的方式来处理请求对象,比如可以解除对象构造和对象使用的耦合,即实体解耦和时间解耦。

用途: 经常用于数据库事务操作,设备控制,多线程核心(Active Object)以及GUI的do/undo管理等。或者用于消除过多的条件分派。

组合模式: 经常用于Chain ofResponsibility中的请求封装;经常和Composite组合使用,提供统一的对待Command的途径,封装“一对多”的关系。

Iterator

意图:提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。

动机: 封装对对象的访问规则或者算法。本质:提供一致的遍历接口

适用性: 适用于为多个不同聚合类提供一致的遍历接口。

结构:抽象迭代器提供类似hasNext(),Next(), Remove()等接口,并由具体聚合类通过createIterator()创建具体的迭代器。

优点: 封装聚合类内部实现,提供一致对外接口

Interpreter

意图:给定一个语言, 定义它的文法的一种表示,并定义一个解释器, 该解释器使用自身定义表示来解释语言中的句子。

动机: 采用不同的对象来表示不同的文法,使文法易于组合使用。提供一种可选择的方式,将易变的组合逻辑推给客户代码。

结构:将每一个语法规则表示成一个类,方便于实现语言。

适用性:方便实现简单语言的解释器(常用yacc和lex工具来编写语言的解释器原型)

Mediator

意图:用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可独立地改变它们之间的交互。

动机: 封装多个对象间的交互关系,解除耦合。

适用性: 适用于多个对象交互关系复杂且易变的情况。常用于协调GUI组件。

结构: 提供一个幕后类来统一管理不同对象间的交互关系。

优点: 封装多个对象间的交互关系,使客户更加容易编程。

Memento

意图:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到保存的状态。

动机: 保存对象状态,用于对象状态的回滚或重新构造。

适用性: 用于对象内部状态易变,且对象状态具有某种价值的场合。

结构: 使用简单的结构体即可完成任务。

优点: 提供保存对象的另一种选择。

缺点:存储耗时,常用语言自带的序列化(serialization)机制存储系统状态。

Observer

意图:定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动刷新。

动机: 封装对象间一对多的依赖关系,提供统一的管理点。

适用性: 所有具有一对多依赖关系且需要传递状态信息的对象管理。

结构: 使用双抽象结构(主题和观察者),一个抽象管理主题状态的通知行为(简化主题派生类的行为),一个抽象用于封装不同的状态观察者。

优点: 在具有多个观察者时,可简化状态通知部分的Hard-coding,并且易于扩展。

变化: 在状态比较复杂的情况下,一般采用某种约定的参数来提示观察者状态发生了什么样的变化,简化对象更新过程。

State

意图:允许一个对象在其内部状态改变时改变它的行为(外部行为)。对象看起来似乎修改了它所属的类。

动机: 分离状态机的逻辑和动作;或者是分离状态和行为(动作)。

结构:state定义所用具体状态的共同接口(事件接口);任何具体状态实现该相同接口。Context拥有所有状态对象。根据不同事件,context在不同状态对象中切换,从而改变自身行为。

优点:避免用户直接和状态交互;去除掉大量的条件语句;使系统更加易于扩展和维护。

变化:1)一般来讲,当状态改变是固定的,状态转换逻辑适合放在Context中;当转换更动态的时候,通常将状态转换逻辑放在State中,但这会使状态类之间产生依赖。总之,该决策决定了究竟哪个类是对修改封闭的(context或state)。2)如有多个Context,则可考虑共享所有的状态类。

  补充:有限状态自动机(FSM)

  有限状态自动机的两种表示方式: 状态迁移图(STD)和状态迁移表(STT)

  状态迁移图(STD)至少由4部分组成。圆形表示状态;连接状态的箭头被称为迁移;迁移被用一个后面跟着动作名的时间做了标记,组成事件/动作对。类似数字电子中的状态转移图,本质上就是一个东西。

  状态迁移表(STT)用一个表的形式来描述系统中状态的转移。表中的每一列表示一个状态迁移的完整过程。由下面4个部分组成一列:

起始状态  触发迁移的事件  终止状态   所执行的动作

  使用状态迁移图(STD)和状态迁移表(STT)来描述自动状态机是非常有效的,并且非常容易检测那么未知的以及没有处理的状态转移情况。这对编程是很有帮助的,因为在实际编码中,非常容易遗漏非正常的状态转移,而这些遗漏往往是错误的根源。

  实现有限状态自动机(FSM)的技术:

  嵌套的switch/case语句,解释迁移表和State模式。

  在简单其状态迁移中,使用嵌套的switch/case语句就足够的。在复杂情况下,使用State模式比较好。解释迁移表也容易实现,但不是这里讨论的目标,最主要的就是表的查找。

  State的标准结构图:

  State模式彻底的分离了状态机的逻辑和动作。动作是在Context类中实现的,而逻辑则是分布在State类的派生类中。这使得二者可以非常容易的独立变化,互补影响。例如,只要使用State的另一个派生类。就可以非常容易地在一个不同的状态逻辑中重用Context类的动作。此外,我们也可以在不影响State派生类逻辑的情况下创建Context的派生类来更改或者替换动作的实现。

  可以使用状态机的地方:作为GUI中高层应用策略;GUI交互控制器;分布式处理等。几乎凡有状态存在的地方均可以考虑采用有限状态机。

Strategy

意图:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。该模式使得算法的变化可独立于使用它的客户。

动机: 封装不同的算法。定制一组可以互换的算法族。

适用性: 同一个问题存在多种不同的解决方案。

结构: 为了满足算法之间的互换性,必须使用继承,并且遵循Liskov原则。

优点: 封装算法的变化。

组合模式: 经常在使用算法的基类Context中使用Template Method。如果Context的派生类中要求所使用的算法动态改变,还常常把Factory Method内嵌到派生类中来创建不同的算法类。

区别:策略模式和状态模式具有相同类图。策略模式是围绕可互换的算法来创建业务的,由Client自行决定具体策略。状态模式则通过改变对象内部状态帮助对象控制自己的行为。

Template Method

意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

动机: 消除子类中的重复代码,简化子类代码。

适用性: 当各个子类中,混杂着不变和可变的行为时,就可以使用该模式。将不变的行为放入父类中,子类只需定制可变的行为。这里不变还包括行为的执行顺序。

结构: 必须使用继承关系。

优点: 消除子类的重复行为。

组合模式: 经常和FactoryMethod、Strategy一起使用。在分解不变和可变行为时,还常可借助组合方法(ComposeMethod)和Collecting Parameter模式。

Visitor

意图:表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

动机: 在不改变原类(一般是特定的数据结构)的情况下,增加新的功能接口。

适用性: 原有的类结构不能或者不容易改变;或者新加的功能不属于原类的职责范畴。

结构: 双重分派。增加一个Visitor类,并针对所有要访问的派生类增加单独的访问接口(在Visitor中)。本质上,Visitor模式中的两次分派形成一个功能矩阵。Visitor的接口名字和其接受的派生类类型分别是功能矩阵的两个变化轴。

优点: 使用Visitor模式,使程序中的数据结构(原类)独立于它的用途。

用途: 一般如果应用程序中存在有需要以多种不同方式进行解释的数据结构,就可以使用Visitor模式。比如使用Visitor模式来遍历所有的配置数据来初始化不同的应用程序子系统。最常见的应用:遍历大量的数据结构并产生不同类型的报表。

缺点:会破环组合类的封装。

Null Object

意图: 提供一个没有任何行为的对象。

动机: 消除代码中四处存在的无效对象判断

适用性: 只要对无效对象的判断逻辑多次出现时,就有引入Null Object的必要。

结构: Null Object肯定是作为派生类的一个种类出现,并用于取代没有合适派生类可用的情形。比如,对象放在Map中,查找可能无效。

优点: 消除对无效对象的判断逻辑,提供系统的可靠性。

 
 

面向对象设计原则

  1. 依赖倒置原则(DIP)
  • 高层模块(稳定)不应该依赖于低层模块(变化),二者都应该依赖于抽象(稳定) 。
  • 抽象(稳定)不应该依赖于实现细节(变化) ,实现细节应该依赖于抽象(稳定)。
  1. 开放封闭原则(OCP)
  • 对扩展开放,对更改封闭。
  • 类模块应该是可扩展的,但是不可修改。
  1. 单一职责原则(SRP)
  • 一个类应该仅有一个引起它变化的原因。
  • 变化的方向隐含着类的责任。
  1. Liskov 替换原则(LSP)
  • 子类必须能够替换它们的基类(IS-A)。
  • 继承表达类型抽象。
  1. 接口隔离原则(ISP)
  • 不应该强迫客户程序依赖它们不用的方法。
  • 接口应该小而完备。
  1. 优先使用对象组合,而不是类继承
  • 类继承通常为“白箱复用”,对象组合通常为“黑箱复用” 。
  • 继承在某种程度上破坏了封装性,子类父类耦合度高。
  • 而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低。
  1. 封装变化点
  • 使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合。
  1. 针对接口编程,而不是针对实现编程
  • 不将变量类型声明为某个特定的具体类,而是声明为某个接口。
  • 客户程序无需获知对象的具体类型,只需要知道对象所具有的接口。
  • 减少系统中各部分的依赖关系,从而实现“高内聚、松耦合”的类型设计方案。
 

2/26

 

 

 Bridge模式

Decorator 模式

 

 

 一旦有接口,接口就得稳定

封装变化点

---- 使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合。

 

  模式依据其目的可分为创建型(Creational)、结构型(Structural)、或行为型(Behavioral)三种。创建型模式与对象的创建有关;结构型模式处理类或对象的组合;行为型模式描述类或对象之间的职责分配和交互。

 

 

 

 

 

课件&代码见:https://github.com/rhyspang/CPP-Design-Patterns

原文地址:https://www.cnblogs.com/cx2016/p/13276793.html