目录
工厂模式解决什么问题?
在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.
原创文章,作者:,如若转载,请注明出处:https://blog.ytso.com/273815.html