What & How & Why

C++设计模式 第二周

本页内容是 Boolan C++ 开发工程师培训系列的笔记。
因个人水平有限,我撰写的笔记不免出现纰漏。如果您发现错误,请留言提出,谢谢!


设计模式:对象创建

传统的对象创建一般是通过栈上创建或者是 new 在堆上创建。但不管哪种方式,对象创建的时候都必须要有一个具体的类;这样的对象创建过程是紧耦合的。我们可以通过对象创建类型的设计模式来避免这样的紧耦合,从而支持对象创建的稳定。

对象创建类型设计模式的典型例子有:

  • Factory Method
  • Abstract Factory
  • Prototype
  • Builder

Factory Method

在软件设计中,经常要面临创建对象的工作。由于需求的变化,需要创建的对象往往也是需要变化的。我们知道,传统常规意义上的对象创建方法会导致紧耦合,那么有没有一种方法来避免这样的紧耦合呢?

来看一个具体的例子。我们在前面分析过一个文件分割的例子;现在我们假设这个文件分割的算法有好多种。那么如果从变化的场景来考虑的话,这个问题应该怎么解决?

从面向对象设计的角度来考虑,我们应该设计一个抽象基类,在子类创建对象的过程中使用基类对象来创建,这样可以保证创建对象时声明的类的类型和对象都是抽象的。这样的声明可以让我们将对象的类型判断放置到运行时去决定。当然,针对这样的写法,我们需要通过子类继承基类,然后在子类中重写对象创建的定义来实现,比如如下代码结构:

class ISplitter {
public:
    virtual void split()=0;
    virtual ~ISplitter(){}
};
class BinarySplitter : public ISplitter { .... };
class TxtSplitter: public ISplitter { .... };
class PictureSplitter: public ISplitter { .... };
class VideoSplitter: public ISplitter{ .... };
这样的写法在构思上是没有问题的。我们设计类的时候,一般都会用抽象类来代替具体类。这样的写法是严格遵循依赖倒置原则的,我们需要将对象的创建依赖到一个稳定的类型上。这样的编程手法,我们称之为面向接口编程。不过如果按照这样的写法,我们会发现一个问题:抽象类是不能直接用于对象创建的。如我要创建一个 BinarySplitter 的对象,不管是在栈上创建还是在堆上用 new 创建,我们都必须指定具体的类型:
BinarySplitter splitter; //create object on stack
ISplitter * splitter = new BinarySplitter(filePath, number) ;
这样的写法必然要求编译时的细节依赖,显然是违反依赖倒置原则的。那我们应该怎么做呢?

Factory Method 就是用于解决这个问题的。

在上面的代码中,我们可以看到:如果使用 new,那就必然会产生对具体类型的依赖。因此解决这个问题的关键就是要避开使用这个 new。对于下面代码来说:
ISplitter * splitter = new BinarySplitter(filePath, number) ;
赋值操作符左边已经通过抽象类实现了面向接口编程,因此我们只需要思考如何将右边的内容也实现面向接口编程就可以了。我们想到,对象除了被创建,还是可以作为函数的返回值返回的;因此我们可以尝试将对象的创建设计为一个函数(方法),然后用该函数返回创建好的对象:
class SplitterFactory {
public:
    ISplitter* CreateSplitter() {
        return new BinarySplitter(filePath, number); 
    }
};
但是这样写还是没有解决根本的问题:
SplitterFactory factory;
ISplitter * splitter = factory.CreateSplitter();
我们发现 CreateSplitter() 也需要依赖 BinarySplitter 的;也就是说,这里的创建过程也是一个间接的具体依赖。也就是说,我们必须要想办法使 SplitterFactory 类摆脱对具体类型的依赖。这又要怎么解决呢?

其实换一下思路,我们现在无非就是需要避免编译时的具体类型依赖。那么顺着这样的思路,我们很快就能想到,如果可以把 CreateSplitter() 中的对象类型决定丢到运行时去决定的话,是不是就可以了?

C++中怎样才能使类型依赖延迟?没错,就是虚函数和多态。既然 SplitterFactory 不能在编译时刻依赖具体类型,那么我们就把它做成抽象基类就好了:
class SplitterFactory {
public:
    virtual ISplitter* CreateSplitter() = 0;
    virtual ~SplitterFactory();
};
既然 SplitterFactory 已经是纯虚基类了,那么我们是不是可以用指针来调用 CreateSplitter() 了?
SplitterFactory *factory;
ISplitter * splitter = factory -> CreateSplitter();
啊哈,这个不就是多态的标准模样吗?

那剩余的问题就是处理我们的基类对象 factory 了。这和之前我们看到的,通过多态重写创建的子类是同样的写法了:
class BinarySplitterFactory: public SplitterFactory {
public:
    virtual ISplitter* CreateSplitter(){ return new BinarySplitter();}
};
class TxtSplitterFactory: public SplitterFactory {
public:
    virtual ISplitter* CreateSplitter() { return new TxtSplitter(); }
};
class PictureSplitterFactory: public SplitterFactory {
public:
    virtual ISplitter* CreateSplitter(){ return new PictureSplitter(); }
};
class VideoSplitterFactory: public SplitterFactory{
public:
    virtual ISplitter* CreateSplitter() { return new VideoSplitter(); }
};
到此,我们发现我们每一个类都有一个具体的工厂实现。现在的流程,就从我们直接去创建一个具体类型的对象,改为了交给我们的工厂基类去创建;而工厂基类通过子类的重写来创建指定类型的对象了。形式上,我们将这样的使用方法称为多态 new

当然,你可能会说,在具体的 Factory 的实现中,也是有具体依赖的,那是否也违反了依赖倒置原则?

就该软件整体的设计来说,这是没有关系的。通过上面的设计,我们在创建对象的过程中,再也没有需要具体依赖的类型了。我们的创建过程,依赖的是一个稳定的抽象类。而我们设计的理念,并不是要把变化都消灭掉,而是把这些变化都约束到一个指定的区域,使得整个程序更加有序,可控。这也是 Factory Method 处理该问题的思想。

来看一看 Factory Method 设计模式的定义吧:

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


<html>

<img src=“/_media/programming/cpp/boolan_cpp/factorymethod2.svg” width=“900”/>

</html>

总结:

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

Abstract Factory

Abstract Factory 抽象工厂模式是从 Factory Method 工厂模式中衍生出来的;它和 Factory Method 唯一不同的区别在于, Factory Method 是对单一对象进行逐个创建处理,而 Abstract Factory 需要处理一系列互相依赖的对象的创建工作。

什么叫“一系列互相依赖的对象”?我们把这个条件分解一下,可以得到两个结论:

  • Abstract Factory 处理的对象创建有很多个
  • Abstract Factory 处理的对象是具有相互依赖的特性的


接着来看看具体的例子:

假设我们需要设计一个数据库连接系统。数据库的一般连接实现都分为了好几个步骤。如果只考虑具体的实现方法,那么一个数据库连接系统的基本思路是这样的:

//create connection
SqlConnection* connection = new SqlConnection();
    connection->ConnectionString = "...";
//using commend
SqlCommand* command =  new SqlCommand();
    command->CommandText="...";
    command->SetConnection(connection);
//read data
SqlDataReader* reader = command->ExecuteReader();
    ....
这是一个具体的实现(SQL数据库的实现);考虑到根据数据库的不同;那么 new 的对象也会相应的更改。因此,这是一个需要创建很多个对象,并且对象有变化的需求。

这让人不经想到了工厂模式:要把创建对象的过程抽象化,稳定化,工厂模式是搞的定的。因此,按照工厂模式的思路来,首先应该就是按照面向接口编程的方法,把所有的具体实现抽象出类(接口)来,并配套上对应的工厂抽象类:
class IDBConnection{ .... };
class IDBConnectionFactory{
public:
    virtual IDBConnection* CreateDBConnection()=0;
};
class IDBCommand{  
};
class IDBCommandFactory {
public:
    virtual IDBCommand* CreateDBCommand()=0;
};
class IDataReader{ .... };
class IDataReaderFactory {
public:
    virtual IDataReader* CreateDataReader()=0;
};
按照工厂模式的实现方法,我们需要对上面每一个工厂抽象类进行具体的实现。比如我们要实现SQL数据库的具体链接方法,就需要写出如下的代码:
//sql connection implementation 
class SqlConnection: public IDBConnection{ .... };
class SqlConnectionFactory:public IDBConnectionFactory { .... };
//sql commend implementation 
class SqlCommand: public IDBCommand { .... };
class SqlCommandFactory:public IDBCommandFactory { .... };
//sql data read implementation 
class SqlDataReader: public IDataReader { .... };
class SqlDataReaderFactory:public IDataReaderFactory { .... };
到这里问题似乎解决了;具体的实现已经交给工厂的多态去处理了。但实际上这段代码有很大的问题。仔细想一下数据库的链接方式,我们发现以上这三个方法实际上需要配套使用的。举个例子:如果我们使用 sql 的 connection 对象,那我们也必须同时使用 sql 配套的 command 对象去操作它。这也就是我们前面提到的一系列互相依赖的对象的一个典型例子。

反观我们在抽象类中写出的代码,我们发现因为我们将所有的类型决定交给了运行时处理,因此在运行时的类型决定,其实是我们自己说了算的。

也就是说,针对 IDBConnectionFactoryIDBCommandFactoryIDataReaderFactory 这三个具体实现,我可以写出任意的具体实现组合,比如 Sqlconnection 加上 Oraclecommand 加上 Mysqlreader。。 这样简直就是乱套了嘛。

那应该怎么去修改呢?其实很简单,把这三个工厂合并为一就可以了。既然这些对象互相依赖,那么很显然,他们可以在一个序列里实现:
class SqlDBFactory:public IDBFactory{
public:
    virtual IDBConnection* CreateDBConnection() = 0;
    virtual IDBCommand* CreateDBCommand() = 0;
    virtual IDataReader* CreateDataReader() = 0;
};
//concrete stuffs
class SqlConnection: public IDBConnection { .... };
class SqlCommand: public IDBCommand { .... };
class SqlDataReader: public IDataReader { .... };

看到这里,你也应该明白这实际上就是一个特化的,用于处理多个相互依赖对象的工厂模式了。这就是 Abstract Factory 的核心概念:将一系列互相依赖的对象整合到一起使用工厂处理。它在本质上也是属于工厂模式的一种,但因为牵涉到多个相互依赖对象,实际上的处理又显得稍有不同。

来看看 Abstract Factory 的具体定义吧:

提供一个接口,让该接口负责创建一系列的“相关或者相互依赖的对象”,无需指定他们具体的类。
——《设计模式》GoF


<html>

<img src=“/_media/programming/cpp/boolan_cpp/abstractfactory.svg” width=“1000”/>

</html>

总结:

  • 如果没有应对“多系列对象构建”的需求变化,则没有必要使用 Abstract Factory 模式,使用简单的工厂完全足以胜任。
  • “一系列对象”指的是在某一特定系列下的对象之间具有相互依赖或作用的关系。不同系列的对象之间是不存在相互依赖的关系的。
  • Abstract Factory 模式主要在于应对以系列作为变化的单位的需求变动。相对于处理单个对象的工厂模式来说,它的缺点在于难以应对以对象为单位的需求变动上。

Prototype

有些时候我们需要创建一些很复杂的对象:比如构造函数的参数一大堆,而我们又看不懂参数到底在说啥(STL里很多这样的东西);或者比如我们创建了一个对象,然后慢慢的添砖加瓦做到了相当的复杂程度。某个时间我们想获取一个该对象的副本,发现原有的工厂方法创建的对象比修改过的这个差远了,要想弄还得一步一步再加上去;或者在开始设计软件的时候,你压根就不知道你未来需要添加什么样的功能。

在这些情况下,使用 Prototype 原型模式是一种不错的解决方法。Prototype 模式也属于对象创建的范畴;具体的说,它也是工厂模式的一个变种。但相比起工厂模式, Prototype 模式更强调克隆。这个特点造就了该模式非常适用于想获得与已存在对象非常相似的新对象的状况。Prototype 会直接定义一个原型对象,这个原型对象往往是复杂的,而且会剧烈变化的;这样的情况导致一开始就设计好这个对象的话,代价会非常高。在 Prototype 模式下,我们只需要建立一个原型对象,之后所有的对象创建都可以基于这个原型对象来实现。

Prototype 模式的实现非常简单。相比起工厂模式需要接口加工厂抽象类的结构,Prototype 将这两者整合到了一起:

class ISplitter {
public:
    virtual void split()=0;
    virtual ISplitter* clone() = 0; // 
    virtual ~ISplitter() {}
};
上面这个 clone() 方法取代了以前的 CreateSplitter() 成为了新的对象创建方法。通过对 clone() 方法的重写,我们就可以准确的返回一个具体类型对象的拷贝了。这个新对象的创建过程是通过子类重写拷贝构造函数来实现的,这也是 Prototype 模式最大的特点。在后续的编码过程中,如果某个类需要实现 Clone 功能,就只需要继承原型类,然后重写自己的默认复制构造函数就好了:
/* Concrete class */
class BinarySplitter : public ISplitter{
public:
    virtual ISplitter* clone() { return new BinarySplitter(*this); }
};
class TxtSplitter: public ISplitter{
public:
    virtual ISplitter* clone() { return new TxtSplitter(*this); }
};
class PictureSplitter: public ISplitter{
public:
    virtual ISplitter* clone() { return new PictureSplitter(*this); }
};
class VideoSplitter: public ISplitter{
public:
    virtual ISplitter* clone() { return new VideoSplitter(*this); }
};
需要注意的是,在使用的时候,原型对象是不能直接用于调用方法的。我们必须先创建原型对象的拷贝,再用这个拷贝去调用方法:
ISplitter * splitter = prototype->clone(); //need to be clone before calling function
splitter->split();
原型模式的实质就是通过现有的对象,再复制一个新的对象出来。和诸多对象创建设计模式一样,它也绕开了直接使用 new 创建对象的方法,同时通过原型模式创建出来的对象,接口也是统一的(稳定的)。来看一看原型模式的定义:

使用原型实例指定创建对象的种类,然后通过拷贝这些原型来创建新的对象。
——《设计模式》GoF


<html>

<img src=“/_media/programming/cpp/boolan_cpp/prototpye.svg” width=“800”/>

</html>

总结:

  • Prototype 模式同样也是对类对象的使用者具体类(易变)之间的耦合关系进行隔离;这些具体的易变类也都需要一致(稳定)的接口。
  • Prototype 模式使用原型克隆的方法来创建易变类的实例。这样的做法使得我们可以非常灵活的、动态的创建一系列拥有稳定接口的新对象。而创建的工作也非常简单:只需要注册一个新的原型对象,然后在任何需要使用的地方 Clone 即可。
  • Prototype 模式中的 Clone 方法可以利用某些框架中的序列化来实现深拷贝。

Builder

在软件的构建过程中,我们有时候会创建这样的对象:对象包含着各种部分;每个部分都经常变化,这些部分的组成结构却十分稳定。那么有没有一种封装机制可以将组成结构部分(稳定部分)隔离出来,从而保证该结构不受部分的变化而改变呢?

这样相似的情况我们在 Template Method 模式中见过。在创建对象模式中,我们有一种单独的设计模式 Builder 构建器来解决这个问题。

来看一个具体的例子。我们现在有一个游戏场景需要建一所房子,房子的类型可能不同,但建房的流程是一定的:比如打地基,墙壁,窗户,屋顶等等,这些是无论建造哪种房子都一定要做的是事情。不过每一个过程的都可能有变化:墙壁可能材质不同,窗户可能样式不同等等。因此,按照这样的思维,我们首先可以设计出一个初始化函数(稳定)用于主要的流程,然后再在这个函数中调用其不同版本的步骤函数(变化):

class House {
public:
    /* House Builder */
     void Init() {
         this->BuildPart1();
         this->BuildPart2();
         this->BuildPart3();
         .....
     }
     virtual ~House(){}
protected: 
    House* pHouse;
    virtual void BuildPart1() = 0;
    virtual void BuildPart2() = 0;
    virtual void BuildPart3() = 0;
    virtual void BuildPart4() = 0;
    virtual void BuildPart5() = 0;
};
注:此处的 Init() 在 C++ 中不能使用构造函数代替。因为构造函数需要在编译期间就知道具体实现,但这里调用的方法都是虚函数,需要在运行时指定。在其他语言中(Java, C#)则可以。

当有了这个基类以后,我们就可以直接在子类里重写这些步骤函数了。比如我们要建一个石头房子:
class StoneHouse: public House{
    virtual void BuildPart1(){ //pHouse->Part1 = ...; }
    virtual void BuildPart2(){ .... }
    virtual void BuildPart3(){ .... }
    virtual void BuildPart4(){ .... }
    virtual void BuildPart5(){ .... } 
};
到这里实际上我们需要的功能基本都实现了。子类中只需重写属于部分的内容就可以;而结构部分(建造房子的基本顺序)是稳定的,不会被改变的。

不过上述的代码还存在一些优化空间。有时候我们的对象除了基础结构的内容,还会有其他的杂七杂八的功能。如果混淆在一起,会使对象的构建过程变得更加复杂。为此,我们需要将上述的功能分离。这样做将使对象更为轻便,程序结构更加清晰。上述例子中,我们就可以把 House 类中具有构建功能一部分单独分离出来:
class House { .... };
/* Construction Part */
class HouseBuilder {
public:
     void Init() {
         this->BuildPart1();
         this->BuildPart2();
         this->BuildPart3();
         .....
     }
     // function that fetch the result 
     House* GetResult(){
        return pHouse;
    }
    virtual ~HouseBuilder(){}
protected:
    House* pHouse;
    virtual void BuildPart1()=0;
    virtual void BuildPart2()=0;
    virtual void BuildPart3()=0;
    virtual void BuildPart4()=0;
    virtual void BuildPart5()=0;
};
其实到现在程序已经很完善了。当然,我们可以进一步的做拆分,将整个初始化的过程再拆出去,通过一个 HouseBuilder 的指针来执行结构中子部分的调用:
class HouseDirector{
public:
    HouseBuilder* pHouseBuilder;
    HouseDirector(HouseBuilder* pHouseBuilder){
        this->pHouseBuilder=pHouseBuilder;
    }
    House* Construct(){
        pHouseBuilder->BuildPart1();
        pHouseBuilder->BuildPart2();
        pHouseBuilder->BuildPart3();
        pHouseBuilder->BuildPart4();
        pHouseBuilder->BuildPart5();
        return pHouseBuilder->GetResult();
    }
};
到此,我们已经完成了一个成熟的 Builder 版本。之所以做这些额外的步骤,是因为需要将对象的结构和表现做分离,使得同样的构建过程创建不同的表示。

来看一下 Builder 的定义:

讲一个复杂对象的构建部分与其表示部分相分离,使得同样的构建过程(稳定)可以创建不同的表示(变化)。
—— 《设计模式》GoF


<html>

<img src=“/_media/programming/cpp/boolan_cpp/builder.svg” width=“700”/>

</html>

总结:

  • Builder 模式主要用于对象的构建需要按照一定的步骤来实现的状况,而这一定的步骤是一个稳定的结构算法,步骤的内容则会经常变化。
  • Builder 模式需要关注变化点的位置。一般来说,哪里有变化,我们就把哪里封装起来。对于部分的频繁变化正是 Builder 模式需要处理的问题,但也带来一个副作用:Builder 模式强调结构算法的稳定,因此它难以应对结构算法本身的变动。
  • Builder 模式中,对于不同语言,其构造器调用虚函数的方式可能有区别。

设计模式:接口隔离

我们在写一些功能比较多的程序时,经常会无意识的模糊不同模块之间的边界。有时候,我们会直接在一个层中调用用另一个层的方法。我们知道,直接调用带来的依赖就是紧耦合;随着软件的复杂,这样的行为增多以后,我们会发现这种直接依赖彼此接口的行为会大量增加对象之间的耦合度。在这样的情况下,如果我们需要修改被依赖的层级中的内容,就需要将依赖该内容的大量内容重新修改。因此,我们有必要使用一个间接层来组织和管理这些复杂的信息。接口隔离设计模式的思想正是应此而生的。

接口隔离的核心思想是间接Indirection)。间接这个思想其实来自于人类社会的传统经验,它体现了对稳定和变化的剥离。而在软件工程中,间接更是无处不在:因为需要处理人与硬件的关系,于是有了软件;因为要处理人与软件之间的关系,于是有了操作系统……而我们发现,间接的实现部分,总是将当前具体关系划分为稳定和变化两个部分,而间接自身,则是沟通这两个部分的桥梁。在软件设计中,接口隔离设计模式顺承了、并在内容上更加强调了这个思想。

典型的接口隔离设计模式主要有以下四种:

  • Façade
  • Proxy
  • Adapter
  • Mediator

Façade

Façade 门面模式,与其说是一种模式,更不如说是一种设计的方法。它强调实现一个稳定的接口层,并依赖该接口层实现对应用端的稳定性。

一个典型的例子就是数据访问系统。通常我们访问内部数据有好多种方法,而数据的类型又有好多种。如果我们按照直接使用接口的方法来进行访问,那么会产生较高耦合度的依赖:

<html>

<img src=“/_media/programming/cpp/boolan_cpp/facade_1.svg” width=“500”/>

</html>

一旦内部某个数据或者方法对象接口发生变更,那么所有依赖其实现的外部对象均需要重新更新。这样的关系显然对于客户端来说是及其不友好的。而 Façade 模式则提供了一个应用层,并保证这个应用层具有稳定的接口。该接口对外稳定;因此无论内部的方法或结构如何变化,只要对外遵从应用层的接口,那么客户端的设计就不用根据内部的变动而做出相应的变动:

<html>

<img src=“/_media/programming/cpp/boolan_cpp/facade_2.svg” width=“800”/>

</html>

通过 Façade 模式,我们实现了内部和外部的隔离。通过接口层的稳定,我们提高了外部对内部系统的复用度。Façade 模式应该更被作为一种设计的原则和思想的表达应用到软件工程中去,是一个应该在架构层面上注重的要点。

来看一看 Façade 模式的具体定义:

Façade 模式为子系统中的一组接口提供一个一致的(稳定的)界面。它定义了一个高层接口,这个接口使得这一系列子系统更加容易使用(复用)。
—— 设计模式 GoF


<html>

<img src=“/_media/programming/cpp/boolan_cpp/facade.svg” width=“400”/>

</html>

总结:

  • 从客户程序的角度来看, Façade 模式简化了整个组件系统的接口。对于内部和外部客户程序来说, Façade 模式实现了一种解耦的效果——即内部的子系统的任何变化不会影响到 Façade 接口的变化,从而使客户端程序获得一个稳定的依赖。
  • 相比从单个类的层次,Façade 设计模式更注重从架构的层次去看待整个系统;更多的时候, Façade 模式代表的是一种架构设计模式。
  • Façade 设计模式并非是一个像容器一样的概念,因此我们并不能随意的放进任意对象。 Façade 模式中,其内部区域(子系统)应该具有相互耦合关系比较大的特性,而不是一个简单的功能集合。

Proxy

在软件的开发过程中,基于某种原因,直接访问有些对象会给使用者或者系统带来很多麻烦(比如创建对象的开销、相关的安全机制、进程之外的访问等等)。这样的情况下,如果我们希望按照原有的方式(透明操作)去访问这个对象,但又希望回避这些“麻烦”,我们就倾向于建立一个 Proxy 来作为原有对象和访问者的中间层。通过这个中间层,我们可以对相关的额外内容进行处理(比如添加一些安全性的验证等等),从而实现了在不失去对对象的透明操作的情况下,同时管理和控制这些对象特有的复杂性。

Proxy 设计模式的原理很简单,但对于 Proxy 对象本身的构建需要针对原始对象所需要的特性来实现。因此,对于不同的原始对象,Proxy 的具体实现会有很大的不同:比如因为安全原因需要屏蔽客户端直接访问真实对象, 或者在远程调用中需要使用代理类处理远程方法调用的技术细节,也可能为了提升系统性能,对真实对象进行封装,从而达到延迟加载的目的等等。但不管如何变化,Proxy 设计模式始终应该具有如下四个部分:

  • Subject Interface:定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法;
  • Real Subject:真正实现业务逻辑的类;
  • Proxy :用来代理和封装真实主题;
  • Client:客户端(Main),使用代理类和主题接口完成一些工作。

具体体现到代码中可以表示为:

class ISubject{
public:
    virtual void process();
};
/* Proxy Design */
class SubjectProxy: public ISubject{
public:
    virtual void process(){
        //an Indirection way to call RealSubject
        //....
    }
};
class ClientApp{  
    ISubject* subject; 
public: 
    ClientApp() { subject=new SubjectProxy(); }
    void DoTask(){
        //...
        subject->process();
        //....
    }
};
可以注意到的是,代理类与真正的类使用了相同的接口。这样的设计也可以从现实社会的角度来看待:真正的当事人由于某些问题,需要授权代理人去代办一些事宜。因此在处理的结果上,代理人与当事人具有同样的效果。

来看一下 Proxy 模式的具体定义:

为其他对象提供一种代理以控制(隔离,使用接口)对这个对象的访问。
——《设计模式》GoF


<html>

<img src=“/_media/programming/cpp/boolan_cpp/proxy.svg” width=“600”/>

</html>

总结:

  • “增加一层间接层” 是软件系统中对很多复杂问题的一种常见解决方法。在面向对象系统中,直接使用某些对象会带来很多问题,作为间接层的 Proxy 对象便是解决这一种问题的常用手段。
  • 具体 Proxy 设计模式的实现方法和力度根据需求不同相差很大。有些可能对单个对象做非常细的力度控制(比如 Copy-on-write);有些可能对组件模块提供抽象代理层,在架构的层次上对对象做 Proxy
  • Proxy 并不一定要求保持接口完整的一致性,只要能够实现间接控制,有时候损失一些透明性是可以接受的。

Adapter

Adapter 这个概念与生活联系非常紧密。它作为一种旧类型到新类型的转换方式,在生活中随处可见。比如我们在使用显示器的时候,面对显示器和显卡不同的接口,会用到转接口;又譬如我们在使用笔记本的时候,需要将 220V 的标准电压转化为笔记本适用的电压。总的来说,Adapter 作为一种中间的存在,解决了接口不一致的交流问题。

而在软件的设计的过程中,我们很可能会遇到接口不一致的情况:某一些现存的对象具有一种接口,而新开发的环境中又有新的接口标准。那么我们如何将这些现存的对象放置到新的环境中使用呢?

Adapter 设计模式可以通过将旧的类的接口转化为新的接口,从而解决这样的问题。

来看一看 Adapter 具体是如何实现的吧:

首先,Adapter 通过继承新接口类的方式,获取了新接口的标准:

//target Interface (new Interface)
class ITarget{
public:
    virtual void process()=0;
};
//Old Interface
class IAdaptee{
public:
    virtual void foo(int data)=0;
    virtual int bar()=0;
};
//class with old interface
class OldClass: public IAdaptee{
    //....
};
class Adapter: public ITarget{ //继承
    ....
};
而接下来,Adapter 又通过组合旧接口类来得到现存对象中的功能:
class Adapter: public ITarget{
protected:
    IAdaptee* pAdaptee;//Combination 
public:
    Adapter(IAdaptee* pAdaptee){ this->pAdaptee = pAdaptee; }
    virtual void process(){
        int data=pAdaptee->bar();
        pAdaptee->foo(data); 
    }
};
上面的 Adapter 类通过组合得到了旧接口类型的入口指针,然后通过构造函数初始化使得 Adapter 具有了旧接口类的功能,最后通过多态的方式调用了旧接口类下的具体类的实现方法。

因为 Adapter 的转换作用,在新环境下使用旧接口下的具体对象也变得非常简单:
//a class under old interface
IAdaptee* pAdaptee = new OldClass();
//use Adapter to fetch the obejct, then adapt it and deliver it with new interface
ITarget* pTarget = new Adapter(pAdaptee);
pTarget->process();
当然,在实际的开发工作中,Adapter 的实现可能远比我们描述的要复杂:它可能会有很多接口,而且也可能必须要基于一定的前提(比如两个接口必须存在可以转化的可能)。

以上的这种 Adapter 的类型,被称为对象适配器。除此之外,还有一种 Adapter 被称为类适配器。这种 Adapter 通过多继承来实现适配:即通过保护继承来得到现有对象的实现,同时通过公有继承得到新的接口规范。但因为其通过继承获得了旧的具体类的内容,因此在灵活性上远不如类适配器

同时需要指出的是,以上的 Adapter 实现只是其中的一种。我们熟知的 STL 中的容器 StackQueue 也是适配器的一个典型例子,但这两种适配器并没有通过继承来得到新的标准接口,而是内部通过 Deque 类型创建了一个对象,然后直接拿来使用。换句话说,这种适配器不但具有适配的功能,本身也是一种具体的实现。就此看来,Adapter 是一种非常灵活的设计模式;我们使用的时候也应该将实现放到具体的情况中去讨论。

最后来看看 Adapter 设计模式的定义:

将一个类的接口转换为客户希望的另一个接口。Adapter 模式使得原本由于接口不兼容而不能一起工作的类可以一起工作。
——《设计模式》GoF


<html>

<img src=“/_media/programming/cpp/boolan_cpp/adapter.svg” width=“600”/>

</html>

总结:

  • Adapter 模式主要应用于“希望复用一些现存的类,但是接口由于复用环境要求不一致的情况”,在遗留代码复用和类库迁移等方面非常有用。
  • GoF 23 中定义了两种 Adapter 模式的实现结构:对象适配器和类适配器。但类适配器采用了多继承的实现方式,一般不推荐使用。对象适配器采用了对象组合的方式,更符合耦合精神。
  • Adapter 模式可以以非常灵活的方式实现,并不用拘泥于 GoF 23 中定义的两种结构。比如,我们完全可以将 Adapter 模式中的现存对象,作为具有新接口方法的参数,来达到适配的目的。

Mediator

在软件设计中,我们有时候会遇到大量对象相互关联依赖的情况。该情况导致对象之间往往维持一种非常复杂的引用关系,如果遇到一些需求的变更,那么这一系列的引用关系将会面临不断的变化。

一个典型的例子就是 GUI 与内部数据的交互。从功能上来说,我们需要 GUI 和内部数据实现同步,即通过 GUI 可以操作内部数据,而内部数据的变化也会直接在 GUI 上反馈出来。从实现上来说,实现上述功能会要求 GUI 和内部数据在关系上相互依赖。然而这样的关系会造成对变化对象的依赖,这是违反了依赖倒置原则的。因此,我们有必要为这种关系提供一个中介层,将 GUI 和 内部数据中有关联的对象通过指定的规范链接起来。相比起直接相互依赖,这种链接属于间接链接;通过稳定该中间层,实现了关联对象的解耦合。我们把这样的设计方法称为 Mediator 中介者模式。

中介者模式通过中介对象来管理之前拥有依赖的对象之间的交互,并依赖这样的设计消除对象之间的紧耦合。该方法主要依靠两个步骤来实现:

  1. 封装变化。
  2. 去除对象之间的显式引用(即编译时依赖)。

通过以上的更改,中介者模式可以将原有的网状类关系转化为一种星型的类关系,由中介者来承担对象之间的交流业务:

<html>

<img src=“/_media/programming/cpp/boolan_cpp/mediator_1.svg” width=“800”/>

</html>

需要注意的是,我们在进行上述处理时通过了中介者对对象关系进行了解耦合;但同时带来的后果就是将对象交互的负担全交给了中介者。为此,中介者的需要具有相应的控制逻辑来控制对象之间的交流。举个例子,如果我们需要做对象之间的数据交流,我们需要在中介者内部实现对象绑定的协议:比如传递对象的属性,名称,或者制定其他相关的交互协议等等。这一系列的控制逻辑往往使中介者的设计显得更为复杂。

从具体的实现上来说,中介者模式一共有四种角色:

  • Mediator:定义了 Colleague 对象直接交互的接口。
  • ConcreteMediatorMediator 的子类,维持了多个同事类的引用,并协调各同事类的协作行为。
  • Colleague:维持了一个抽象中介者的引用,使得各子类可以和中介者通信。
  • ConcreteColleague:是 Colleague 的子类,只需要实现自己的行为,并与中介者交流即可。


从上述的信息可以看出来,中介者模式通过间接的方法,使类关系从网状关系转变到了星状关系,简化了类关系。这样的做法使得类对象之间可以独立变动,不再受其他对象的影响。从这一点上看,中介者模式与门面模式非常相似;唯一的区别只是在于门面模式是处理系统之间的关系,而中介者模式是处理系统内部对象的关系。

来看一看中介者模式的定义吧:

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


<html>

<img src=“/_media/programming/cpp/boolan_cpp/mediator.svg” width=“600”/>

</html>

总结:

  • Mediator 模式通过将多个对象间的控制逻辑进行集中式管理,达到了将多个对象间复杂的管理关系解耦的目的;同时也将多个对象相互关联的类关系(网状关系)转换成了多个对象与一个中介者相关联的关系(星状关系),从而简化了系统的维护,抵御了可能的变化。
  • 因为其实现机制,Mediator 具体对象的实现可能会非常复杂。这时候我们可以对 Mediator 对象进行相应的分解处理。
  • Facade 模式是用于系统之间(单向)的对象的关联关系解耦;而 Mediator 模式是用于系统内各个对象(双向)之间的关联关系解耦。