在C++中,通常,我们用构造函数创建对象。但这种方式存在几个限制:
工厂模式绕开了上面的限制。从调用者来看,工厂方法仅是一个普通方法,返回类的实例。不过,工厂模式经常和继承一起使用,可以通过传入不同的参数,以构造不同的派生类对象。
本文主要探讨使用抽象基类(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.