(Boolan)C++设计模式 <七> ——工厂方法(Factory Method)、 抽象工厂(Abstract Factory)、原型模式(Prototype)、构建器(Builder)

“对象创建”模式

通过“对象创建”模式绕开直接new一个具体的类型,来避免对象创建(new)过程中所导致的紧耦合(依赖具体类型),从而支持对象创建的稳定。他是接口抽象之后的第一步工作。

  • 典型模式
    1. Factory Method
    • Abstract Factory
    • Prototype
    • Builder

工厂方法 (Factory Method)

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

  • 动机
    在软件系统中,经常会面临着创建对象的工作,由于需求变化,需要创建的对象的具体类型也会经常的变化

假设还是拿之前的文件分割器作为例子,如果不使用具体的设计模式,那么在具体创建对象的时候,需要在初始化对象的时候指定出具体的实现细节,虽然ISplitter * splitter = new BinarySplitter(); 是面向接口的编程,将变量生命为了抽象的基类,但在实际new时,依赖了一个具体的实现类。实际上这里违反了依赖导致原则。如果这时候具体的Splitter还未完成实现,那么编译时会报错,如果需求发生变更那么此时,也需要重新修改和编译源代码。

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{
    
};


class MainForm : public Form
{
    TextBox* txtFilePath;
    TextBox* txtFileNumber;
    ProgressBar* progressBar;

public:
    void Button1_Click(){


        
        ISplitter * splitter=
            new BinarySplitter();//依赖具体类
        
        splitter->split();

    }
};

对于之前的代码,在应对变化时,ISplitter * splitter = new BinarySplitter();这里是核心的问题。那么如何才能绕开new一个具体的细节呢?

那么我们可以使用一个函数,来讲所需要的对象返回给我们的方式来获取这个对象,为了代码的进一步隔离,我们把他直接抽象到一个新的类中,并用写成一个虚函数,由工厂的子类来自己实现创建自己的方法。也就形成了一个模式,每个实现方法有一个类,配套的每个类都有一个工厂,而这些工厂都继承自一个抽象的工厂。那么就得到了下面的代码。


//抽象类
class ISplitter{
public:
    virtual void split()=0;
    virtual ~ISplitter(){}
};


//工厂基类
class SplitterFactory{
public:
    virtual ISplitter* CreateSplitter()=0;
    virtual ~SplitterFactory(){}
};





//具体类
class BinarySplitter : public ISplitter{
    
};

class TxtSplitter: public ISplitter{
    
};

class PictureSplitter: public ISplitter{
    
};

class VideoSplitter: public ISplitter{
    
};

//具体工厂
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();
    }
};



class MainForm : public Form
{
    SplitterFactory*  factory;//工厂

public:
    
    MainForm(SplitterFactory*  factory){
        this->factory=factory;
    }
    
    void Button1_Click(){

        
        ISplitter * splitter=
            factory->CreateSplitter(); //多态new
        
        splitter->split();

    }
};

现在的工厂通过MainForm的构造函数传入其中,依赖其实依旧存在,但是MainForm并没有具体类型的依赖,而依赖的都是抽象类。其实对于设计模式来说,并没有办法消除依赖,而是将依赖控制在了最可控的地方。

UML

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

抽象工厂 (Abstract Factory)

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

  • 动机
    在软件系统中,经常会面临着“一系列相互依赖的对象”的创建工作,同时由于需求的变化,往往存在着更多系列对象的创建工作。

假设需要写一个数据访问层,那么需要创建一系列对象,比如需要connection对象、command对象等等。同时在不同的数据库选择的时候,也存在变化的问题。所以一下代码是写死的代码,不能很好的适应未来变化。

class EmployeeDAO{
    
public:
    vector<EmployeeDO> GetEmployees(){
        SqlConnection* connection =
            new SqlConnection();
        connection->ConnectionString = "...";

        SqlCommand* command =
            new SqlCommand();
        command->CommandText="...";
        command->SetConnection(connection);

        SqlDataReader* reader = command->ExecuteReader();
        while (reader->Read()){

        }

    }
};

那么,为了应对数据库变化的情况,那么将数据库访问的一些对象进行向上抽象。


//数据库访问有关的基类
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 Server
class SqlConnection: public IDBConnection{
    
};
class SqlConnectionFactory:public IDBConnectionFactory{
    
};


class SqlCommand: public IDBCommand{
    
};
class SqlCommandFactory:public IDBCommandFactory{
    
};


class SqlDataReader: public IDataReader{
    
};
class SqlDataReaderFactory:public IDataReaderFactory{
    
};

//支持Oracle
class OracleConnection: public IDBConnection{
    
};

class OracleCommand: public IDBCommand{
    
};

class OracleDataReader: public IDataReader{
    
};



class EmployeeDAO{
    IDBConnectionFactory* dbConnectionFactory;
    IDBCommandFactory* dbCommandFactory;
    IDataReaderFactory* dataReaderFactory;
    
    
public:
    vector<EmployeeDO> GetEmployees(){
        IDBConnection* connection =
            dbConnectionFactory->CreateDBConnection();
        connection->ConnectionString("...");

        IDBCommand* command =
            dbCommandFactory->CreateDBCommand();
        command->CommandText("...");
        command->SetConnection(connection); //关联性

        IDBDataReader* reader = command->ExecuteReader(); //关联性
        while (reader->Read()){

        }

    }
};

对于上面的代码来说,把所有的数据库访问有关的对象都创建了基类,如果为了实现其他的数据库,只需要针对对应的基类派生出子类。那么,其实也就是每个对象的创建都使用了工厂模式。

现在已经能够很好的解决变化的问题,但是这时候存在一个新的问题,那就是,这一系列对象并不是彼此分隔的,比如MySQL的Connection对象对应着MySQL的Commend,如果使用错了,那么肯定会在运行时报错,无法完成我们想要达到的目的。那么这个问题该如何解决呢?

我们可以尝试把对应的一系列的对象放在同一个工厂类中,以此来避免出现这样的问题。


//数据库访问有关的基类
class IDBConnection{
    
};

class IDBCommand{
    
};

class IDataReader{
    
};


class IDBFactory{
public:
    virtual IDBConnection* CreateDBConnection()=0;
    virtual IDBCommand* CreateDBCommand()=0;
    virtual IDataReader* CreateDataReader()=0;
    
};


//支持SQL Server
class SqlConnection: public IDBConnection{
    
};
class SqlCommand: public IDBCommand{
    
};
class SqlDataReader: public IDataReader{
    
};


class SqlDBFactory:public IDBFactory{
public:
    virtual IDBConnection* CreateDBConnection()=0;
    virtual IDBCommand* CreateDBCommand()=0;
    virtual IDataReader* CreateDataReader()=0;
 
};

//支持Oracle
class OracleConnection: public IDBConnection{
    
};

class OracleCommand: public IDBCommand{
    
};

class OracleDataReader: public IDataReader{
    
};



class EmployeeDAO{
    IDBFactory* dbFactory;
    
public:
    vector<EmployeeDO> GetEmployees(){
        IDBConnection* connection =
            dbFactory->CreateDBConnection();
        connection->ConnectionString("...");

        IDBCommand* command =
            dbFactory->CreateDBCommand();
        command->CommandText("...");
        command->SetConnection(connection); //关联性

        IDBDataReader* reader = command->ExecuteReader(); //关联性
        while (reader->Read()){

        }

    }
};

以上的代码不难看出,把对应的一套工厂封装在了一起,那么,这就是一种Abstract Factory的模式,其实他的名字如果是Family Factory好像更加合适一点。

Abstract Factory UML

要点总结
如果没有对应“多系列对象构建”的需求变化,则没有必要使用Abstract Factory 模式,这时候使用简单的工厂完全可以。
“系列对象”指的是在某一特定系列下的对象之间有相互依赖或作用关系。不同系列的对象之间不能相互依赖。
Abstract Factory 模式主要在于应对“新系列”的需求变动,其缺点在于难以应对“新对象”的需求变动。

原型模式(Prototype)

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

  • 动机
    在软件系统中,经常面临着“某些结构复杂的对象”的创建工作;由于需求的变化,这些对象经常面临着剧烈的变化,但是他们却拥有着比较稳定一直的接口。

假设将工厂模式中的ISplitter和SplitterFactor两个基类进行合并。并添加一个克隆自己的方式来构建对象的虚方法。使用的时候应该是通过原型对象构造的对象来clone出所需要的对象。


//抽象类
class ISplitter{
public:
    virtual void split()=0;
    virtual ISplitter* clone()=0; //通过克隆自己来创建对象
    
    virtual ~ISplitter(){}

};





//具体类
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);
    }
};


class MainForm : public Form
{
    ISplitter*  prototype;//原型对象

public:
    
    MainForm(ISplitter*  prototype){
        this->prototype=prototype;
    }
    
    void Button1_Click(){

        ISplitter * splitter=
            prototype->clone(); //克隆原型
        
        splitter->split();
        
        

    }
};
Prototype UML

这时候有一个问题,就是原型模式通过克隆的方式获得对象的目的到底是什么呢?这样能够解决什么样的问题呢?
对于某些结构复杂的对象的创建。在某些状态下的工厂会变得比较复杂,并且,创建出来的对象不一定是我们想要的,那么我们只需要使用clone方法就可以得到我们需要的对象。
那么简单一句话就是:本就是你需要从A的实例得到一份与A内容相同,但是又互不干扰的实例的话,就需要使用原型模式。

要点总结
Prototype模式同样用于隔离类对象的使用者和具体类型(易变类)直接的耦合关系,他同样要求这些“易变类”拥有“稳定的接口”。
Prototype模式对于“如何创建易变类的实体对象”采用“原型克隆”的方法来做,它使得我们可以非常灵活地“动态创建”拥有某些稳定接口的新对象——所需工作仅仅是注册一个新类的对象(即原型),然后在任何需要的地方clone。
Prototype模式中的Clone方法可以利用某些框架中的序列化来实现深拷贝。

构建器(Builder)

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

  • 动机
    在软件系统中,有时候面临着“一个复杂对象”的创建工作,期通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将他们组合在一起的算法却相对稳定。

Builder其实和Template Method有一些相似,而Builder主要是为了解决对象创建的问题。

假设需要构建一个房屋的对象,这个房子的建造过程相对固定的,比如打地基、修墙、装门等等,虽然步骤相对固定,但是具体的墙、门、窗可能都存在差异。
那么使用如下的代码,把固定的部分实现为虚函数的流程(这里的做法很像Template Method)。
具体的房子只需要继承House的类,并重写那些虚方法就行了。

class House{
    //....
};

class HouseBuilder {
public:
    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;
    
};

class StoneHouse: public House{
    
};

class StoneHouseBuilder: public HouseBuilder{
protected:
    
    virtual void BuildPart1(){
        //pHouse->Part1 = ...;
    }
    virtual void BuildPart2(){
        
    }
    virtual void BuildPart3(){
        
    }
    virtual void BuildPart4(){
        
    }
    virtual void BuildPart5(){
        
    }
    
};


class HouseDirector{
    
public:
    HouseBuilder* pHouseBuilder;
    
    HouseDirector(HouseBuilder* pHouseBuilder){
        this->pHouseBuilder=pHouseBuilder;
    }
    
    House* Construct(){
        
        pHouseBuilder->BuildPart1();
        
        for (int i = 0; i < 4; i++){
            pHouseBuilder->BuildPart2();
        }
        
        bool flag=pHouseBuilder->BuildPart3();
        
        if(flag){
            pHouseBuilder->BuildPart4();
        }
        
        pHouseBuilder->BuildPart5();
        
        return pHouseBuilder->GetResult();
    }
};
Builder UML
  • 要点总结
    Builder模式主要用于“分步骤构建一个复杂对象”。在这其中“分步骤”是一个稳定算法,而复杂对象的各个部分则经常变化。
    变化的点在哪里,封装哪里——Builder模式主要在于应对“复杂对象各个部分”的频繁需求变动。其缺点在于难以应对“分步骤构建算法的需求变动”。
    在Builder模式中,要注意不同语言中构造器内调用虚函数的差别(C++ vs. C#)。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容