C++通用动态抽象工厂的实现详解

目录

  • 背景
  • 实现
  • 寄存参数
  • 存储所有构造出来的对象
  • 寄存指针,可析构的单例
  • 装饰工厂函数,责任链工厂
  • 允许构造函数之外的参数组合
  • 总结

背景 一开始,我是想到了下面这个场景:
struct A {void Foo() {}}; struct B {void Bar() {A().Foo(); }};

如上面代码,B的Bar中构造了A,然后调用了A的Foo函数。假如我想在别的场景中复用B的Bar,但是这时A的Foo就不符合需求了,需要定制,于是我们想到可以用多态,把A的Foo改成虚函数,然后写出A1:
struct A {A() = default; virtual ~A() = default; virtual void Foo() {}}; struct A1 : public A {void Foo() override {}};

B不应该知道A1的存在,为了让B用上A1,同时也为以后可能会拓展的A2、A3做准备,我们写了个A的工厂函数GetA()来生成A。于是代码变成:
std::unique_ptr GetA() {if (Condition1()) {return std::unique_ptr(new A1()); } else {return std::unique_ptr(new A()); }}struct B {void Bar() {GetA()->Foo(); }};

如果B中还要构造别的C、D等类,难道我们要为每个类都写一个工厂函数吗?这成本也太高了,而且可能大部分类都只有一个,不用搞继承,写工厂函数就是无用功。那么,有没有一种通用的方式可以在写B代码的时候就对A、C、D等类的构造都留一手,使得以后可以由B外的代码控制实际构造的是A1等,而不需要修改B的代码?这就是我写动态抽象工厂的原始需求。

实现 思路很简单,就是为每个类都自动生成一个工厂函数就好了,然后在B中不直接构造A、C、D等对象,而是都调用对应类的工厂函数。然后这个自动生成的工厂默认就是构造原始的类的对象,比如A,也有接口可以改成生成子类,比如A1。对每个类生成一个工厂函数自然就想到用模板了。至于这个接口怎么实现,就有两大分支,分别是编译期和运行期。编译期一般就是用模板特化了。我觉得运行期会更有趣,用法会更多,就选了运行期。
运行期的意思就是要搞个变量来存下这个修改的工厂函数,自然就想到用std::function。当然免不了的是要把这个变量传给B。如果管理A的是一个变量,管理C、D的是另外两个变量,那就要传很多很多变量给B,这样也太繁琐了,所以应该一个变量存下所有类的工厂函数,然后把这个变量传遍所有需要使用工厂函数的对象或函数当中。所以这个变量的类型是一个确定的类型,不能带模板(或者说模板参数不能跟工厂对应的类相关,如A、C、D等)。那么模板就放到方法当中了。很自然地,这个类型的接口就应该是这样:
struct DynamicAbstractFactory {template std::unique_ptr New(Args&&...); };

这里插一段,为什么叫动态抽象工厂呢?按照我的理解,工厂模式就是实现一个返回T*的函数F,里面用ifelse来控制最终返回的是T还是T的某个子类。抽象工厂模式就是连这个函数F都是可变的。动态是指这个F是运行时可变。
那么这个接口怎么实现呢?我的想法是用一个map来记录类型组合(T, Args&&...)到工厂函数std::function的映射,并存储std::function。New的实现就是查找map中有没有对应的工厂函数,有就调用工厂函数,没有就调用T本身的构造函数。当然,也需要提供一个接口来修改这个map。
要实现这个map还有三个细节:
  • 存储的std::function是不同的类型,需要用类型擦除存储。如果可用C++17的话可直接用std::any,但我的环境有些老代码用gcc7编不过,所以还是只能用C++11,于是用std::shared_ptr来代替(我一开始还是用std::unique_ptr+虚析构函数+继承来实现的,后来才知道std::shared_ptr自带这个功能)。
  • map的key是一个类型组合,就用std::type_index吧。由于std::function已经把整个类型组合包进去了,而且一定会实例化,就直接用它吧。于是key就成了std::type_index(typeid(std::function))。
  • 由于接口New(Args&&...)的每个参数类型Args&&都是引用类型,为了保持一致性,为了map能找到正确的函数,要求std::function中的每个参数也是引用类型,所以上面都写作std::function。比如std::function会转换成std::function
再加上修改map的接口SetFunc,第一版的动态抽象工厂就做好了:
class DynamicAbstractFactory { public:template std::unique_ptr New(Args&&... args) {auto iter = index2func_.find(std::type_index(typeid(std::function))); if (iter != index2func_.end()) {return std::unique_ptr((*reinterpret_cast*>(iter->second.get()))(std::forward(args)...)); }return std::unique_ptr(new T(std::forward(args)...)); }template void SetFunc(std::function&& func) {index2func_[std::type_index(typeid(std::function))] = std::make_shared>(std::move(func)); } protected:std::unordered_map> index2func_; };

于是B的代码及使用就变成这样:
class B { public:B(DynamicAbstractFactory& factory) : factory_(factory) {}void Bar() {factory_.New()->Foo(); factory_.New(); factory_.New(); } protected:DynamicAbstractFactory& factory_; }; // 旧环境,用原始A、C、D// 当然B也可以用factory来生成void Run() {DynamicAbstractFactory factory; factory.New(factory)->Bar(); }// 新环境,用A1、C、Dvoid Run1() {DynamicAbstractFactory factory; std::function get_a = []() {return new A1(); }; factory.SetFunc(std::move(get_a)); factory.New(factory)->Bar(); }

这样就满足了一开始的需求,B在构造A、C、D的时候都留了一手,B并不需要知道实际构造的是什么,在B的外部,Run()和Run1(),可控制在B里具体要构造的对象。
写完后发现这东西作用不止于此,下面写写一些扩展用法。

寄存参数 子类的构造函数的参数可以跟父类不一样,通过lambda捕获来寄存。
struct A2 : public A {A2(int i) {}void Foo() override {}}; void Run2() {DynamicAbstractFactory factory; int i = 0; std::function get_a = [i]() {return new A2(i); }; factory.SetFunc(std::move(get_a)); factory.New(factory)->Bar(); }


存储所有构造出来的对象 上面的接口返回std::unique_ptr,还要管理对象生命周期,不如更进一步,用factory来管理所有它构造的对象,在factory析构时统一析构。因为我一般写后台rpc接口,可以在rpc请求开始时构造factory,在构造好回包后析构factory,这样在整个请求周期构造的对象都在,指针不会失效,而且在请求结束后可以很方便地进行异步析构,直接把factory丢到析构线程就好。
于是New接口返回值由std::unique_ptr改成T*,同时New可能会造成误解,改成Get。当然,存储肯定要用到类型擦除存储。就成了下面这样:
class GeneralStorage { public:GeneralStorage(size_t reserve_size = 256) {storage_.reserve(reserve_size); }~GeneralStorage() {// 保证按添加顺序逆序析构while (storage_.size() > 0) {storage_.pop_back(); }}template T* EmplaceBack(Args&&... args) {auto p_obj = std::make_shared(std::forward(args)...); storage_.push_back(p_obj); return p_obj.get(); } protected:std::vector> storage_; }; class DynamicAbstractFactoryWithStorage { public:template T* Get(Args&&... args) {auto iter = index2func_.find(std::type_index(typeid(std::function))); if (iter != index2func_.end()) {return (*reinterpret_cast*>(iter->second.get()))(std::forward(args)...); }return storage_.EmplaceBack(std::forward(args)...)); }template void SetFunc(std::function&& func) {index2func_[std::type_index(typeid(std::function))] = std::make_shared>(std::move(func)); } protected:std::unordered_map> index2func_; GeneralStorage storage_; };

有个细节是对于改变过的工厂函数返回的指针是不应该存在storage_中的,而应该是在工厂函数中把对象存进storage_。上面的Run1()应该改成这样:
void Run1() {DynamicAbstractFactoryWithStorage factory; std::function get_a = [&factory]() {return factory.Get(); }; factory.SetFunc(std::move(get_a)); factory.Get(factory)->Bar(); }


寄存指针,可析构的单例 当返回值由std::unique_ptr改成T*,就可以实现寄存指针了。可析构的单例指每次请求都重新构造,在请求结束后析构,但是请求之中只构造一次。看下面例子:
struct C {C(DynamicAbstractFactoryWithStorage& factory) {std::function func = [this](DynamicAbstractFactoryWithStorage& factory) {return this; }; factory.SetFunc(std::move(func)); }}; void Run() {DynamicAbstractFactoryWithStorage factory; factory.Get(factory); // 构造C,并通过SetFunc把对象的指针寄存到factory中factory.Get(factory); // 调用C构造函数中的func,直接返回寄存的指针,不重复构造C// factory析构时C的对象将被析构}


装饰工厂函数,责任链工厂 只要再加个接口GetFunc来获取当前的工厂函数,就可以对工厂函数玩装饰模式了。
GetFunc接口:
// 其它代码与上面一样class DynamicAbstractFactoryWithStorage { public:template std::function GetFunc() {auto iter = index2func_.find(std::type_index(typeid(std::function))); if (iter != index2func_.end()) {return *reinterpret_cast*>(iter->second.get()); }std::function default_func = [this](Args&&... args) {return storage_.EmplaceBack(std::forward(args)...)); }; return default_func; }};

统计调用次数:
struct C {C(DynamicAbstractFactoryWithStorage& factory) {std::function func = [this](DynamicAbstractFactoryWithStorage& factory) {return this; }; factory.SetFunc(std::move(func)); }uint32_t cnt_ = 0; }; void Run() {DynamicAbstractFactoryWithStorage factory; auto func = factory.GetFunc(); std::function get_a = [func, &factory]() {++factory.Get()->cnt_; return func(); }; factory.SetFunc(std::move(get_a)); factory.Get(factory)->Bar(); }

用责任链模式搞个工厂:
struct D {D() {}D(int i) {}}; struct D1 : public D {D1(int i) {}static void SetFactoryD(DynamicAbstractFactoryWithStorage& factory) {// GetFunc的结果是std::fuction类型的,这里经过了一次类型转换std::function func = factory.GetFunc(); std::function new_func = [func, &factory](int i) -> D* {// 责任链模式if (Check(i)) {return factory.Get(i); } else {return func(i); }}; factory.SetFunc(std::move(new_func)); }// 构造D1的条件static bool Check(int i) {return i == 1; }}; // 与D1类似,除了Checkstruct D2 : public D {D2(int i) {}static void SetFactoryD(DynamicAbstractFactoryWithStorage& factory) {std::function func = factory.GetFunc(); std::function new_func = [func, &factory](int i) -> D* {if (Check(i)) {return factory.Get(i); } else {return func(i); }}; factory.SetFunc(std::move(new_func)); }// 构造D2的条件static bool Check(int i) {return i == 2; }}; void Run() {DynamicAbstractFactoryWithStorage factory; D1::SetFactoryD(factory); D2::SetFactoryD(factory); factory.Get(0); // Dfactory.Get(1); // D1factory.Get(2); // D2}


允许构造函数之外的参数组合 上面的实现要求new T(std::forward(args)...)能合法生成一个T对象指针,在一些情况下很难做到,比如T中有难以初始化的成员,又比如T是一个抽象类:
struct E {E() = default; virtual ~E() = default; virtual void Foo() = 0; };

这样就要修改Get接口的逻辑,改成如果能合法调用构造函数,就调用,否则就不调用。但是这样放开之后,就各种参数组合都可以搞了,我觉得这样可能会很混乱,这边设置了这个参数组合,那边设置了另外的参数组合,不知道一共设置了哪几种参数组合。我觉得还是要加点限制,就规定参数组合必须在基类中定义。规定了一个方法名FactoryGet,所有非构造函数的参数组合要定义一个静态FactoryGet方法,方法返回T*,比如:
struct E {E() = default; static E* FactoryGet(int) {return nullptr; }virtual ~E() = default; virtual void Foo() = 0; };

这样Get接口的逻辑就可以改成如果能合法调用构造函数,就调用,否则就调用对应的FactoryGet方法,其他参数组合将会编译报错。同时也规定FactoryGet获得的指针不存进通用存储。于是DynamicAbstractFactoryWithStorage就改成这样:
// new T(std::forward(args)...)// T::FactoryGet(std::forward(args)...)// 要求上面两个表达式有且仅有一个合法并且返回T*,Get调用合法的那个。template struct DefaultGet; template struct DefaultGet(0))...)), T*>::value, void>::type> {static T* Get(GeneralStorage& storage, Args&&... args) {return T::FactoryGet(std::forward(args)...); }}; template struct DefaultGet(0))...)), typename std::decay::type*>::value, void>::type> {static T* Get(GeneralStorage& storage, Args&&... args) {return storage.EmplaceBack::type>(std::forward(args)...); }}; class DynamicAbstractFactoryWithStorage { public:// 每个Args都要是引用template using FuncType = std::function; template T* Get(Args&&... args) {auto iter = index2func_.find(std::type_index(typeid(FuncType))); if (iter != index2func_.end()) {return (*reinterpret_cast*>(iter->second.get()))(std::forward(args)...); }return DefaultGet::Get(storage_, std::forward(args)...); }template void SetFunc(std::function&& func) {index2func_[std::type_index(typeid(FuncType))] = std::make_shared>(std::move(func)); }template FuncType GetFunc() {auto iter = index2func_.find(std::type_index(typeid(FuncType))); if (iter != index2func_.end()) {return *reinterpret_cast*>(iter->second.get()); }FuncType default_func = [this](Args&&... args) {return DefaultGet::Get(storage_, std::forward(args)...); }; return default_func; } protected:std::unordered_map> index2func_; GeneralStorage storage_; };

这样E就能像上面那样用了。另外,想要返回const指针也是可以的。
struct E {E() = default; // 返回值改成了const E*static const E* FactoryGet(int) {return nullptr; }virtual ~E() = default; virtual void Foo() = 0; }; struct E1 : public E {E1(int i) {}static void SetFactoryE(DynamicAbstractFactoryWithStorage& factory) {std::function func = factory.GetFunc(); std::function new_func = [func, &factory](int i) -> const E* {if (Check(i)) {return factory.Get(i); } else {return func(i); }}; factory.SetFunc(std::move(new_func)); }static bool Check(int i) {return i == 1; }void Foo() override {}}; void Run() {DynamicAbstractFactoryWithStorage factory; E1::SetFactoryE(factory); factory.Get(0); // nullptrfactory.Get(1); // const E1*}


总结 【C++通用动态抽象工厂的实现详解】到此这篇关于C++通用动态抽象工厂的文章就介绍到这了,更多相关C++通用动态抽象工厂内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

    推荐阅读