C++ DP.08 Factory Method
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::string, std::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::string, std::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