查看原文
其他

C++ DP.08 Factory Method

cpluspluser CppMore 2023-04-20

8.0 Singleton优化补充

注:以后每篇的第0节作为上篇模式的补充(如果存在问题的话),正文从第1节开始。

Singleton的遗留问题在于无法支持带有参数的对象,而我们无法知道未知类型构造时是否存在参数,以及存在几个参数,以前的解决方法是重载或特化,提供1-10个不同参数版本的实现,这令代码非常重复。

modern c++可以使用之前介绍的可变参数模板来优雅的解决,实现如下:

1template<typename... Args>
2static T& instance(Args&&... args) {
3    static T obj(std::forward<Args>(args)...);
4    return obj;
5}

这里还运用了rvalue references和perfect forwarding,modern c++系列中也有介绍。

此外,dead-refernce版本也需要修改,限于篇幅,这里不做展开,直接参考okdp库实现:https://github.com/gxkey/okdp/blob/master/singleton/singleton.hpp

8.1 设计中的变与不变

一个系统要使寿命够长,一定要能够适应变化,否则它到达极限点的时间会很短,之后便会走向衰落。

一个软件系统,要适应用户需求、语言特性更新、运行环境、产品变更等等变化,若不对变化做出反应,软件便会走向死亡。世界是非连续性的,变化再所难免,那么应对变化就可分为提前防御和后期修复,提前防御就是事先采用好的架构设计,后期修复就是软件的重构。

好的架构可以提前防御变化,它提炼了那些不变的东西,这不仅可以增强软件的稳定性,还能提高代码的可复用性。以不变应万变,这些由开发经验总结的不变的东西,就形成的特定问题的设计模式。

开发中需要实例化对象,对象由类产生。C中可用malloc创建对象,创建对象分为两步,分配内存和初始化数据。C++将两步合一,只要使用new,那么会自动完成分配内存,并调用构造函数初始化数据。二者的共同点在于,都必须要指定具体的类型才能产生对象,类型在编译期产生,而对象却在执行期产生,这意味着什么?

意味着你无法在执行期产生一个类,在代码中指定的类型无法动态改变,这是不变的东西。而在后期你又有极大可能需要改变类型(只因要产生不同的对象),此时每个new的地方都可能需要你修改,那要修改的地方可就多了。

那么如何处理此处情况呢?依旧使用抽象化工具,那么将对象的创建抽象就可得到一批创建型模式,它们分别适用于不同的情境,如已经介绍过的Singleton,就是对“只能产生一次的对象”进行抽象。这里已经有了一个创建型模式的一点思路,因为new一个类型无法改变,那么就需要抽象出一个不变的接口,在Singleton中为instance(),之后再借助C++抽象化工作泛型编程,将类型抽象,以后面对任何类型,都可以自动实现“只能产生一次的对象”。

而工厂模式,专注于“以统一的接口产生对象”,这个接口一定是稳定的,我们命名为"create"。接口属于抽象层,具体的类属于具体层(DIP);接口开放,具体的类封闭(OCP);接口供用户使用,实现则自己隐藏。[注:DIP,依赖倒转原则;OCP,开放-封闭原则]

依赖多态,抽象层的接口得以作用到具体层的实现,我们可经由抽象接口来操作具体实现。此时,易变的东西,转换为了稳定的东西。

8.2 Factory Method

工厂模式通常分为三个模式:Simple Factory、Factory Method、Abstract Factory。

三者都提供有上述抽象后的“create”接口,区别在于适用的范围,Simple Factory用于简单地产生同类产品,Factory Method用于产生一类不同的产品,Abstract Factory用于产生多类不同的产品。

比如:

1// only show
2Fruit* create(const char* name)
3
{
4    if(name == "Apple")
5        return new Apple;
6    else if(name == "Pineapple")
7        return new Pineapple;
8    else ...
9}

这便是一个典型的Simple Factory,经由传入的类名称,可以动态生成不同的水果对象。不过它采用了缺点众多的type switch(形如if-elseif, switch…等依赖叠加条件分支进行的分派方式称为type switch)来完成分派,这会导致代码难以维护,难以扩展,而且在相应的文件中要包含所有具体产品的头文件。

Factory Method试图通过多态来解决这些问题,比如改变上述代码为:

1// only show
2struct FruitFactory 
3{

4    virtual Fruit* create() 0;
5    virtual ~FruitFactory() {}
6};
7
8struct AppleFactory : public FruitFactory 
9{
10    virtual Fruit* create() override
11    
{
12        return new Apple;
13    }
14};
15
16struct PineappleFactory : FruitFactory 
17{
18    virtual Fruit* create() override
19    
{
20        return new Pineapple;
21    }
22};
23
24// 此时这样使用
25FruitFactory* factory = new AppleFactory;
26Fruit* apple = factory.create();

type switch的确被消除了,此时要想扩展新的产品,比如Lemon,只要添加一个新的LemonFactory,而无需修改原本的代码(OCP)。此外,若要切换产品为菠萝,你只需将new AppleFactory更改为new PineappleFactory,无需更改其它任何地方的代码。可以看到,Factory Method比Simple Factory好的地方在于支持OCP,更改代码不会影响原有的代码。

然而,Factory Method的缺点也显而易见,它比Simple Factory要写的代码多得多,每一个产品都要对应一个Factory类。

8.3 加入okdp,泛化实作Object Factory

要把Factory Method加入okdp,只靠上面这种实现可不够,我们要的是泛化后的产品,这样别人才能直接使用。

其实上述实现还有一个致命缺点,它无法在执行期产生对象,因为它直接使用new AppleFactory,而类只能在编译期产生。

一个有效的解决办法是经由object-class-object,先由一个对象在执行期获取指定类型,在经由此类型产生对象。常用方法是通过一个对象标识符来获取指定类型,这个标识符可以是int,也可以是enum,也可以是字符串。

欲通过对象标识符获取指定类型,可以通过MFC动态生成对象那种侵入式的方式,还可以通过建立映射关系来完成。

侵入式会修改原有类的结构,而且不够现代,所以我们选择映射的方式,于是添加一个map<string, function>。

此外,这是个产生对象的接口,叫Factory Method不太合适,所以我们重新命名为object_factory,现在搭建起基本结构:

1// first implement
2template<typename AbstractProduct>
3class object_factory
4{

5public:
6    template<typename T>
7    void register_type(const std::string& key)
8    
{
9        map_.emplace(key, []{ return new T(); });
10    }
11
12    void unregister(const std::string& key)
13    
{
14        this_type::instance().map_.erase(key);
15    }
16
17    AbstractProduct* create(const std::string& key)
18    
{
19        if(map_.find(key) == map_.end())
20            throw std::invalid_argument("error: unknown object type passed to factory!");
21        return map_[key]();
22    }
23private:
24    std::map<std::stringstd::function<AbstractProduct*()>> map_;
25};

麻雀虽小,但结构已然俱全。我们想要可以这样使用:

1object_factory<Fruit> factory;
2factory.register_type<Apple>("Apple");
3factory.register_type<Pineapple>("Pineapple");
4
5auto apple = factory.create("Apple");

但是很可惜,编译器会抱怨你register_type无法编译,因为成员函数不支持偏特化。可是我们又必须使用该特性,此时可以如何解决?

我们想要像函数一样使用,而又想要偏特化特性,大家应该可以想到仿函数。

于是,将register_type变为一个仿函数:

1template<typename T>
2struct register_type
3{

4    // registers T into object factory.
5    register_type(const std::string& key)
6    {
7        map_.emplace(key, []{ return new T(); });
8    }
9
10    // register T with variadic parameters into object factory 
11    template<typename... Args>
12    register_type(const std::string& key, Args... args)
13    {
14        //(std::cout << ... << args) << std::endl;
15        map_.emplace(key, [=]{ return new T(args...); });
16    }
17};

这里还使用可变参数模板提供了多参数版本,现在可以满足我们的上述注册要求“使用像函数,却拥有偏特化的特性”了。

哦,等等!工厂应该全局只有一个,而我们的object_factory却可以有无限个实例,这让你想到了什么?没错,上篇实现的Singleton可以使我们的类天生具有Singleton属性,于是借助okdp来更改实现:

1template<typename AbstractProduct>
2class object_factory : public okdp::singleton<object_factory<AbstractProduct>>
3{
4    using this_type = object_factory<AbstractProduct>;
5public:
6
7    template<typename T>
8    struct register_type
9    {

10        // registers T into object factory.
11        register_type(const std::string& key)
12        {
13            this_type::instance().map_.emplace(key, []{ return new T(); });
14        }
15
16        // register T with variadic parameters into object factory 
17        template<typename... Args>
18        register_type(const std::string& key, Args... args)
19        {
20            //(std::cout << ... << args) << std::endl;
21            this_type::instance().map_.emplace(key, [=]{ return new T(args...); });
22        }
23    };
24
25    // removes object 
26    void unregister(const std::string& key)
27    
{
28        this_type::instance().map_.erase(key);
29    }
30
31
32    /// return concrete object by invoking new operator
33    /// !!!note!!! if use this method user should delete it to avoid memory leaks.
34    AbstractProduct* create(const std::string& key)
35    
{
36        if(this_type::instance().map_.find(key) == this_type::instance().map_.end())
37            throw std::invalid_argument("error: unknown object type passed to factory!");
38        return this_type::instance().map_[key]();
39    }
40
41private:
42    std::map<std::stringstd::function<AbstractProduct*()>> map_;
43};

喔!我们基本什么也没有做,而object_factory已然成为一个单例类,这正是okdp带给你的美好体验。

接着,把所有的实现也被替换为了单例产生的对象之上,如代码中所示,现在我们就初步完成了任务。再也无需为类型额外添加一个对应的工厂类,只要将具体产品进行注册就可以,此外,还可以在执行期来产生对象。

8.4 所有权管理,使用智能指针

目前为止,可以通过create接口获取已注册的对象,我们再看回使用代码:

1object_factory<Fruit> factory;
2factory.register_type<Apple>("Apple");
3factory.register_type<Pineapple>("Pineapple");
4
5auto apple = factory.create("Apple");

这里的create是以new的形式返回类对象,这意味着对象的所有权将由用户掌管(事实上,Factory相关模式的所有权都属于用户)。而用户总是会在不经意间忘记使用delete释放对象,所以作为库作者,我们最好提供自动管理对象生命期的方式,智能指针可以实现这一点。

但需要注意,并非所有用户都希望返回一个智能指针,在某些情况下,他可能更想获得原始指针,因此我们最好二者兼备。

我们无需更改原有create接口,只需增加两个接口,一个产生shared_ptr,一个产生unique_ptr。这两个智能指针管理的对象可以经由create直接产生,在object_factory中添加接口如下:

1/// return concrete object by invoking shared ptr
2std::shared_ptr<AbstractProduct> create_shared(const std::string& key)
3{
4    return std::shared_ptr<AbstractProduct>(create(key));
5}
6
7
8/// return concrete object by invoking unique ptr
9std::unique_ptr<AbstractProduct> create_unique(const std::string& key)
10{
11    return std::unique_ptr<AbstractProduct>(create(key));
12}

现在,我们可以通过三个接口来获取想要的对象,

1auto apple = factory.create("Apple");
2auto spApple = factory.create_shared("Apple");
3auto upApple = factory.create_unique("Apple");
4
5// 原始指针由自己释放
6delete apple;

它们所产生的对象的区别在于对象的所有权管理上,智能指针会自己释放对象,而原始指针依赖于用户自己管理。

8.5 提供更容易使用的注册宏

通过目前的实作,要产生对象需要先调用register_type注册对象:

1object_factory<Fruit> factory;
2factory.register_type<Apple>("Apple");
3factory.register_type<Pineapple>("Pineapple");

可以发现注册代码存在着大量重复,其实只有注册类型不同,却要重复写不少代码,此时可以将重复的部分继续抽象,提取出来。而我们又不想改变原有代码,此时可以经由宏来提供接口(宏是C的抽象化工具,也可实现元编程)。

我们提供一个FACTORY_REGISTER宏:

1#define FACTORY_REGISTER(Base, Derived, ...) \
2    static okdp::object_factory<Base>::register_type<Derived> reg_##Derived(#Derived, ##__VA_ARGS__);

由于每个对象只会被注册一次,所以可以根据对象名来自动生成静态变量名称,由于register_type是个仿函数,所以我们可以声明多个变量,而在声明之时,其构造函数已经获取参数并完成了向map中的注册操作,这得益于object_factory是个Singleton对象。

这里稍微介绍一下实现中的宏操作,Base是抽象产品,即例子中的Fruit;Derived是具体产品,即例子中的Apple, Pineapple等等;而"…"用于接受任意个数的参数,参数用于产生对象时传给具体产品的构造函数。

"##"后而加类型名,会将类型和前面的字符串连接起来,所以若Derived是Apple,那么reg_##Derived就会被替换为reg_Apple。单个“#”后加类型名,会将这个类型变为字符串,所以Apple就会替换为"Apple"。最后,__VA_ARGS__用于展开可变参数,在其前面加上"##"可用于支持0个参数,否则在没有参数的情况下会带上一个“,“,这会导致编译失败。

一切就绪,如今我们可以这样注册对象:

1FACTORY_REGISTER(Fruit, Apple)
2FACTORY_REGISTER(Fruit, Pineapple)
3
4// 使用和注册分离
5using FruitFactory = okdp::object_factory<Fruit>;
6FruitFactory factory;
7auto apple = factory.create("Apple");
8auto pineapple = factory.create("Pineapple");

而且由于是声明的静态变量,所以可以将注册对象写在全局之处,将注册和产生对象进行分离。

8.6 使用Concepts优化代码

等等,要是用户注册一个不属于抽象产品的具体产品会如何,如:

1FACTORY_REGISTER(Fruit, Log) // aha!

可以肯定的是,这绝对是编译不过的,因为返回类型是抽象产品Fruit,而Log不属于此继承体系。为了避免用户的谩骂,我们希望能提供良好的提示,然而当我们进行编译时却会得到如下结果:

错误代码多达5页,我们很难在这些提示中找到有用信息来修正代码,这正是我们在Using C++20 Concepts中所说的模板不支持Well-specified interfaces,所以编写模板代码查错很痛苦:(

而Concepts正好可以解决此问题,因此这里我们将使用C++最新的特性之一来优化代码。在此之前,我们需要确定编译器是否支持C++20,如果不支持而你又使用了此特性,别人就会编译不了你的库。

常用法是根据__cpluplus宏来判断C++版本,如果支持C++20我们则使用Concept,反之则只好保留旧实现并对用户注以提示。

首先依赖版本来选择是否包含concept库,

1#if defined(__cplusplus) && __cplusplus >= 201709L 
2    #include <concepts>
3#endif

其次为注册代码添加约束,只有派生自抽象产品的具体产品才能够注册:

1template<typename T>
2#if defined(__cplusplus) && __cplusplus >= 201709L 
3requires std::derived_from<T, AbstractProduct>
4#endif
5struct register_type
6{
};

std::derived_from是C++20提供的默认concept,可以判断一个类是否派生自某类,这正合我意。

现在重新使用C++20来编译刚才错误的代码:

现在错误提示显而易见,GOOD JOB!

8.7 使用okdp为你的类添加工厂

我们以okdp来实现本文的例子,你只需提供具体产品名称,就能直接得到一套抽象产品的对象工厂。

实现如下:

1#include "object_factory.hpp"
2#include <iostream>
3
4struct Fruit 
5{

6    virtual void print() std::cout << "fruit print" << std::endl; }
7};
8
9struct Apple : public Fruit
10{
11    const  char* str_;
12    Apple(const  char* str) : str_(str) {}
13    void printstr() std::cout << str_ << std::endl; }
14
15    virtual void print() override {
16            std::cout << "apple print" << std::endl;
17           }
18};
19
20struct Pineapple : public Fruit
21{
22    virtual void print() override {
23        std::cout << "pineapple print" << std::endl;
24    }
25};
26
27// 注册产品
28FACTORY_REGISTER(Fruit, Apple, "register apple")
29FACTORY_REGISTER(Fruit, Pineapple)
30
31int main()
32
{
33    using FruitFactory = okdp::object_factory<Fruit>;
34    FruitFactory factory;
35
36    auto apple = (Apple*)factory.create("Apple");
37    apple->print();
38    apple->printstr();
39
40    auto pineapple = factory.create("Pineapple");
41    pineapple->print();
42
43
44    return 0;
45}

输出如下:

使用起来非常简单吧!

实际上,Abstract Factory的一种实现方式就可以直接借助object_factory,不过C++还有更加牛逼的实现方式,就是利用我们在Understanding variadic templates中介绍的recursive inheritance templates,不同的是,那里只是最简单的使用,和Abstract Factory中复杂的recursive inheritance templated code比起来算是小巫见大巫。

所以大家最好先消化本篇的Factory,因为后续两篇的模式实现将会非常复杂,而你也将真正进入到C++的模板元编程世界。

最后,若想查看okdp::object_factory的完整实现或测试代码的,可直接到github clone:https://github.com/gxkey/okdp


-okdp系列文章-

C++ DP.07 Singleton


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存