======C++设计模式 第二周======
本页内容是 //Boolan// C++ 开发工程师培训系列的笔记。\\
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//
\\
//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 { .... };
\\
看到这里,你也应该明白这实际上就是一个特化的,用于处理多个相互依赖对象的工厂模式了。这就是 //Abstract Factory// 的核心概念:将**一系列互相依赖的对象**整合到一起使用工厂处理。它在本质上也是属于工厂模式的一种,但因为牵涉到多个相互依赖对象,实际上的处理又显得稍有不同。
\\
\\
来看看 //Abstract Factory// 的具体定义吧:
>提供一个接口,让该接口负责创建**一系列**的“**相关或者相互依赖**的对象”,无需指定他们具体的类。
>——《设计模式》//GoF//
\\
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//
\\
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//
\\
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//
\\
//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// 中的容器 //Stack// 和 //Queue// 也是适配器的一个典型例子,但这两种适配器并没有通过继承来得到新的标准接口,而是内部通过 //Deque// 类型创建了一个对象,然后直接拿来使用。换句话说,这种适配器不但具有适配的功能,本身也是一种具体的实现。就此看来,//Adapter// 是一种非常灵活的设计模式;我们使用的时候也应该将实现放到具体的情况中去讨论。
\\
\\
最后来看看 //Adapter// 设计模式的定义:
>将一个类的接口转换为客户希望的另一个接口。//Adapter// 模式使得原本由于接口不兼容而不能一起工作的类可以一起工作。
> ——《设计模式》//GoF//
\\