本页内容是 Boolan C++ 开发工程师培训系列的笔记。
因个人水平有限,我撰写的笔记不免出现纰漏。如果您发现错误,请留言提出,谢谢!
传统的对象创建一般是通过栈上创建或者是 new
在堆上创建。但不管哪种方式,对象创建的时候都必须要有一个具体的类;这样的对象创建过程是紧耦合的。我们可以通过对象创建类型的设计模式来避免这样的紧耦合,从而支持对象创建的稳定。
对象创建类型设计模式的典型例子有:
在软件设计中,经常要面临创建对象的工作。由于需求的变化,需要创建的对象往往也是需要变化的。我们知道,传统常规意义上的对象创建方法会导致紧耦合,那么有没有一种方法来避免这样的紧耦合呢?
来看一个具体的例子。我们在前面分析过一个文件分割的例子;现在我们假设这个文件分割的算法有好多种。那么如果从变化的场景来考虑的话,这个问题应该怎么解决?
从面向对象设计的角度来考虑,我们应该设计一个抽象基类,在子类创建对象的过程中使用基类对象来创建,这样可以保证创建对象时声明的类的类型和对象都是抽象的。这样的声明可以让我们将对象的类型判断放置到运行时去决定。当然,针对这样的写法,我们需要通过子类继承基类,然后在子类中重写对象创建的定义来实现,比如如下代码结构:
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) ;
这样的写法必然要求编译时的细节依赖,显然是违反依赖倒置原则的。那我们应该怎么做呢?
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
类摆脱对具体类型的依赖。这又要怎么解决呢?
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 使得一个类的实例化延迟(目的:解耦,手段:虚函数)到子类。
——《设计模式》GoF
<html>
<img src=“/_media/programming/cpp/boolan_cpp/factorymethod2.svg” width=“900”/>
</html>
总结:
new
)会导致软件的脆弱。
Abstract Factory 抽象工厂模式是从 Factory Method 工厂模式中衍生出来的;它和 Factory Method 唯一不同的区别在于, Factory Method 是对单一对象进行逐个创建处理,而 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
对象去操作它。这也就是我们前面提到的一系列互相依赖的对象的一个典型例子。
IDBConnectionFactory
、IDBCommandFactory
、IDataReaderFactory
这三个具体实现,我可以写出任意的具体实现组合,比如 Sql 的 connection
加上 Oracle 的 command
加上 Mysql 的 reader
。。 这样简直就是乱套了嘛。
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 { .... };
提供一个接口,让该接口负责创建一系列的“相关或者相互依赖的对象”,无需指定他们具体的类。
——《设计模式》GoF
<html>
<img src=“/_media/programming/cpp/boolan_cpp/abstractfactory.svg” width=“1000”/>
</html>
总结:
有些时候我们需要创建一些很复杂的对象:比如构造函数的参数一大堆,而我们又看不懂参数到底在说啥(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>
总结:
在软件的构建过程中,我们有时候会创建这样的对象:对象包含着各种部分;每个部分都经常变化,这些部分的组成结构却十分稳定。那么有没有一种封装机制可以将组成结构部分(稳定部分)隔离出来,从而保证该结构不受部分的变化而改变呢?
这样相似的情况我们在 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 版本。之所以做这些额外的步骤,是因为需要将对象的结构和表现做分离,使得同样的构建过程创建不同的表示。
讲一个复杂对象的构建部分与其表示部分相分离,使得同样的构建过程(稳定)可以创建不同的表示(变化)。
—— 《设计模式》GoF
<html>
<img src=“/_media/programming/cpp/boolan_cpp/builder.svg” width=“700”/>
</html>
总结:
我们在写一些功能比较多的程序时,经常会无意识的模糊不同模块之间的边界。有时候,我们会直接在一个层中调用用另一个层的方法。我们知道,直接调用带来的依赖就是紧耦合;随着软件的复杂,这样的行为增多以后,我们会发现这种直接依赖彼此接口的行为会大量增加对象之间的耦合度。在这样的情况下,如果我们需要修改被依赖的层级中的内容,就需要将依赖该内容的大量内容重新修改。因此,我们有必要使用一个间接层来组织和管理这些复杂的信息。接口隔离设计模式的思想正是应此而生的。
接口隔离的核心思想是间接(Indirection)。间接这个思想其实来自于人类社会的传统经验,它体现了对稳定和变化的剥离。而在软件工程中,间接更是无处不在:因为需要处理人与硬件的关系,于是有了软件;因为要处理人与软件之间的关系,于是有了操作系统……而我们发现,间接的实现部分,总是将当前具体关系划分为稳定和变化两个部分,而间接自身,则是沟通这两个部分的桥梁。在软件设计中,接口隔离设计模式顺承了、并在内容上更加强调了这个思想。
典型的接口隔离设计模式主要有以下四种:
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>
总结:
在软件的开发过程中,基于某种原因,直接访问有些对象会给使用者或者系统带来很多麻烦(比如创建对象的开销、相关的安全机制、进程之外的访问等等)。这样的情况下,如果我们希望按照原有的方式(透明操作)去访问这个对象,但又希望回避这些“麻烦”,我们就倾向于建立一个 Proxy 来作为原有对象和访问者的中间层。通过这个中间层,我们可以对相关的额外内容进行处理(比如添加一些安全性的验证等等),从而实现了在不失去对对象的透明操作的情况下,同时管理和控制这些对象特有的复杂性。
Proxy 设计模式的原理很简单,但对于 Proxy 对象本身的构建需要针对原始对象所需要的特性来实现。因此,对于不同的原始对象,Proxy 的具体实现会有很大的不同:比如因为安全原因需要屏蔽客户端直接访问真实对象, 或者在远程调用中需要使用代理类处理远程方法调用的技术细节,也可能为了提升系统性能,对真实对象进行封装,从而达到延迟加载的目的等等。但不管如何变化,Proxy 设计模式始终应该具有如下四个部分:
具体体现到代码中可以表示为:
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();
//....
}
};
可以注意到的是,代理类与真正的类使用了相同的接口。这样的设计也可以从现实社会的角度来看待:真正的当事人由于某些问题,需要授权代理人去代办一些事宜。因此在处理的结果上,代理人与当事人具有同样的效果。
为其他对象提供一种代理以控制(隔离,使用接口)对这个对象的访问。
——《设计模式》GoF
<html>
<img src=“/_media/programming/cpp/boolan_cpp/proxy.svg” width=“600”/>
</html>
总结:
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 具有了旧接口类的功能,最后通过多态的方式调用了旧接口类下的具体类的实现方法。
//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 模式使得原本由于接口不兼容而不能一起工作的类可以一起工作。
——《设计模式》GoF
<html>
<img src=“/_media/programming/cpp/boolan_cpp/adapter.svg” width=“600”/>
</html>
总结:
在软件设计中,我们有时候会遇到大量对象相互关联依赖的情况。该情况导致对象之间往往维持一种非常复杂的引用关系,如果遇到一些需求的变更,那么这一系列的引用关系将会面临不断的变化。
一个典型的例子就是 GUI 与内部数据的交互。从功能上来说,我们需要 GUI 和内部数据实现同步,即通过 GUI 可以操作内部数据,而内部数据的变化也会直接在 GUI 上反馈出来。从实现上来说,实现上述功能会要求 GUI 和内部数据在关系上相互依赖。然而这样的关系会造成对变化对象的依赖,这是违反了依赖倒置原则的。因此,我们有必要为这种关系提供一个中介层,将 GUI 和 内部数据中有关联的对象通过指定的规范链接起来。相比起直接相互依赖,这种链接属于间接链接;通过稳定该中间层,实现了关联对象的解耦合。我们把这样的设计方法称为 Mediator 中介者模式。
中介者模式通过中介对象来管理之前拥有依赖的对象之间的交互,并依赖这样的设计消除对象之间的紧耦合。该方法主要依靠两个步骤来实现:
通过以上的更改,中介者模式可以将原有的网状类关系转化为一种星型的类关系,由中介者来承担对象之间的交流业务:
<html>
<img src=“/_media/programming/cpp/boolan_cpp/mediator_1.svg” width=“800”/>
</html>
需要注意的是,我们在进行上述处理时通过了中介者对对象关系进行了解耦合;但同时带来的后果就是将对象交互的负担全交给了中介者。为此,中介者的需要具有相应的控制逻辑来控制对象之间的交流。举个例子,如果我们需要做对象之间的数据交流,我们需要在中介者内部实现对象绑定的协议:比如传递对象的属性,名称,或者制定其他相关的交互协议等等。这一系列的控制逻辑往往使中介者的设计显得更为复杂。
从具体的实现上来说,中介者模式一共有四种角色:
从上述的信息可以看出来,中介者模式通过间接的方法,使类关系从网状关系转变到了星状关系,简化了类关系。这样的做法使得类对象之间可以独立变动,不再受其他对象的影响。从这一点上看,中介者模式与门面模式非常相似;唯一的区别只是在于门面模式是处理系统之间的关系,而中介者模式是处理系统内部对象的关系。
来看一看中介者模式的定义吧:
用一个中介对象来封装(封装变化)一系列的对象交互。中介者使得各个对象不需要显示的相互引用(编译时依赖→运行时依赖),从而使其耦合松散(管理变化),而且可以独立的改变他们之间的交互。
——《设计模式》GoF
<html>
<img src=“/_media/programming/cpp/boolan_cpp/mediator.svg” width=“600”/>
</html>
总结: