0%

【设计模式】工厂模式

本篇介绍了工厂模式及相关的面向对象设计原则。工厂模式分为工厂方法(Factory Method)和抽象工厂(Abstract Factory)两类:

  • 工厂方法定义了一个用于创建对象的接口,让子类决定实例化哪一个类。 工厂方法使得一个类的实例化延迟到子类。
  • 抽象工厂提供了一个接口,让该接口负责创建一系列相关或者相互依赖的对象,而无需在主程序中指定它们具体的类型。

1 引例

假设有一个披萨店,整个披萨店的工作流程可以表示为下面的代码:

1
2
3
4
5
6
7
8
9
Pizza* orderPizza() {
Pizza* pizza = new Pizza();

pizza->prepare();
pizza->bake();
pizza->cut();
pizza->box();
return pizza;
}

当我们需要更多类型的披萨的时候,就需要加上一些判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Pizza* orderPizza(string& type) {
Pizza* pizza;

if(type == "cheese") {
pizza = new CheesePizza();
}
else if(type == "greek") {
pizza = new GreekPizza();
}
else if(type == "pepperoin"){
pizza = new PepperoinPizza();
}

pizza->prepare();
pizza->bake();
pizza->cut();
pizza->box();
return pizza;
}

当我们之后想要去掉或加上某些类型的披萨时,就需要不断地删改上面的代码,因为我们违背了前面提到过的一些设计原则:应该把代码中需要变化的部分封装起来。

于是我们将创建披萨对象的代码提取出来,放到一个单独的对象中,这个对象称之为“工厂”,因为它只负责生成披萨对象,而orderPizza()方法就不需要再关注披萨对象的具体类型了,它只知道会从工厂拿到一个披萨,然后对披萨进行处理即可。于是工厂类的定义可能像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class PizzaFactory {
public:
Pizza* createPizza(string& type) {
Pizza* pizza;
if(type == "cheese") {
pizza = new CheesePizza();
}
else if(type == "greek") {
pizza = new GreekPizza();
}
else if(type == "pepperoin"){
pizza = new PepperoinPizza();
}
return pizza;
}
};

之后orderPizza()方法就变得非常简洁,并且与具体的披萨类型解耦开来:

1
2
3
4
5
6
7
8
9
10
11
Pizza* orderPizza(string& type) {
PizzaFactory* factory = new PizzaFactory();
Pizza* pizza;
pizza = factory->createPizza(type);

pizza->prepare();
pizza->bake();
pizza->cut();
pizza->box();
return pizza;
}

这是一种常见的代码优化思路,但还称不上是设计模式,因为这更像是一种好的编程习惯,这种方法被称为简单工厂。

当我们要在不同地区开连锁店时,虽然我们希望所有店都使用相同的流程,但是不同地区的披萨风味可能有所不同,因此不同店的披萨类型也有所不同,此时如果使用简单工厂,我们需要为不同地区编写各自的工厂类,然后在orderPizza()方法中判断当前所处哪一个地区,再实例化对应的工厂,生产对应的披萨。这样一来又回到了一开始的问题,因此简单工厂并不足够灵活。

2 工厂方法

解决上述问题的方法是我们可以设计一个披萨店的抽象类PizzaStore,那么orderPizza()自然是该类中的一个方法,同时该类“抽象”的地方在于拥有一个创建披萨的纯虚函数createPizza(string& type),所有不同地区的连锁店都是对该抽象类的一个具体实现,都拥有自己的创建披萨方法,也就是说,这个纯虚函数是一个工厂方法,负责生成不同类型的披萨对象,而所有类的orderPizza()方法都无需关注披萨对象的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class PizzaStore {
public:
Pizza* orderPizza(string& type) {
Pizza* pizza;
pizza = createPizza(type);

pizza->prepare();
pizza->bake();
pizza->cut();
pizza->box();
return pizza;
}

virtual Pizza* createPizza(string& type) = 0;
};
// 纽约的分店
class NewYorkPizzaStore : public PizzaStore {
public:
virtual Pizza* createPizza(string& type){
Pizza* pizza;
if(type == "cheese") {
pizza = new CheesePizza();
}
else if(type == "greek") {
pizza = new GreekPizza();
}
else if(type == "pepperoin"){
pizza = new PepperoinPizza();
}
return pizza;
}
};
// 芝加哥的分店
class ChicagoPizzaStore : public PizzaStore {
public:
virtual Pizza* createPizza(string& type){
Pizza* pizza;
// 创建其它类型的披萨
return pizza;
}
};

上述代码的好处在于,使用了工厂方法负责对象的创建,并且把具体的创建行为封装在子类中,只要顾客决定了到哪家分店吃披萨,就决定了将会创建哪些披萨对象,也就是把具体对象的创建延迟到了子类中去决定,而客户代码中统一调用披萨店的orderPizza()方法即可,这样一来无论是新开分店,还是某个分店改变了披萨类型,都不会对客户代码产生任何影响,这就是解耦。另一个潜在的好处是继承关系使得每一家分店还可以自己定义披萨的制作流程,而无需局限在基类orderPizza()方法所定义的流程框架中。

上面的披萨店是披萨的“创建者”,那么自然还需要有被创建的“产品”披萨,披萨类的设计很简单:

image-20230304141104864

于是工厂方法模式的类图一目了然:

image-20230304141227114

工厂方法模式定义了一个创建对象的接口,但由子类来实现该接口,从而决定实例化的类是哪一个。工厂方法让类把实例化推迟到子类。需要注意的是,这里所说的“让子类决定”并不是指子类能够在运行时做决定,而是指在实际编写创建者类时,不需要知道实际创建的产品是哪一个,选择使用了哪个子类,就决定了创建的产品是哪些。另外,工厂方法模式中,基类不必须是抽象的,我们可以定义一些默认的对象创建行为,这样即使没有具体的子类,基类本身也可以创建对象。

回到一开始,如果不使用工厂方法,而是使用一种最直观的方式来实现,可能的代码会是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class PizzaStore {
public:
Pizza* orderPizza(string& area, string& type) {
Pizza* pizza;
pizza = createPizza(area, type);

pizza->prepare();
pizza->bake();
pizza->cut();
pizza->box();
return pizza;
}

Pizza* createPizza(string& area, string& type) {
Pizza* pizza;
if(area == "NewYork") {
if(type == "cheese") {
pizza = new NYCheesePizza();
}
else if(type == "greek") {
pizza = new NYGreekPizza();
}
else if(type == "pepperoin"){
pizza = new NYPepperoinPizza();
}
}
if(area == "Chicago") {
if(type == "cheese") {
pizza = new CCheesePizza();
}
else if(type == "greek") {
pizza = new CGreekPizza();
}
else if(type == "pepperoin"){
pizza = new CPepperoinPizza();
}
}
return pizza;
}
};

这样实现的披萨店的类图如下:

image-20230304143043787

显然披萨店这一个类,依赖于所有的具体披萨类,当任何一个披萨的具体实现改变了,都会影响到披萨店。这是因为这样的设计使得“高层”的组件披萨店依赖了“低层”的组件披萨,违背了依赖倒置原则

设计原则:依赖倒置原则

不能让高层组依赖低层组件,也就是要让类依赖于抽象,而不是依赖于具体的其他类。

相比之下,上面的工厂方法模式所实现的类图如下:

image-20230304143134405

所有披萨店只依赖于披萨基类这一个类,而披萨基类正是一个抽象类,而所有的具体披萨类也只依赖于披萨基类这一个类,因此满足了依赖倒置原则。

工厂方法模式可以解决单个对象的需求变化,但是要求创建对象的工厂方法必须参数一致。但有些情况下,我们面对的可能是更复杂的情况,当一系列互相依赖或互相作用的多个对象发生变化时,工厂方法就不能很好地解决问题了。

3 抽象工厂

继续以披萨店为例,不同地区可能会制作同一类型的披萨,但是不同地区对同样的披萨使用的原料都不同,不过制作披萨所需要的原料都是同一组,比如都需要一种面团、一种酱料和一种芝士,只是不同地区对这些原料组件都有不同的实现方式。具体来说,在任何地方制作披萨都需要使用一个原料家族,不同地方的家族中,都包含同样的成员,但这些成员的具体实现不同,比如纽约使用一种芝士,而芝加哥使用另一种芝士。

于是我们需要创建一些原料工厂来负责生产不同的原料家族,但它们都需要继承自同一个抽象基类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 原料工厂抽象类
class IngredientFactory {
public:
virtual Dough* createDough() = 0;
virtual Sauce* createSauce() = 0;
virtual Cheese* createCheese() = 0;
...
};
// 纽约的原料工厂
class NewYorkIngredientFactory : public IngredientFactory{
public:
virtual Dough* createDough() {
return new NYDough();
}
virtual Sauce* createSauce() {
return new NYSauce();
}
virtual Cheese* createCheese() {
return new NYCheese();
}
};
// 芝加哥的原料工厂
class ChicagoIngredientFactory : public IngredientFactory{
public:
virtual Dough* createDough() {
return new CDough();
}
virtual Sauce* createSauce() {
return new CSauce();
}
virtual Cheese* createCheese() {
return new CCheese();
}
};

然后就可以在披萨类中加入这些原料:

1
2
3
4
5
6
7
8
9
10
11
class Pizza {
public:
Dough* dough;
Sauce* sauce;
Cheese* cheese;

virtual void prepare() = 0;
void bake() {}
void cut() {}
void box() {}
};

而所有的具体披萨类型都要和原料工厂绑定,从而使用该工厂的原料制作披萨:

1
2
3
4
5
6
7
8
9
10
11
12
class CheesePizza : public Pizza {
public:
IngredientFactory* ingredientFactory;

CheesePizza(IngredientFactory* if) : ingredientFactory(if) {}

virtual void prepare() {
dough = ingredientFactory->createDough();
sauce = ingredientFactory->createSauce();
cheese = ingredientFactory->createCheese();
}
};

然后在披萨店中制作披萨的时候就需要选择一个原料工厂来创建披萨:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 纽约的分店
class NewYorkPizzaStore : public PizzaStore {
public:
virtual Pizza* createPizza(string& type){
Pizza* pizza;
// 使用纽约原料工厂的原料
IngredientFactory* ingredientFactory = new NewYorkIngredientFactory();
if(type == "cheese") {
pizza = new CheesePizza(ingredientFactory);
}
else if(type == "greek") {
pizza = new GreekPizza(ingredientFactory);
}
else if(type == "pepperoin"){
pizza = new PepperoinPizza(ingredientFactory);
}
return pizza;
}
};
// 芝加哥的分店
class ChicagoPizzaStore : public PizzaStore {
public:
virtual Pizza* createPizza(string& type){
Pizza* pizza;
// 使用芝加哥原料工厂的原料
IngredientFactory* ingredientFactory = new ChicagoIngredientFactory();
// 创建其它类型的披萨
return pizza;
}
};

上面的整个过程我们创建了一个抽象的原料工厂,用来创造原料家族,而原料家族就是由一系列相互关联、相互依赖的对象构成的,这些对象都有可能发生变化,但通过抽象工厂,我们把这种变化约束在了具体的工厂当中,而不会影响代码的其他部分。此外,还可以通过替换不同的工厂使得同一类对象产生不同的行为,比如我们可以将披萨类中的原料工厂替换成其他的原料工厂,从而实现通过组合来创建不同的披萨对象的功能。

抽象工厂模式定义了一个接口,用于创建一系列相关或依赖的对象,而不需要明确指定他们的具体类型。抽象工厂允许客户代码使用一个抽象的接口来创建一系列产品,而不需要关注产生的具体产品是什么,这样一来客户和产品就被解耦开来。抽象工厂的类图如下:

image-20230304151212414

在上面的例子中,各种原料就是一系列相关的产品,原料工厂是负责生成这一系列产品的抽象工厂,而披萨店则是原料工厂的客户。在披萨店的代码中我们不需要关注原料具体是哪些,只负责制作披萨就可以了。

4 总结与对比

两种工厂模式的异同总结如下:

  • 工厂方法用于应对“单个对象”的需求变化,而抽象工厂用于应对“一个系列”的需求变化。
  • 工厂方法通过继承来实现,把对象创建委托给子类,子类实现工厂方法来创建对象;而抽象工厂通过对象组合来实现,对象的创建被实现在抽象工厂所声明的方法中。
  • 所有工厂模式都是通过减少客户程序(主程序)和具体类之间的依赖来实现松耦合的。
  • 所用工厂模式都遵循依赖倒置原则,指导我们要避免依赖具体类,而要尽量依赖抽象。
---- 本文结束 知识又增加了亿点点!----

文章版权声明 1、博客名称:LycTechStack
2、博客网址:https://lz328.github.io/LycTechStack.github.io/
3、本博客的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系博主进行删除处理。
4、本博客所有文章版权归博主所有,如需转载请标明出处。