【设计模式】Abstract Factory(抽象工厂)模式
概念
提供一个接口创建以创建一系列相关或相互依赖的对象,而无须指定它们具体的类。
个人理解
1. 适用范围
如果说生成器(Builder)模式是为创建单一复杂对象提供接口并掩饰其内部组成,那么抽象工厂模式的应用场景更适用于多个对象聚为一个分组,而这个分组有不同的系列。
通俗的例子比如,vs界面的白色风格和黑色风格,窗口、滚动条、字体颜色等这些对象实际上除了样式风格不一样,在使用上是完全一致的,不可能它换个颜色我就不能使用它的功能了。
再比如,一个项目我链接了一个数据库,使用它的增删改查的功能,但是它是sql还是mysql还是sqlite实际在客户端那边是无所谓的,只要能用这些功能就行。
- 一个系统要独立于它的产品的创建、组合和表示。
- 一个系统要由多个产品系列中的一个来配置。(上面的数据库例子)
- 要强调一系列相关产品对象的设计以便进行联合使用。
- 提供一个产品类库,但只想显示它们的接口而不是实现。
需要理解的是,客户端需要的是产品的功能,而产品具体使用什么方式实现这些功能,通过什么样式呈现出同样的功能,客户端是不关心的。这样实际上在客户端这边我们只需要得到一个 抽象的产品 ,它提供客户端需要的所有功能接口。
我们的初衷是让用户根据自己需要获得对应一系列的对象,并不希望他看到对象里面功能的具体实现,因此需要一个类来根据用户所想来分发对应的对象,这个类就称为 工厂 ,同时我们又希望存在多个具体工厂提供不同系列创建的对象。
2. 代码实现(C++)
2.1 思路
有两个产品对象A和B,A有A1和A2两种,B有B1和B2两种,且A1和B1捆绑为一个系列,A2和B2捆绑为一个系列。
那么抽象工厂模式有以下几个模块:
- AbstractProductA:抽象产品A的接口,它声明A中需要的所有功能接口,而把具体细节的创建交给子类。
- ProductA1、ProductA2:继承于AbstractProductA,用户获得的是一个抽象的产品,因此这两个类中所声明的函数是用户不可见的。(产品B类似)
- AbstractFactory:一个抽象的工厂接口,主要声明创建一个系列所需要的所有对象列表、获得这些对象的方法以及创建这些对象的抽象接口。
- ConcreteFactory1:继承于AbstractFactory,把对象的创建重写为系列1(即A1和B1)的创建方法。(ConcreteFactory2类似,即系列2)
2.2 产品模块
把产品对象A和B场景化为保存一个存放常量字符串链表的类,A1和A2的区别设置在头节点字符串的不同,产品A存在一个对外的(用户可用)功能函数 addA 以向链表中添加节点,最后提供一个 GetList 函数用于获取链表的头节点。
具体类图如下:
抽象产品的构造函数只起到把链表头尾指针初始化的作用,字符串赋值放在了 InitA 中供具体工厂调用来真正创建具体的产品A1/A2。
代码如下:(B类似不再赘述。)
typedef struct ListNode
{
const char* str;
ListNode* next;
ListNode(const char* s)
{
str = s;
next = nullptr;
}
}ListNode;
class AbstractProductA
{
public:
AbstractProductA()
{
headA = new ListNode("Product A head");
tailA = headA;
}
void addA(const char* s)
{
ListNode* p = new ListNode(s);
tailA->next = p;
tailA = p;
}
ListNode* GetList()
{
return headA;
}
protected:
ListNode* headA;
ListNode* tailA;
virtual void InitA() {};
};
class ProductA1 :public AbstractProductA
{
public:
void InitA()
{
headA->str = "Product A1 head"; //A1、A2的区别体现在此处
}
};
class ProductA2 :public AbstractProductA
{
public:
void InitA()
{
headA->str = "Product A2 head";
}
};
2.3 工厂模块
工厂主要是提供创建A、B对象的以及获得它们的接口。
具体结构如下图:
这样设计,创建A和B是分开的,也就是用户可用自行选择什么时候调用CreateProductA/B函数来创建对应A/B对象。
代码如下:(ConcreteFactory2类似,不再赘述)
class AbstractFactory
{
public:
virtual void CreateProductA() {}
virtual void CreateProductB() {}
AbstractProductA& GetProdctA()
{
return productA;
}
AbstractProductB& GetProdctB()
{
return productB;
}
protected:
AbstractProductA productA;
AbstractProductB productB;
};
class ConcreteFactory1 :public AbstractFactory
{
public:
void CreateProductA()
{
ProductA1 product;
product.InitA();
productA = product;
}
void CreateProductB()
{
ProductB1 product;
product.InitB();
productB = product;
}
};
2.4 用户使用
前面提到过,对于用户来说,抽象的产品已经应有尽有,用户无需知道 A1和A2具体要怎么去区分创建,只需要知道有这么两个具体工厂可以创建它们就可以。而且使用时就算实例的是AbstractFactory引用类型的具体工厂也是可以的。
代码如下:(具体工厂2的使用类似,不再赘述)
AbstractFactory* factory = new ConcreteFactory1(); //实例工厂
factory->CreateProductA(); //创建产品
factory->CreateProductB();
AbstractProductA productA = factory->GetProdctA(); //获取产品
AbstractProductB productB = factory->GetProdctB();
productA.addA("factory1"); //使用产品的功能函数
productB.addB("factory1");
//可视化看看有没有问题
ListNode* p = productA.GetList();
while (p->next != NULL)
{
cout << p->str << " -> ";
p = p->next;
}
cout << p->str << endl;
p = productB.GetList();
while (p->next != NULL)
{
cout << p->str << " -> ";
p = p->next;
}
cout << p->str << endl;
实例工厂处,选择实例 AbstractFactory 的引用类型,目的是之后还要测试一下 ConcreteFactory2 ,到时直接 factory = new ConcreteFactory2(); 就可以不用再新建一个具体工厂对象了。
2.5 运行结果
运行结果如下:
3. 优缺点
优点:
- 客户与类的实现分离。 从上面是可以看出客户使用的是抽象产品的接口,完全没有与ProductA1有过交互,可以说客户不知道具体的类的名称(不出现在客户代码)。
- 易于实现产品系列切换。 这个很好理解,比如甲方爸爸突然说要换一个数据库,当你没有用抽象工厂模式,你发现客户端的代码全是具体类型数据库接口的调用,一改要改客户代码N多处,头都大了;而当你用了抽象工厂模式,你就可以丝毫不慌,在开始时换一个具体工厂的实例就可以完美切换。
- 有利于产品的一致性。 当一个系统一次只能使用一个系列的产品,那么抽象工厂模式就很容易保证这一点。
缺点:
- 难以扩展新种类。 这个体现在两个方面:
- 为已有产品添加 新的系列:这样的话需要在各个抽象产品类下新建一个子类,然后在抽象工厂下新建一个具体工厂子类,假设目前存在n个产品,那么一下就要新写n+1个类,这个工作量可以接受但也不小(当然新的系列也完全可以是使用现有的具体产品来排列组合,比如上面的例子我把A1和B2捆绑为一个新的系列命名为ConcreteFactory3,此时工作量就不大了)。
- 在产品列表添加 新的产品种类:这个可以说是灾难,首先要为这个对象新建一个抽象产品类,并为每个系列在其下新建一个具体产品类;其次抽象工厂类中要声明这个对象、它的创建方法以及它的获取方法;最后要在各个具体工厂类中实现它的创建方法。假设目前存在n个系列,那么就要新写/修改2n+2个类,工作量巨大还难改全。
由此看来,抽象工厂模式还是更适合一个系列中的产品种类基本不扩展的情况。
但但但是,也是有方法可以定义可扩展的工厂。当然它是有代价的,经典用灵活性换安全性。
4. 可扩展的工厂(简单工厂)
为创建对象的操作添加一个参数,可以是enum的种类,可以是字符串,它为创建对象的操作提供了需要创建的对象的种类。要让这么做有效(创建的对象能够存放在一个可访问的地址,或是直接将创建操作的返回类型设置为对象对应的类型),那么首先所有的对象就需要继承在一个基类下。如果想要用户使用对象具体的功能接口,那么就需要对这个对象做一个向下类型转换。用上面的场景来实现的话,如下代码:
enum PRODUCT_TYPE
{
A1,
A2,
B1,
B2
};
class AbstractProduct
//此类用于让所有抽象产品继承这个类
//即:class AbstractProductA :public AbstractProduct
{
public:
virtual void test() {}
//此函数无实际用处,由于C++的dynamic_cast需要多态的参数,这个类必须存在虚函数才加的
};
class Factory
{
public:
AbstractProduct* CreateProduct(PRODUCT_TYPE PT)
{
switch (PT)
{
case A1:
{
ProductA1 p;
p.InitA();
product = &p;
return product;
break;
}
case A2:
{
ProductA2 p;
p.InitA();
product = &p;
return product;
break;
}
case B1:
{
ProductB1 p;
p.InitB();
product = &p;
return product;
break;
}
case B2:
{
ProductB2 p;
p.InitB();
product = &p;
return product;
break;
}
default:
return product;
break;
}
}
private:
AbstractProduct* product;
};
使用的代码如下:
Factory factory;
AbstractProduct* pro = factory.CreateProduct(A1);
AbstractProductA productA = *dynamic_cast<AbstractProductA*>(pro);
productA.addA("factory");
ListNode* p = productA.GetList();
while (p->next != NULL)
{
cout << p->str << " -> ";
p = p->next;
}
cout << p->str << endl;
运行结果也不无不同:
上面的代码表示,创建A和B,由 PRODUCT_TYPE 指定,用户想要哪个提供参数就行,而使用过程中,需要用到C++的动态强制转换 dynamic_cast ,但是这个动态强制转换并不能总是生效,向下对 AbstractProduct 转换成 AbstractProductA 可能会失败,不能保证安全性。
除了不安全之外,同时产品的名字也或多或少地展现给了用户(当然你也可能自定义字符串与类名做区分)。最重要的一点是,它把所有产品都糅合在一个工厂里,而抽象工厂模式将对象以系列区分,一个应用在使用时不能创建其他系列的对象。