设计模式4:“对象创建”模式——Factory Method, Abstract Factory, Prototype, Builder
“对象创建”模式
通过“对象创建”模式绕开new,来避免对象创建(new)过程中所导致的紧耦合(依赖具体类),从而支持对象创建的稳定。它是接口抽象之后的第一步工作。
典型模式:
- Factory Method
- Abstract Factory
- Prototype
- Builder
Factory Method工厂方法
动机(Motivation)
- 在软件系统中,经常面临这创建对象的工作;由于需求的变化,需要创建的对象的具体类型经常变化。
- 如何应对这种变化?如何绕过常规的对象创建方法(new),提供一种“封装机制”来避免客户程序和这种“具体对象创建工作”的紧耦合。
在先前的课程中,我们分析过一个文件分割的例子,如果文件分割的算法有很多种的话,如何实现灵活的切换呢?
先看一下代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class ISplitter{ 2 public: 3 virtual void split()=0; 4 virtual ~ISplitter(){} 5 }; 6 7 class BinarySplitter : public ISplitter{ 8 9 }; 10 11 class TxtSplitter: public ISplitter{ 12 13 }; 14 15 class PictureSplitter: public ISplitter{ 16 17 }; 18 19 class VideoSplitter: public ISplitter{ 20 21 }; 22 23 24 class MainForm : public Form 25 { 26 TextBox* txtFilePath; 27 TextBox* txtFileNumber; 28 ProgressBar* progressBar; 29 30 public: 31 void Button1_Click(){ 32 33 ISplitter * splitter= 34 new BinarySplitter();//依赖具体类 35 36 splitter->split(); 37 38 } 39 };
从面向对象设计的思想出发,我们设计一个抽象基类(稳定),子类(变化)在创建对象过程中使用基类对象来创建,这样可以让我们将对象的类型判断放到运行时去决定。
ISplitter * splitter=new BinarySplitter();
这样的创建方法显然违背了之前提过的依赖倒置原则,那是否存在一种方法来绕开这种细节依赖呢?我们是否可以参考上面的分割算法实现方法,通过一个抽象基类来实现对象创建?
- 直接创建:BinarySplitter bs();
- 直接调用new函数;
- 用“绕”的方法,让函数返回一个对象;
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 //抽象类 2 class ISplitter{ 3 public: 4 virtual void split()=0; 5 virtual ~ISplitter(){} 6 }; 7 8 //工厂基类 9 class SplitterFactory{ 10 public: 11 virtual ISplitter* CreateSplitter()=0; 12 virtual ~SplitterFactory(){} 13 }; 14 15 //具体类 16 class BinarySplitter : public ISplitter{ 17 18 }; 19 20 class TxtSplitter: public ISplitter{ 21 22 }; 23 24 class PictureSplitter: public ISplitter{ 25 26 }; 27 28 class VideoSplitter: public ISplitter{ 29 30 }; 31 32 //具体工厂 33 class BinarySplitterFactory: public SplitterFactory{ 34 public: 35 virtual ISplitter* CreateSplitter(){ 36 return new BinarySplitter(); 37 } 38 }; 39 40 class TxtSplitterFactory: public SplitterFactory{ 41 public: 42 virtual ISplitter* CreateSplitter(){ 43 return new TxtSplitter(); 44 } 45 }; 46 47 class PictureSplitterFactory: public SplitterFactory{ 48 public: 49 virtual ISplitter* CreateSplitter(){ 50 return new PictureSplitter(); 51 } 52 }; 53 54 class VideoSplitterFactory: public SplitterFactory{ 55 public: 56 virtual ISplitter* CreateSplitter(){ 57 return new VideoSplitter(); 58 } 59 }; 60 61 class MainForm : public Form 62 { 63 SplitterFactory* factory;//工厂 64 65 public: 66 67 MainForm(SplitterFactory* factory){ 68 this->factory=factory; 69 } 70 71 void Button1_Click(){ 72 73 ISplitter * splitter= 74 factory->CreateSplitter(); //多态new 75 76 splitter->split(); 77 78 } 79 };
1 SplitterFactory* factory;//工厂 2 ISplitter * splitter=factory->CreateSplitter(); //多态new
我们将创建对象这种方法直接抽象到了一个抽象基类(工厂基类)中,同时子类通过各自的具体方法(细节依赖)来实现对象创建。通过这种方法,我们在创建对象的时候,不再依赖具体的类,只有在运行时才进行具体类型的判断,从而实现依赖倒置原则。
模式定义定义一个用于创建对象的接口,让子类决定实例化哪一个类。
Factory Method使得一个类的实例化延迟(目的:解耦,手段:虚函数)到子类。
--《设计模式》Gof
要点总结:
- Factory Method 模式用于隔离类对象的使用者和具体类型之间的耦合关系。面对一个经常变化的具体类型,紧耦合关系(new)会导致软件的脆弱。
- Factory Method模式通过面对对象的手法,将所要创建的对象工作延迟到子类,从而实现一种扩展(而非更改)的策略,较好地解决了这种紧耦合关系。
- Factory Method模式解决“单个对象”的需求变化。缺点在于要求创建方法/参数相同。
在软件中,可以通过将new封装到工厂类的create函数中,并且将工厂类抽象出一个基类,实现new的多态性。这样可以保证对象构建函数的复用,实现将变化集中的目的。
抽象工厂 (Abstract Factory)
动机(Motivation)
- 在软件系统中,经常面临着“一系列相互依赖的对象”的创建工作;同时,由于需求的变化,往往存在更多系列对象的创建工作。
- 如何应对这种变化?如何绕过常规的对象创建方法(new),提供一种“封装机制”来避免客户程序和这种“具体对象创建工作”的紧耦合。
抽象工厂(Abstract Factory)是从工厂模式中衍生出来的,工厂模式是对单一对象的创建进行处理,实现依赖倒置,而抽象工厂模式需要处理“一系列相互依赖的对象”的创建工作。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class EmployeeDAO{ 2 3 public: 4 vector<EmployeeDO> GetEmployees(){ 5 SqlConnection* connection = 6 new SqlConnection(); 7 connection->ConnectionString = "..."; 8 9 SqlCommand* command = 10 new SqlCommand(); 11 command->CommandText="..."; 12 command->SetConnection(connection); 13 14 SqlDataReader* reader = command->ExecuteReader(); 15 while (reader->Read()){ 16 17 } 18 } 19 };
很明显上面的代码不满足依赖倒置原则,难以满足变化的要求。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 //数据库访问有关的基类 2 class IDBConnection{ 3 4 }; 5 class IDBConnectionFactory{ 6 public: 7 virtual IDBConnection* CreateDBConnection()=0; 8 }; 9 10 11 class IDBCommand{ 12 13 }; 14 class IDBCommandFactory{ 15 public: 16 virtual IDBCommand* CreateDBCommand()=0; 17 }; 18 19 20 class IDataReader{ 21 22 }; 23 class IDataReaderFactory{ 24 public: 25 virtual IDataReader* CreateDataReader()=0; 26 }; 27 28 29 //支持SQL Server 30 class SqlConnection: public IDBConnection{ 31 32 }; 33 class SqlConnectionFactory:public IDBConnectionFactory{ 34 35 }; 36 37 38 class SqlCommand: public IDBCommand{ 39 40 }; 41 class SqlCommandFactory:public IDBCommandFactory{ 42 43 }; 44 45 46 class SqlDataReader: public IDataReader{ 47 48 }; 49 class SqlDataReaderFactory:public IDataReaderFactory{ 50 51 }; 52 53 //支持Oracle 54 class OracleConnection: public IDBConnection{ 55 56 }; 57 58 class OracleCommand: public IDBCommand{ 59 60 }; 61 62 class OracleDataReader: public IDataReader{ 63 64 }; 65 66 class EmployeeDAO{ 67 IDBConnectionFactory* dbConnectionFactory; 68 IDBCommandFactory* dbCommandFactory; 69 IDataReaderFactory* dataReaderFactory; 70 71 72 public: 73 vector<EmployeeDO> GetEmployees(){ 74 IDBConnection* connection = 75 dbConnectionFactory->CreateDBConnection(); 76 connection->ConnectionString("..."); 77 78 IDBCommand* command = 79 dbCommandFactory->CreateDBCommand(); 80 command->CommandText("..."); 81 command->SetConnection(connection); //关联性 82 83 IDBDataReader* reader = command->ExecuteReader(); //关联性 84 while (reader->Read()){ 85 86 } 87 } 88 };
上面的代码将数据连接系统的具体实现用工厂模式实现,为所有与数据库访问相关的对象创建了抽象基类,从而使得这些对象的创建实现多态性,但是这时候出现了一个新问题:
这些对象需要一起配套使用,也就是说SQL的connection对象必须和SQL的command对象一起使用,不然就会在运行时报错。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 //数据库访问有关的基类 2 class IDBConnection{ 3 4 }; 5 6 class IDBCommand{ 7 8 }; 9 10 class IDataReader{ 11 12 }; 13 14 15 class IDBFactory{ 16 public: 17 virtual IDBConnection* CreateDBConnection()=0; 18 virtual IDBCommand* CreateDBCommand()=0; 19 virtual IDataReader* CreateDataReader()=0; 20 21 }; 22 23 24 //支持SQL Server 25 class SqlConnection: public IDBConnection{ 26 27 }; 28 class SqlCommand: public IDBCommand{ 29 30 }; 31 class SqlDataReader: public IDataReader{ 32 33 }; 34 35 36 class SqlDBFactory:public IDBFactory{ 37 public: 38 virtual IDBConnection* CreateDBConnection()=0; 39 virtual IDBCommand* CreateDBCommand()=0; 40 virtual IDataReader* CreateDataReader()=0; 41 42 }; 43 44 //支持Oracle 45 class OracleConnection: public IDBConnection{ 46 47 }; 48 49 class OracleCommand: public IDBCommand{ 50 51 }; 52 53 class OracleDataReader: public IDataReader{ 54 55 }; 56 57 58 class EmployeeDAO{ 59 IDBFactory* dbFactory; 60 61 public: 62 vector<EmployeeDO> GetEmployees(){ 63 IDBConnection* connection = 64 dbFactory->CreateDBConnection(); 65 connection->ConnectionString("..."); 66 67 IDBCommand* command = 68 dbFactory->CreateDBCommand(); 69 command->CommandText("..."); 70 command->SetConnection(connection); //关联性 71 72 IDBDataReader* reader = command->ExecuteReader(); //关联性 73 while (reader->Read()){ 74 75 } 76 77 } 78 };
从上面的代码可以看出,我们将配套的一系列对象,打包到一个工厂中进行封装,其实就是工厂模式的一个特化,用于处理一系列相互依赖的对象。
模式定义
提供一个接口,让该接口负责创建一系列“相关或相互依赖的对象”,无需指定它们具体的类。
--《设计模式》Gof
结构
要点总结:
- 如果没有应对“多系列对象构建”的需求变化,则没有必要使用 Abstract Factory 模式,这时候使用简单的工厂完全可以。
- “系列对象”指的是在某一特定系列下的对象之间具有相互依赖或作用的关系。不同系列的对象之间不能相互依赖。
- Abstract Factory 模式主要在于应对“新系列”的需求变动,其缺点在于难以应对“新对象”的需求变动。
![](https://images2017.cnblogs.com/blog/1293190/201712/1293190-20171206183647159-1533353933.jpg)
原型模式(Prototype)
- 在软件系统中,经常面临着“某些结构复杂的对象”的创建工作;由于需求的变化,这些对象经常面临着剧烈的变化,但是他们却拥有着比较稳定一致的接口。
- 如何应对这种变化?如何向“客户程序(使用这些对象的程序)”隔离出“这些易变对象”,从而使得“依赖这些易变对象的客户程序”不随着需求改变而改变?
原型模式也是属于对象创建模式,它用来处理某些结构复杂对象的创建工作,这也是与工厂模式最大的区别。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 //抽象类 2 class ISplitter{ 3 public: 4 virtual void split()=0; 5 virtual ISplitter* clone()=0; //通过克隆自己来创建对象 6 7 virtual ~ISplitter(){} 8 9 }; 10 11 //具体类 12 class BinarySplitter : public ISplitter{ 13 public: 14 virtual ISplitter* clone(){ 15 return new BinarySplitter(*this); 16 } 17 }; 18 19 class TxtSplitter: public ISplitter{ 20 public: 21 virtual ISplitter* clone(){ 22 return new TxtSplitter(*this); 23 } 24 }; 25 26 class PictureSplitter: public ISplitter{ 27 public: 28 virtual ISplitter* clone(){ 29 return new PictureSplitter(*this); 30 } 31 }; 32 33 class VideoSplitter: public ISplitter{ 34 public: 35 virtual ISplitter* clone(){ 36 return new VideoSplitter(*this); 37 } 38 }; 39 40 class MainForm : public Form 41 { 42 ISplitter* prototype;//原型对象 43 44 public: 45 46 MainForm(ISplitter* prototype){ 47 this->prototype=prototype; 48 } 49 50 void Button1_Click(){ 51 52 ISplitter * splitter= 53 prototype->clone(); //克隆原型 54 55 splitter->split(); 56 57 } 58 };
由上面的代码可以看出,原型模式主要是通过子类重写拷贝构造函数来实现对象的创建。
模式定义
使用原型实例指定创建对象的种类,然后通过拷贝这些原型来创建新的对象。
--《设计模式》Gof
结构:
要点总结:
- Prototype模式同样用于隔离类对象的使用者和具体类型(易变类)直接的耦合关系,他同样要求这些“易变类”拥有“稳定的接口”。
- Prototype模式对于“如何创建易变类的实体对象”采用“原型克隆”的方法来做,它使得我们可以非常灵活地“动态创建”拥有某些稳定接口的新对象——所需工作仅仅是注册一个新类的对象(即原型),然后在任何需要的地方clone。
- Prototype模式中的Clone方法可以利用某些框架中的序列化来实现深拷贝。
构建器(Builder)
动机(Motivation)
- 在软件系统中,有时候面临着“一个复杂对象”的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将他们组合在一起的算法却相对稳定。
- 如何应对这种变化?如何提供一种“封装机制”来隔离出“复杂对象的各个部分”的变化,从而保持系统中的“稳定构建算法”不随着需求改变而改变。
构建器解决的问题是一个复杂对象的创建工作,其通常由各个部分的子对象用一定算法构成。下面以游戏中房子的构建为例进行说明,这个房子的建造过程是相对固定的,比如大地节,墙,门,窗的构建等等,但是具体的墙,门,窗可能不同。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class House{ 2 //.... 3 4 House(){ // 错误示例,不能这样写 5 6 this->BuildPart1(); // 构造函数中是静态绑定,此时调用纯虚函数会报错,不会去调用子类 7 8 for (int i = 0; i < 4; i++){ 9 this->BuildPart2(); 10 } 11 12 bool flag = pHouseBuilder->BuildPart3(); 13 14 if (flag){ 15 this->BuildPart4(); 16 } 17 18 this->BuildPart5(); 19 } 20 21 void Init(){ // 流程相对固定 22 23 this->BuildPart1(); // 构造第一部分 24 25 for (int i = 0; i < 4; i++){ 26 this->BuildPart2(); 27 } 28 29 bool flag = pHouseBuilder->BuildPart3(); 30 31 if (flag){ 32 this->BuildPart4(); 33 } 34 35 this->BuildPart5(); 36 } 37 virtual ~HouseBuilder(){} 38 protected: 39 virtual void BuildPart1() = 0; 40 virtual void BuildPart2() = 0; 41 virtual void BuildPart3() = 0; 42 virtual void BuildPart4() = 0; 43 virtual void BuildPart5() = 0; 44 }; 45 46 class StoneHouse: public House{ 47 48 protected: 49 50 virtual void BuildPart1(){ 51 //pHouse->Part1 = ...; 52 } 53 virtual void BuildPart2(){ 54 55 } 56 virtual void BuildPart3(){ 57 58 } 59 virtual void BuildPart4(){ 60 61 } 62 virtual void BuildPart5(){ 63 64 } 65 }; 66 67 int main() 68 { 69 House* pHouse = new StoneHouse(); 70 pHouse->Init(); 71 }
因此将稳定的部分用抽象基类来实现,具体的房子即子类只需重写这些虚函数即可。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class House{ // 表示 2 //.... house 与 HouseBuilder 相分离 3 }; 4 5 class HouseBuilder { // 构建 6 public: 7 House* GetResult(){ 8 return pHouse; 9 } 10 virtual ~HouseBuilder(){} 11 protected: 12 13 House* pHouse; 14 virtual void BuildPart1()=0; 15 virtual void BuildPart2()=0; 16 virtual void BuildPart3()=0; 17 virtual void BuildPart4()=0; 18 virtual void BuildPart5()=0; 19 20 }; 21 22 class StoneHouse: public House{ 23 24 }; 25 26 class StoneHouseBuilder: public HouseBuilder{ 27 protected: 28 29 virtual void BuildPart1(){ 30 //pHouse->Part1 = ...; 31 } 32 virtual void BuildPart2(){ 33 34 } 35 virtual void BuildPart3(){ 36 37 } 38 virtual void BuildPart4(){ 39 40 } 41 virtual void BuildPart5(){ 42 43 } 44 45 }; 46 47 // 稳定 48 class HouseDirector{ // 同样的构建过程 49 50 public: 51 HouseBuilder* pHouseBuilder; 52 53 HouseDirector(HouseBuilder* pHouseBuilder){ 54 this->pHouseBuilder=pHouseBuilder; 55 } 56 57 House* Construct(){ 58 59 pHouseBuilder->BuildPart1(); 60 61 for (int i = 0; i < 4; i++){ 62 pHouseBuilder->BuildPart2(); 63 } 64 65 bool flag=pHouseBuilder->BuildPart3(); 66 67 if(flag){ 68 pHouseBuilder->BuildPart4(); 69 } 70 71 pHouseBuilder->BuildPart5(); 72 73 return pHouseBuilder->GetResult(); 74 } 75 };
模式定义
将一个复杂对象的构建与其表示相分离,使得同样的构建过程(稳定)可以创建不同的表示(变化)。--《设计模式》Gof
要点总结:
- Builder模式主要用于“分步骤构建一个复杂对象”。在这其中“分步骤”是一个稳定算法,而复杂对象的各个部分则经常变化。
- 变化的点在哪里,封装哪里——Builder模式主要在于应对“复杂对象各个部分”的频繁需求变动。其缺点在于难以应对“分步骤构建算法”的需求变动。
- 在Builder模式中,要注意不同语言中构造器内调用虚函数的差别(C++ vs. C#)。
解决方案 :与模板方法类似,将创建流程在基类中实现,子过程作为protected的虚函数,在子类中再实现。实现时需要注意不同语言在构建对象时调用虚函数的区别。Prototype与Builder实际应用中并不多。