C/C++教程

C++ 工厂模式

本文主要是介绍C++ 工厂模式,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

目录
  • 工厂模式解决什么问题?
  • 抽象基类
  • 工厂方法
    • 简单实现(参数化的工厂)
    • 工厂方法的意义
  • 扩展工厂
  • 参考

工厂模式解决什么问题?

在C++中,通常,我们用构造函数创建对象。但这种方式存在几个限制:

  • 没有返回值。构造函数不能返回结构,如果发生错误,调用者无法通过返回NULL指针得知。(不过可以在构造函数内抛出异常)
  • 命名限制。C++要求构造函数名与所在类的名字相同,也就是说,如果我们调用了A类构造函数,那么很容易知道是构造A类对象,并且只能构造出A类对象,无法构造B类对象。
  • 静态绑定创建。构造对象时,必须指定编译时能确定的特定类名,构造函数没有运行时动态绑定的概念。例如,不能通过A的构造函数创建B类对象,也不能通过反射创建类对象。
  • 不允许虚构造函数。C++中,不能声明virtual构造函数。必须指定编译时要构造的对象的精确类型,编译器才能为特定类型分片内存并为任意基类调用构造函数。不能在构造函数中调用virtual方法,并期望它们调用派生类的重写版本,因为派生类此时还没有初始化,而调用virtual方法前提是派生类对象已经初始化。

工厂模式绕开了上面的限制。从调用者来看,工厂方法仅是一个普通方法,返回类的实例。不过,工厂模式经常和继承一起使用,可以通过传入不同的参数,以构造不同的派生类对象。

本文主要探讨使用抽象基类(Abstract Base Class)实现工厂模式。


抽象基类

抽象基类是包含一个或多个纯虚函数(pure virtual function)的类。它不能实例化,只能用作基类,由派生类提供纯虚方法的实现。
例,

// renader.h
#include <string>

class IRender // 抽象基类, I作为前缀表明这是一个接口类
{
public:
    virtual ~IRender() {}
    virtual bool LoadScene(cosnt std::string& filename) = 0;  // 加载场景
    virtual void SetViewportSize(int w, int h) = 0;           // 设置视角大小
    virtual void SetCameraPosition(double x, double y, double z) = 0; // 设置相机位置
    virtual void SetLookAt(double x, double y, double z) = 0; // 设置视点
    virtual void Render() = 0;
};

通过为方法添加后缀“=0”,将方法声明为纯虚函数,从而表明该类是一个抽象基类(无法通过new操作费实例化)。

注意:“纯虚方法不提供实现”的说法是错误的,因为可以在.cpp中提供默认实现。但在派生类中仍需要显式重写方法。

抽象基类可以用来描述多个类共享的行为的抽象单元,(从语法层面)指定所有派生类都必须遵守的契约。


工厂方法

工厂模式的意图:定义一个创建对象的接口,让其子类自己决定实例化哪一个类,工厂模式使一个类的实例化延迟到其子类进行。

所谓创建对象的接口,就是指的工厂方法。对用户来说,不再由A a = new A(); 这种方式来创建A类对象,而是由工厂方法来创建。至于具体创建哪个,可以由具体的工厂方法来决定。

其实有两类工厂类:
1)工厂类本身是一个抽象类,由具体的工厂类来创建不同对象,通常是一个具体的工厂只创建一种对象;

2)参数化的工厂方法,由不同参数决定创建何种对象,通常一个工厂创建多种对象。

通过构造函数创建指定对象:

工厂模式创建对象(具体的工厂类):

工厂模式创建对象(参数化的工厂):

简单实现(参数化的工厂)

在renderer.h基础上,实现一个简单的工厂方法,返回IRender类型对象。

// rendererfactory.h
#include "renderer.h"
#include <string>

class RendererFactory
{
public:
    IRenderer* CreateRender(const std::string& type);
};

RendererFactory::CreateRender() 只是返回一个对象实例的普通方法,不能返回具体类型为IRenderer的实例,因为IRenderer是抽象基类无法实例化,不过可以返回派生类实例。方法根据字符串参数决定要创建那个派生类的实例。

假设已经实现了3个IRenderer派生类:OpenGLRenderer、DirectXRenderer和MesaRenderer,并且用户不知道这些类型:API对用户是完全透明的,而且用户不include这3个派生类头文件。
基于此,提供工厂方法的一个实现:

// rendererefactory.cpp
#include "rendererefactory.h"
#include "openglrenderer.h"
#include "directxrenderer.h"
#include "mesarenderer.h"

IRenderer *RendererFactory::CreateRender(const std::string& type)
{
    if (type == "opengl")
        return new OpenGLRenderer();
    else if (type == "directx")
        return new DirectXRenderer();
    else if (type == "mesa")
        return new MesaRenderer();
    return NULL; // 不支持类型
}

工厂方法的意义

1)运行时由用户或配置,决定创建何种类
上面示例中,工厂方法可以返回IRenderere的3个派生类中的任意一个,返回哪个类型取决于客户传递的字符串参数。这也就意味着,允许用户运行时决定要创建哪个派生类,可以根据用户输入或读取的配置文件来创建不同的类,而不是编译时要求用户使用正常的构造函数创建固定的类。

2)隐藏派生类,用户无需关注
各个派生类的头文件仅包含在工厂方法的cpp文件中,不会出现在公有头文件rendererfactory.h中。也就是说,这些私有头文件不需要和API一起发布,用户看不到不同renderer的私有细节。用户只能通过字符串变量指定renderer(当然参数也可以改成其他类型,比如枚举)。

上面工厂方法的缺陷:如果要为系统添加新的渲染器,将不得不修改rendererfactory.cpp。如果要分次添加100个,可能要修改100次!这个问题可以通过可扩展的对象工厂来解决。


扩展工厂

如何将具体的派生类和工程方法解耦,并支持在运行时添加新的派生类?
可以这样修改工厂类:工厂类维护一个映射,此映射将类型名与创建对象的回调关联起来;然后,可以允许新的派生类通过一对新的方法调用来实现注册和注销。

因为工厂对象维护了一个映射,所以是有状态的。因此,最好强制要求任一时刻都只能创建一个工厂对象。这也是为何多数工厂对象时单例的原因。简洁起见,示例使用静态方法和变量,以实现单例工厂:

// rendererefactory.h
#include "renderer.h"
#include <string>
#include <map>

class RendererFactory
{
public:
    typedef IRenderer*(*CreateCallback)();
    static void RegisterRenderer(const std::string& type, CreateCallback cb);
    static void UnregisterRenderer(const std::string& type);
    static IRenderer* CreateRenderer(const std::string& type);

private:
    typedef std::map<std::string, CreateCallback> CallbackMap;
    static CallbackMap renderers_;
};

其相关cpp实现文件:

// rendererfactory.cpp
#include "rendererfactory.h"

// 在RendererFactory中实例化静态变量
RendererFactory::CallbackMap RendererFactory::renderers_;

void RendererFactory::RegisterRenderer(const std::string& type, CreateCallback cb)
{
    renderers_[type] = cb;
}

void RendererFactory::UnregisterRenderer(const std::string& type)
{
    renderers_.erase(type);
}

IRenderer* RendererFactory::CreateRenderer(const std::string& type)
{
    CallbackMap::iterator it = renderers_.find(type);
    if (it != renderers_.end()) {
        // 调用回调以构造此派生类的对象
        return (it->second)();
    }
    return NULL;
}

用户现在可以在系统中注册、注销新的renderer(渲染器),编译器确保用户的新渲染器遵循IRenderer抽象接口(纯虚函数)。

下面代码展示了用户如何定义自定义渲染器,将其注册到对象工厂,然后通过工厂创建实例。

class UserRenderer : public IRenderer
{
public:
    ~UserRendeer() {}
    bool LoadScene(cosnt std::string& filename) @override { return true; }
    void SetViewportSize(int w, int h) @override {}
    void SetCameraPosition(double x, double y, double z) @override {}
    void SetLookAt() @override {}
    void Render() { std::cout << "User Render" << std::endl; }

    static IRenderer* Create() { return new UserRenderer(); } // 每个自定义renderer必须定义一个Create成员方法, 以向RendererFactory注册该创建自身对象的方法
};

int main()
{
    // 注册一个新的渲染器
    RendererFactory::RegisterRenderer("user", UserRenderer::Create);
    
    // 为新渲染器创建一个实例
    IRenderer* r = RendererFactory::CreateRenderer("user");
    r->Render();
    delete r;

    return 0;
}

相比之前的工厂方法,扩展后的工厂方法RendererFactory有很大改进:添加了注册、注销方法。具体的创建渲染器对象的方法,由具体的渲染器自行负责,提供Create方法创建对象,作为工厂方法创建对象的回调。

通过这种方式,RendererFactory不需要知道具体要创建哪些具体的渲染器,而是由用户来决定。

还需要注意:渲染器的回调必须在运行时对函数RegisterRenderer() 可见,但这不意味着需要暴露API内置渲染器。有两种方式可以隐藏内置渲染器:
1)在API初始化例程中注册这些渲染器,而不是在启动后,由用户运行时决定;
2)混合使用简单工厂模式和扩展工厂模式,工厂方法首先检查类型字符串是否为内置名字,如果不是,再检查类型字符串是否为用户已注册的名字。


参考

[1]Martin Reddy, 刘晓娜, 臧秀涛,等. C++ API设计[M]. 人民邮电出版社, 2013.

这篇关于C++ 工厂模式的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!