Gof设计模式总结


设计模式

1 OO基本原则

S.O.L.I.D

  • 单一职责原则
  • 开放封闭原则
  • 子类替换原则
  • 接口隔离原则
  • 依赖倒置原则

2 策略模式    行为型 对象

 

2.1 概述

定义一系列算法,分别封装起来,使它们之间可以相互替换。

2.2 适用性

  • 一些 相关 的类接口一致,仅仅是行为有异
  • 使算法使用的数据结构不暴露于客户
  • 一个类定义了多种行为,且这些行为在类中是以多个条件语句的形式出现的

2.3 结构

Gof设计模式总结_第1张图片

2.4 角色

  • Context

    维护一个Strategy对象的引用
    可开放接口,让Strategy访问其数据

2.5 优点

  • 定义一组可供重用的算法(共通内容可放置于基类)
  • 替代继承使用组合,更灵活,不用硬编码至Context中
  • 消除条件语句
  • 客户代码可以动态的选择具体算法

2.6 缺点

  • 客户代码必须了解到具体算法之间的区别(增加耦合)

    Gof建议:仅当不同行为是与客户相关的行为有关时,才使用Strategy模式

  • Strategy和Context之间的通信开销

    各具体算法所需要的参数不一样,但是接口共享。导致需要额外增加两边接口,导致这两个类更紧密的耦合

  • 增加了对象的数目

    参考Flyweight模式

2.7 实现

 

2.7.1 需定义Context和Strategy之间数据交换的接口

  • 方法一 Push:将数据作为参数放在Strategy的接口AlgorithmInterface()中。

    缺点:Context可能发送一些Strategy不需要的数据。

  • 方法二 Pull:Context将自身作为参数传递给Strategy,Strategy再调用Get获取数据。

    缺点:Context必须为Strategy定义一堆更精细的Get接口。(C++中可使用友元)

2.7.2 将Strategy作为C++模板参数

template <typename Strategy>
class Context
{
private:
    Strategy theStrategy;
public:
    void ContextInterface() {theStrategy.AlgorithmInterface()}
};
  • 不再需要Strategy抽象基类。
  • 避免多态,使用模板在编译时就绑定Strategy和Context,提高运行效率,牺牲了动态绑定的灵活性。

2.8 相关模式

3 状态模式    行为型 对象

 

3.1 概述

对象的行为随着内部状态的改变而改变。

3.2 适用性

  • 一个对象的行为取决于它的状态,并且需要在 运行时 根据它的状态改变它的行为
  • 大量的依赖于对象状态的分支 条件语句 是一个信号,通常可以用State模式进行改造。

3.3 结构

Gof设计模式总结_第2张图片

3.4 优点

  • 易扩展新的状态,只需定义新的子类
  • State对象可以被多个Context对象共享

    条件:状态 对象 不能持有自己的状态实例。需要将状态实例指定到一个静态变量中(可用单件模式实现)

    如果状态需要利用Context中的数据或方法,可在Handle()方法传入Context的引用。

    这种实现不再需要State类保存自身的引用,可实现没有内部状态只有行为的轻量级对象。

3.5 实现

 

3.5.1 谁定义状态转换

  1. 可由Context全权负责状态转移
  2. 通常由State具体类自身指定它们的后继状态更方便灵活

    可以给Context增加一个接口,让State子类对象显式地设定Context的内部状态。
    由State子类来指定状态转移的缺点是,增加了子类之间的依赖。

3.5.2 可使用表驱动法

  • State模式主要对状态相关的行为进行建模
  • 而表驱动着重于定义状态的转换,通常表的key表示某一状态,Value为它的后继状态。

3.5.3 创建和销毁State对象

  • 面临权衡:(1)需要时创建;(2)提前创建所有的State子类对象
    1. 将要进入的状态在运行时是不可知的,且上下文不经常改变状态时,选择(1)。
    2. 另外,当State对象存储大量的信息时,使用(1)。
    3. 当状态频繁变化时,第(2)种方法更好。Context对象需保存所有State子类对象的引用(不宜扩展)。

3.6 相关模式

3.6.1 与Strategy模式的区别

意图:

  • Strategy定义的是一组平行的算法,这些算法有着共同的目标。
  • State模式更关注根据内在状态的不同,执行不同的行为,这些行为可能目的完全不同。

客户角度:

  • State模式:通常的用法,状态通常跟着Context的行为而改变,对客户来说状态转换规则是不可见的。
  • Strategy模式:为了灵活,通常是由客户来指定具体的策略。

总结:

  • Strategy模式提供了一个继承之外更具弹性的替换方案。
  • State模式更多的用来避免Context中过多的分支语句。

4 观察者模式    行为型 对象

 

4.1 概述

定义对象之间一对多的依赖,当一个对象状态发生变化时,所有依赖于它的对象都得到通知。

4.2 结构

Gof设计模式总结_第3张图片

当一个观察者接收到改变指示后,流程图如下所示:

Gof设计模式总结_第4张图片

4.3 优点

  • Subject和Observer间是抽象耦合

    因为是非紧密耦合,Subject和Observer可以来自于系统中的不同的抽象层次
    低层次Subject一样可以通知高层次Observer,使用该模式不会破坏系统层次
    这就是抽象Subject和Observer的作用。

4.4 缺点

  • 来自某一观察者的意外更新

    某个观察者更新了主题的状态,导致其他观察者也发生了改变。
    如果更新准则定义或维护不当,常常会引起错误的更新。

4.5 实现

 

4.5.1 主题与观察者的关联方式

  • 主题跟踪观察者最简单的方式是保存观察者们的引用
  • 另一种方式是维护一份主题与观察者之间的映射表

4.5.2 观察多个主题

某些情况下,观察多个主题是有意义的,例如:一个表格对象依赖于多个数据源。
需要扩展Update接口使观察者知道是哪一个主题送来的。
主题可以简单的将自己作为观察者Update接口的参数,让观察者知道应去检查哪一个目标。

4.5.3 谁触发更新

Notify谁来调用?

  • 由主题对象的状态设定操作自动调用。
    • 优点:客户不需要调用Notify。
    • 缺点:多个连续的设定操作会产生多次连续更新,可能效率较低。(关键还是要看需求:在更新状态的时候是否需要通知)
  • 客户负责调用Notify
    • 优点:客户可以在一系列状态设定操作之后一次性通知更新。
    • 缺点:给客户增加了触发更新的责任。客户忘记的话,容易出错。

4.5.4 主题删除时,应通知观察者置空主题引用

 

4.5.5 在发出通知前,确保主题的状态自身是一致的

反例如下:

void MySubject::Operation (int newValue)
{
  BaseClassSubject::Operation(newValue);//先触发了通知
  _myInstVar += newValue;//后修改自身状态
}

可以使用模板方法发送通知来避免这种错误。(模板方法规定好修改状态和触发通知的顺序)

4.5.6 推拉模型的取舍

  • 推模型(大多数情况使用它)
    • Update参数传入的信息可能有很多,并非是所有观察者都需要的。
    • 主题对观察者所需要的信息的假定并不总是正确。
  • 拉模型
    • Update传入主题的引用。
    • 观察者自己向主题获取信息。
    • 缺点:可能需要调用多个接口以搜集全观察者自己需要的状态。(耦合度增加)

4.5.7 只关注感兴趣的改变

扩展主题的注册接口,加入interest参数

//主题
void Subject::Attach(Observer*, Aspect& interest);
//观察者
void Observer::Update(Subject*, Aspect& interest);

4.5.8 封装复杂的更新语义(ChangeManager)

当主题与观察者之间的依赖关系特别复杂时,
需要一个ChangeManager对象来维护这些关系。

目的:尽量减少观察者反映其主题的状态变化所需的工作量。
例子:如果一操作涉及到几个主题,就必须保证所有的主题都更改完了,再
一并通知它们的观察者。

该对象主要有 三个职责

  • 管理主题与观察者之间的映射表,提供接口来维护这个映射表。
  • 定义一个特定的更新策略。
  • 根据一个主题的请求,更新所有它的观察者。

详细参考:基于ChangeManager的Observer模式

4.6 扩展

基于ChangeManager的Observer模式

Gof设计模式总结_第5张图片

具体更新策略由具体的ChangeManager来决定:

  • SimpleChangeManager总是更新每一个主题的所有观察者
  • DAGChangeManager实现多个主题变更时,只更新观察者一次

4.7 相关模式

  • ChangeManager是一个Mediator模式的实例
  • ChangeManager通常是Singleton模式

5 模板方法    行为型 

 

5.1 概述

最基本的设计模式,代码复用的基本技术
定义一系列算法的骨架,将其中的一些步骤延迟到子类中。
使子类可以不改变一个算法的结构,而重定义算法的某些特定步骤。

5.2 适用性

  • 多个子类中存在一些公共行为,需要提取出来,做法如下:
    1. 识别代码中不同部分
    2. 提取出新的函数
    3. 用一个新的模板方法替换原算法(公共部分放于其中)
  • 控制子类扩展,模板方法只在特定点调用子类方法

5.3 结构

Gof设计模式总结_第6张图片

5.4 优点

  • 提供了反向的控制结构。即"好莱坞法则":"别找我们,我们找你。"。

    即高层组件调用低层组件,低层组件不能调用高层组件。
    但并非低层组件一定不能调用高层组件,最重要的是避免让
    高层组件和低层组件之间有明显的环状依赖。

  • 一个模板方法整合了一系列操作,从而减少了需要客户程序调用的接口数。
  • 客户代码只依赖于模板方法基类,不依赖于具体类,减少整个系统的 依赖

5.5 实现

  • hook operations 提供缺省1的行为,子类在必要时拓展。例如:
    void AbstractClass::TemplateMethod()
    {
      Operation1();
      Operation2();
      Hook1();
      if (HookFileExisted())
        {
          Operation3();
        }
    }
    
    bool AbstractClass::HookFileExisted()
    {
      return true;
    }  
    

    重要 :模板方法应该指明哪些操作是钩子(可被重定义),哪些操作是抽象操作(必须被重定义)。
    可以做一个命名约定
    需被重定义的操作加上前缀"Do"
    钩子方法加上前缀"Hook"

5.6 相关模式

  • Factory Method常被模板方法调用。
  • Strategy使用委托来改变整个算法,模板方法使用继承来改变算法的一部分。

6 装饰者模式    行为型 对象

 

6.1 概述

动态地给一个对象添加一些额外的职责。提供了比继承更大的灵活2

6.2 结构

Gof设计模式总结_第7张图片

  • 使用继承的目的是为了达到类型匹配,使用户在使用Decorator对象时,与使用Component一样。

6.3 优点

  • 比静态继承更灵活,在运行时添加职责。
  • 继承在添加一些共通职责时,容易产生类爆炸。Decorator添加的职责大多数情况下能重用。
  • 使结构层次较高的类更简洁。不依赖于现有已扩展的Decorator类,定义新类型的Decorator很容易。

6.4 缺点

  • 使用装饰时不应该依赖于对象标识。被装饰了的组件与这个组件本身就对象标识而言,是有区别的。
  • 产生很多小对象。对于不了解系统的人,难以学习,排错也比较困难。

6.5 实现

  1. 接口一致性。所有的Component和Decorator必须有一个公共的父类。
  2. 抽象的Decorator基类是可选的。仅需添加一个职责时,无需Decorator基类。
  3. 保持Component类的简单性。公共父类仅定义接口,尽量避免加入子类并不需要的职责。

6.6 相关模式

 

6.6.1Strategy的比较:

  • Decorator可看做一个对象的 外壳
  • Strategy则是改变对象的内核。

    当Component基类很 庞大 时,使用Decorator代价太高,Strategy模式更好一些。
    比如,绘制边框的职责,既可以使用Decorator模式包一层外壳,
    也可以使用Border对象专门负责,再组合进Context。

6.6.2 Composite模式

可以将装饰视为一个退化的、仅有一个组件的组合。
另外,它的目的在于添加职责,而Composite目的在于对象聚合。

7 单件模式    创建型 对象

 

7.1 概述

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

7.2 适用性

  • 当类只能有一个实例
  • 当这个唯一实例需要通过子类化扩展

7.3 结构

Gof设计模式总结_第8张图片

7.4 优点

  • 受控访问
  • 起到命名空间的作用

    对全局变量的一种改进,全局变量会污染名空间(容易重名)。
    支持静态类的语言,使用静态类解决该问题更简单。

  • 可以被 继承 扩展。
  • 可扩展单例为 多个实例

    允许Singleton类可以管理多个实例(池类技术)。

7.5 实现

class Singleton
{
public:
    static Singleton* GetInstance();
protected:
    Singleton() : _instance(NULL);//隐藏的构造函数
private:
    static Singleton* _instance;
};

Singleton* Singleton::GetInstance ()
{
    if (_instance == NULL) _instance = new Singleton;
    return _instance;
}

7.5.1 同步问题

为了保证在多线程环境下只创建一个实例,需要对GetInstance方法做同步处理。

简单的方法:直接将GetInstance方法声明为synchronized。

这样的做法有个问题:
我们需要同步的只是GetInstance内部负责创建实例的区块,
对整个函数进行同步,如果函数体内内容较多且外部调用很频繁,
开销会很大。

应该只同步创建实例的区块(java示例):

public class Singleton
{
    private static Singleton uniqueInstance;
    private Singleton() {}
    public static Singleton GetInstance() {
        if (uniqueInstance == null) { //判断是否要进入负责创建实例的同步模块
            synchronized (Singleton.class) {//仅一个线程执行此区块,确保只创建一个实例。
                if (uniqueInstance == null) { 
                    uniqueInstance = new Singleton();//对于同步数据,当你的写入依赖于读取的内容的时候,要小心。
                }
            }
        }
        return uniqueInstance;
    }
}

7.5.2 继承问题

问题:子类的单件实例化在何处实现?

  • 在父类的GetInstance中决定使用哪一个单件子类。

    可以传入参数,使用条件语句在运行时期选择适合的子类。
    局限在于硬性限定了可能的Singleton子类的集合。
    优点:支持多态,运行时指定子类。

  • 将GetInstance类从父类中剥出,并将它放入子类。

    客户代码通过类名调用GetInstance自行决定使用哪个子类。
    编译时决定使用哪个子类,非运行时,不够灵活。

  • 使用设定文件(或注册表等)记录单件类。

    GetInstance()读取相关配置项,通过映射表找到相对应的单件类。

7.6 与静态类比较

 

7.6.1 概念上的理解

静态类是单件模式的一种特殊实现方式。

  • 静态类更多的用于与特定实例无关的 全局 属性和 全局 方法的分类(起到命名空间的作用)。
  • 而单件的概念是确实需要一个实例,而且实例只能有一个。比如:注册表对象,线程池对象。

7.6.2 创建的时间

  • 静态类在编译时创建
  • 单件模式的类在运行时创建(创建的时机在一定程度上可选)

7.6.3 扩展性

  • 静态类不能被继承,也无法继承其他类。(如果该类需要实现一些接口,则不能使用静态类)
  • 单件类可以被继承扩展
  • 如需要从一个实例变为多个实例,静态类做不到。单件类可以扩展满足要求 更灵活

7.6.4 总结

  • 静态类更多地用于对全局方法、全局变量的分类组织。
  • 单件模式表示有且仅有一个对象。单件类可以被继承,易于扩展。

当对于是否使用单件模式没把握的时候,使用单件类更好一些。
原因:静态类改成实例类,会改变接口,从而影响所有的客户代码。

7.7 相关模式

经常使用Singleton模式的其他模式:

8 工厂模式    创建型 对象

 

8.1 简单工厂方法

 

8.1.1 结构

Gof设计模式总结_第9张图片

8.2 工厂方法

 

8.2.1 概述

定义一个用于创建对象的接口,让子类决定实例化哪个产品。

8.2.2 结构

Gof设计模式总结_第10张图片

8.2.3 实现

  • 避免子类化

    工厂方法一个潜在的问题是它们可能仅为了创建适当的Product对象
    而迫使你创建Creator子类,C++中可以提供使用模板避免子类化。

    class Creator
    {
    public:
        virtual Product* Create() = 0;
    };
    
    template <class T>
    class StandardCreator : public Creator
    {
    public:
        virtual Product* Create();
    };
    
    template <class T>
    Product* StandardCreator<T>::Create()
    {
        return new T;
    }
    

8.2.4 相关模式

  • Abstract Factory经常使用工厂方法来实现。
  • 工厂方法通常在Template Methods中被调用。

    模板方法指定一系列的具体步骤,而创建对象的一步委托给工厂方法。

  • Prototypes不需要创建Creator的子类。

    但会要求一个针对Product类的Initialize操作。Creator使用Initialize来初始化对象。

  • 与简单工厂方法的比较

    简单工厂在SimpleFactory的create方法中,使用类似Switch语句来根据参数制造产品。
    缺点在于,switch不容易扩展,并且SimpleFactory需要知道所有的产品类,耦合紧密。

8.3 抽象工厂

 

8.3.1 概述

提供创建一系列产品族的接口,而无需指定各产品的具体类。

8.3.2 角色

  • ConcreteFactory

    负责创建各产品对象,每一个具体工厂类都代表一种产品之间的组合。

8.3.3 结构

Gof设计模式总结_第11张图片

8.3.4 优点

  • 使得易于交换产品系列

    通过替换具体的工厂类,来改变产品系列。

  • 有利于产品的一致性

    当一系列产品被设计成一起工作时,抽象工厂可以保证一个应用一次只能使用同一系列的对象。

8.3.5 缺点

  • 难以支持新种类的产品

    AbstractFactory接口定义了可以被创建的产品集合。支持新的产品种类,
    就需要扩展接口,还涉及到所有子类的改变。解决办法

8.3.6 实现

  • 将具体工厂作为单件

    一般每个产品系列只需一个ConcreteFactory的实例。

  • 创建产品。

    AbstractFactory只声明创建产品的接口。
    如果有多个可能的产品系列,具体工厂也可以使用Prototype模式来实现。
    具体工厂使用产品系列中每一个产品的原型实例来初始化,
    且它通过复制它的原型来创建新的产品。

    基于原型的好处:不是每个新的产品系列都需要一个新的具体工厂类。

  • 定义可扩展的工厂

    加入新产品需要扩展接口,影响子类。
    一个更灵活但不太安全的设计是给创建对象的操作增加一个参数。
    AbstractFactory只提供一个Create操作,用参数指定要创建的产品。
    由于产品种类各不相同,此方法只适用于动态类型语言。

    当所有对象都有相同的基类,且产品对象可以安全的强转成正确的
    类型时。才能在C++这样的静态类型语言中使用。

    此方法有个本质的问题,因为返回的都是Object基类,客户无法区分
    或对一个产品类别进行安全的假定。需要dynamic_cast去转换,这种
    自上向下类型的转换并不总是安全的。

    总结:这是一个典型的高度灵活和更高安全性的权衡问题。

8.3.7 相关模式

9 命令模式    行为型 对象

 

9.1 概述

将请求封装成对象,实现统一的Execute()接口,从而可以使用不同的请求
实例对其他对象进行参数化。

典型的例子:
Button控件,对控件设计者来说,只知道Button按下应该会发生
些什么,但具体会发生什么一无所知。只能由使用者来决定。

9.2 适用性

  • 回调机制 的一个面向对象的替代品
  • 支持对请求排队
  • 支持撤销操作

    Excute()在实施操作前记录状态,Undo()利用该记录状态取消之前执行的操作。
    将执行完的命令对象加入一个历史列表,可通过 向前/向后遍历 实现
    一系列的 Undo/Redo

  • 命令对象支持 持久化

    方法:添加Store()和Load()接口
    在执行一些列命令前,调用Store()对命令对象进行序列化和持久化操作。
    一旦系统崩溃,可以使用Load()复原命令对象,并重新执行。

  • 支持事务处理

9.3 结构

Gof设计模式总结_第12张图片

9.4 角色

  • Client

    负责创建具体命令对象并指定它的接收者。
    存储命令对象到某个媒介。

  • Invoker 从存储媒介中获取命令对象,并执行。

9.5 优点

  1. 增加新的Command很容易。
  2. 将调用命令的对象与知道如何实现该命令相关操作的对象解耦。
  3. Command对象和其他对象一样支持扩展。
  4. 支持MacroCommand。复合命令是Composite模式的一个实例。

9.6 实现

  • 一个命令对象职责可大可小。
    • 最小职责仅确定一个接收者和执行该请求的动作
    • 职责也可以大到负责处理所有的功能,不需要接收者,直接包含具体动作。(当没有合适的接收者时使用)
  • 实现undo和redo

    ConcreteCommand类需要存储额外的状态信息,包括:

    • 接收者对象
    • 接收者接口执行操作的参数
    • 接收者的状态值
  • 使用C++模板

    好处:避免每一个动作和接收者都创建一个Command子类。
    问题:1) 不支持撤销操作 2) 无法向接收者的执行接口传入参数

9.7 相关模式

  • Composite可被用来实现宏命令。
  • Memento模式可用来保持一个状态,命令对象用该状态来取消之前执行效果。

10 适配器模式     对象 结构型

 

10.1 概述

将一个或多个类的接口转换成用户希望的接口。别名Wrapper。
现有类的接口与用户希望的接口通常是固定的,无法改变。

10.2 结构

 

10.2.1 类适配器

Gof设计模式总结_第13张图片

10.2.2 对象适配器

将Adapter与Adaptee之间的继承关系变为了组合。

Gof设计模式总结_第14张图片

10.3 角色

  • Target 定义了满足用户需要的接口

10.4 实现细节

 

10.4.1 类适配器还是对象适配器?

  • 重定义Adaptee的行为

    类适配器可以方便地重定义Adaptee的部分行为。
    对象适配器可能需要通过Decorator模式先拓展Adaptee。

  • 适配多个Adaptee?

    类适配器只能适配一个Adaptee。
    对象适配器支持多个。

10.4.2 双向适配器增加透明性

适配器因为改变了接口,Adapter对象与Adaptee对象不兼容(提示:Decorator兼容)。
原本使用Adaptee对象的用户就无法使用Adapter对象。
可使用双向适配器,在实现Target的同时,保留原本Adaptee的接口。

10.5 相关模式

  • Bridge模式的结构与其有些相似,但意图不同。Bridge的目的是将接口部分与实现部分分离。
  • Decorator模式为类增加职责,不改变 原先 的接口。透明性比Adapter好,并支持递归组合。
  • Proxy模式在不改变其接口的条件下,为另一个对象定义了一个代理。
  • Facade模式将一个或多个不同对象的复杂接口进行简化。

11 代理模式    结构型

 

11.1 概述

控制和管理访问

11.2 适用性

  1. 远程代理 代理类隐藏网络层的实现,本地调用代理类就如同调用本地对象一样。
  2. 虚代理 创建开销很大的对象时使用。代理类隐藏创建的细节。
  3. 保护代理 用于权限控制。
  4. 智能指针

11.3 结构

Gof设计模式总结_第15张图片

11.4 角色

  • Proxy

    控制对实体的存取,并可能负责创建和删除实体。

  • Subject

    定义RealSubject与Proxy的共用接口。

11.5 实现细节

 

11.5.1 C++通过重载->,*运算符实现

Image* ImageProxy::operator-> ()
{
    return LoadImage();
}
Image& ImageProxy::operator* ()
{
    return *LoadImage();
}

int main()
{
    ImageProxy imageptr;
    imageptr->Draw();//此处实际调用的是Image的方法
    (*image).Draw();
    return 0;
}

11.5.2 远程代理

远程代理不一定都是通过网络调用的,不同地址空间的对象访问也是远程代理。
远程代理一般需要将对象、调用信息序列化,通过Socket等协议,通知远程的
服务,然后有远程提供服务的程序,调用实体对象。

Java中有成套的解决方案,叫做RMI。

11.5.3 智能指针

  • 对指向实际对象的引用计数,引用计数为0时,自动释放。
  • 第一次引用时,装入内存。
  • 访问实际对象前,检查被锁定。

标准库的例子:

#include <memory>
using namespace std;
class A {};
void f()
{
    auto_ptr<A> ptr(new A);//栈区对象,出栈时释放指针,避免多个函数出口都写释放语句
    try
    {
        //delete a;
        return;
    }
    catch (...)
    {
        //delete a;
    }
    //delete a;
}

11.5.4 虚代理

对于一些开销很大的对象,可能在实际真正用到的时候,才创建对象。
例如:ImageProxy构造中什么都不做,而在Draw的接口中,才真正创建Image对象。

11.6 相关模式

  • Adapter模式:

    主要用于转换接口;而代理模式一般情况下不改变接口,意图不一样。

  • Decorator模式:两者结构类似,但意图不同。

    装饰者模式支持多层装饰;而代理通常只会添加一层访问控制。
    代理模式通常与实际对象接口保持一致。装饰者通常需要增加接口以达到扩展功能的目的。

12 外观模式    结构型

 

12.1 概述

为子系统中的一组接口进行简化,提供一组高级接口,使得子系统更加容易使用。

12.2 适用性

  • 为复杂子系统提供一个简单接口,对大部分用户来说足够用,必要时用户一样可以绕过该接口。
  • 使客户程序从子系统的各层次实现的细节中解脱出来。
  • 多层次结构,可以使用Facade模式定义每一层的抽象操作。可以让各层次之间通过facade进行通信,简化了各层次之间的依赖关系。

12.3 结构

Gof设计模式总结_第16张图片

12.4 优点

  • 实现了用户与子系统之间的 松耦合 关系
  • 对用户屏蔽子系统结构,更易用

12.5 实现细节

  • 使用抽象类实现Facade可以进一步降低客户与子系统的耦合度。
  • C++使用Namespace可以私有化子系统中的类。

12.6 相关模式

  • Abstract Factory模式可与Facade模式一起使用以提供一个单独的创建产品簇的接口。
  • Mediator模式与Facade模式的相似之处:都抽象了一些已有的类的功能。但Mediator的目的是对同级之间的任意通讯进行抽象。
  • 通常来说仅需要一个Facade对象,所以Facade类定义成Singleton类。

13 迭代器模式    结构型

 

13.1 概述

提供遍历集合对象中各元素的方法,并且不将集合具体的数据结构暴露给用户。

13.2 适用性

  • 遍历访问集合对象的内容,无需暴露它的内部结构。
  • 支持对同一集合对象的多种遍历方式。
  • 为遍历不同数据结构的集合对象提供统一的接口(即支持多态迭代)。

13.3 结构

Gof设计模式总结_第17张图片

13.4 角色

  • Iterator

    定义访问和遍历元素的接口

  • ConcreteIterator
    • 实现Iterator定义的接口
    • 在遍历集合时,跟踪当前位置
  • Aggregate

    定义创建迭代器对象的接口

  • ConcreteAggregate

    实现Aggregate定义的接口

13.5 优点

  • 支持以不同的方式遍历一个集合,使改变遍历算法变的容易。
  • 迭代器将遍历的职责从集合类中剥离出来。维护起来更容易。
  • 可以同时对一个集合进行多个遍历,只需多个迭代器实例对象。

13.6 实现细节

 

13.6.1 由谁来控制迭代过程?

由客户来控制的称为外部迭代器(或称为主动迭代器)。
由迭代器自身来控制的,称为内部迭代器(或称为被动迭代器)。

  • 外部迭代器

    使用外部迭代器时,客户必须主动推进迭代的步伐。

  • 内部迭代器

    使用内部迭代器时,客户只需指定一个操作,迭代器保证对集合
    中的每一个元素执行该操作。3
    如何指定操作?支持匿名函数和闭包的语言很容易实现。
    C++中通常有两种方法可以选择

    • 函数指针 劣势在于如果需要更新某种状态,则需要使用全局变量。
    • 子类生成 需要定义额外的类来达到目的。4
  • 权衡

    内部迭代器定义好了迭代逻辑,使用起来更方便;
    外部迭代器由于将迭代逻辑交由用户来控制,使用起来更灵活。

13.6.2 谁定义遍历算法?

  • 由集合自身定义

    由集合自身定义遍历算法。迭代器仅用来指示当前的位置。这种迭代器称为 游标

    客户调用Next()时,需要将游标作为参数传入,Next操作内部仅改变游标的位置状态。
    可改接口为SetCursor(index)和int GetCursor()更容易理解。

  • 由迭代器定义

    遍历算法还可以由迭代器定义,优势在于,使得在相同的集合上使用不同的迭代算法、
    或是在不同的集合上使用相同的迭代算法更简单。

    注意:如果遍历算法会用到集合的私有变量,放在迭代器中,则破坏了集合对象的封装性。

13.6.3 线程安全的迭代器

现实情况下,可能有多个不同线程创建的迭代器引用同一个集合对象。

解决同步问题的一般做法是:
各迭代器对象需要向集合对象进行注册(可用Observer模式),
当改变发生时,集合对象更新每一个迭代器的状态。

13.6.4 关于多态迭代器

  • 结构图中所展示的是多态迭代器的实现

    也可以不需要迭代器抽象基类,这样在 工厂方法CreateIterator
    也就不需要动态new出迭代器具体类对象。

  • 多态迭代器是有代价的

    因为 动态 的分配迭代器对象的本身是有代价的。
    一般情况使用分配在栈区上的具体迭代器即可。

  • 多态意味着需要用new,也就需要用户负责删除它们,这样容易引发错误。

    可以使用Proxy模式,在栈区创建一个代理迭代器对象,在代理迭代器析构中
    释放具体迭代器对象。不能用工厂是因为工厂只负责对象的创建。

    IteratorProxy::IteratorProxy(Type type)
    {
        if (type == Type.Reverse) Iterator* m_iter = new ReverseIterator();
        ...
    }
    
    IteratorProxy::~IteratorProxy()
    {
        delete m_iter;
    }
    
    int main()
    {
        IteratorProxy iter(Type.Reverse);
        iter.next();
        ...
        return 0;
    }
    
    
  • 仅在必须要使用多态时才使用。

13.6.5 迭代器与集合的紧密耦合

迭代器一般作为集合的一个扩展,两者之间是紧密耦合的。

  • 利用C++友元实现

    C++中迭代器可作为它的集合类的一个友元,
    这样集合类中就不必定义一些只有迭代器才用的到的方法。
    当然这破坏了集合类的封装性,但这点仅仅是针对迭代器而言的。

    问题:
    当定义新的ConcreteIterator(为了增加新的遍历方式)时,需要为集合类加上另一个友元。

    解决办法:
    为避免该问题,集合类可定义迭代器父类为友元,
    迭代器子类通过包含一些protected操作,来访问集合类非公共可见成员。

13.6.6Composite模式的协作

 

13.6.7 空迭代器

用于处理边界条件。
一个NullIterator的IsDone()总是返回true,或者HasNext()总是返回false。

提示:
空迭代器更多的用于处理树形结构的集合。
叶节点通常需要一个NullIterator。

13.7 相关模式

  • 迭代器可在Composite模式这样的递归结构上使用。
  • 多态迭代器可以通过Factory Method模式来实例化迭代器子类。
  • 迭代器可使用一个memento来捕获一个迭代状态,即迭代器内部存储memento

14 一些OO提示

 

14.1 活用空对象来避免null值检查

class Object
{
public:
    vitual void DoSomething() = 0;
};

class NullObject : public Object
{
public:
    void DoSomething();
};

void NullObject::DoSomething()
{
    //do nothing
}

Footnotes:

1 hook操作缺省经常是一个空操作。空操作的意义:某些子类可能需要一些"特别"的操作,而大部分子类不需要。

2 继承是在编译时静态扩展父类的职责,装饰者模式是动态的添加职责。

3 实现MapReduce中"Map"的一种方式。

4 具体代码示例参照《Gof设计模式》5.4 10

你可能感兴趣的:(设计模式)