本文用来记录工厂模式中,简单工厂、工厂方法和抽象工厂。

一、前置知识

image-20210710144613164

上图中,每个功能可以看成一个模块,每个模块要想能正常、更好的使用,一定也会依赖其他的模块,所以需要满足:

  • 每个模块负责自己的职责(单一职责),各个模块之间通过接口隔离原则对外暴露功能的使用(接口隔离原则)。
  • 每个模块都应该”承诺”自己对外暴露的接口是不变的。当模块内部发生变化时,其他模块是不需要知道的。这便是依赖于抽象而不依赖于实现(依赖倒置原则)
  • 上层模块只需要知道下层模块暴露出的接口即可,至于实现细节不需要也不应该知道。(迪米特法则)

为了对下面的实例代码进行演示,先明确两个概念:

  1. 产品:对应着类。
  2. 抽象产品:抽象类或接口

需求:

设计一个食物的项目,便于食物种类的扩展,且便于维护。

  1. 食物存在各种各样的种类
  2. 客户端可以对其进行扩展自己所需要的食物。

二、简单工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 抽象产品
interface Food {
void eat();
}

// 具体产品
class Hamburger implements Food {

@Override
public void eat() {
System.out.println("吃汉堡包...");
}
}

// ==================================服务端/客户端==========================================
public class AppTest {
public static void main(String[] args) {
Food f = new Hamburger();
f.eat();
}
}

上面中,存在抽象产品(Food),而具体产品依赖于抽象产品(对其进行实现)。抽象产品和具体产品都是由服务端提供,而客户端就是直接对其使用。

现在考虑如果服务端代码设计成这样会出现什么问题:

上面的代码中,如果服务端作者,修改了具体产品的产品名(Hamburger -> Hamburger2),那么客户端也需要将所有的名称进行修改。

这种设计相当脆弱!因为,作者(服务端)修改了具体产品的类名,那么客户端代码,也要随之一起改变。这样服务器端代码,和客户端代码就是耦合的,这违反了迪米特法则!

我们希望的效果是,无论服务器端代码如何修改,客户端代码都应该不知道,不用修改客户端的代码!

针对上面的问题,服务端一旦修改,客户端代码也要跟着修改!因此,服务单修改代码如下,使用简单工厂模式。

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
42
43
44
45
// 抽象产品
interface Food {
void eat();
}

// 具体产品1
class Hamburger implements Food {

@Override
public void eat() {
System.out.println("吃汉堡包...");
}
}

// 具体产品2
class RiceNoodle implements Food {

@Override
public void eat() {
System.out.println("吃过桥米线...");
}
}

// 工厂
class FoodFactory {
public static Food getFood(int n) {
Food food = null;
switch (n) {
case 1:
food = new Hamburger();
break;
case 2:
food = new RiceNoodle();
}
return food;
}
}

// ==============================服务端/客户端==============================================
public class AppTest {
public static void main(String[] args) {
Food f = FoodFactory.getFood(1);
f.eat();
}
}

采用这种方法,客户端获取具体的产品时,是通过工厂的获得的,即使服务端将产品的名字修改了,而客户端并不用进行修改,这样就达到一个解耦的目的。

简单工厂的优点:

  1. 把具体产品的类型,从客户端代码中,解耦出来。
  2. 服务器端,如果修改了具体产品的类名,客户端不用关心。

这符合了”面向接口编程”的思想,客户端对服务端暴露的接口进行使用(接口是趋于稳定的,不会随便修改名字之类的,前置中提到过每个模块都应该”承诺”自己对外暴露的接口是不变的)。

简单工厂的缺点:

  1. 客户端不得不死记硬背那些常量与具体产品的映射,比如:1对应汉堡包,2对应米线。
  2. 如果具体产品特别多,则简单工厂,就会变得十分臃肿。比如有100个具体产品,则需要在简单工厂的swich中写出100个case!。
  3. 最重要的是,变化来了:客户端需要扩展具体产品的时候,势必要修改简单工厂中的代码(修改工厂中的映射关系),这样便违反了”开闭原则”。

image-20210615163848346

三、工厂方法

根据简单工厂中所提出的确定,接下来采用工厂方法。

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// 抽象产品
interface Food {
void eat();
}

// 具体产品
class Hamburger implements Food {

@Override
public void eat() {
System.out.println("吃汉堡包...");
}
}

class RiceNoodle implements Food {

@Override
public void eat() {
System.out.println("吃过桥米线...");
}
}

interface FoodFactory {
Food getFood();
}

class HamburgerFactory implements FoodFactory {
@Override
public Food getFood() {
return new Hamburger();
}
}

class RiceNoodleFactory implements FoodFactory {
@Override
public Food getFood() {
return new RiceNoodle();
}
}

class Bussiness {
public static void taste(FoodFactory foodFactory) {
Food food = foodFactory.getFood();
System.out.println("评委1,品尝");
food.eat();
System.out.println("评委2,品尝");
food.eat();
System.out.println("评委3,品尝");
food.eat();
}
}


// ==============================服务端/客户端==============================================

// 从这里扩展出了新的功能(食物种类)
class Lp implements Food {

@Override
public void eat() {
System.out.println("吃凉皮...");
}
}

// 新功能对应的工厂 - 通过工厂获得具体产品(该新功能对应的工厂,用于生产具体产品)
class LpFactory implements FoodFactory {

@Override
public Food getFood() {
return new Lp();
}
}

public class AppTest {

public static void main(String[] args) {

// 以下进行了替换,符合开闭原则
FoodFactory ff = new LpFactory();
Food food = ff.getFood();
food.eat();
Bussiness.taste(ff);
}
}

上面代码,工厂方法的思路:

每个抽象产品对应一个工厂接口,每个具体产品都有其对应的具体工厂的实现类对其进行返回。总的思路,工厂用于生产用户所需的产品,每个具体工厂实现抽象工厂的接口,即每个产品对应一个工厂实现,由这个具体的工厂实现返回所需的产品。

优点:

  1. 仍然具有简单工厂的优点,服务器端修改了具体产品的类名以后,客户端不知道
  2. 当客户端需要扩展一个新的产品时,不需要修改作者原来的代码,只是扩展一个新的工厂而已。

吐槽点:

  1. 我们已经知道,简单工厂也好,工厂方法也好,都有一个优点,就是服务器端的具体产品类名变换了以后,客户端不知道,但是,反观我们现在的代码,客户端依然依赖于具体的工厂的类名啊!此时,如果服务器端修改了具体工厂的类名,那么客户端也要随之一起修改。

解释:

工厂的名字,是视为接口的。作者有责任、义务,保证工厂的名字是稳定的。也就是说,虽然客户端依赖于工厂的具体类名,可是IT业内,所有工厂的名字都是趋向于稳定的(并不是说100%不会变的)。至少工厂类的名字,要比具体产品类的名字更加稳定!

  1. 既然产品是我们自己客户端扩展出来的,那为什么不直接自己实例化呢?毕竟这个扩展出来的Lp这个产品,我们自己就是作者。我们想怎么改类名自己都能把控!为什么还要为自己制作的产品做工厂呢?

解释:

因为,作者在开发功能时,不仅仅只会开发一些抽象产品、具体产品、对应的工厂,还会配套搭配一些提前做好的框架。比如:

在服务端有这么一个业务代码:

1
2
3
4
5
6
7
8
9
10
11
class Bussiness {
public static void taste(FoodFactory foodFactory) {
Food food = foodFactory.getFood();
System.out.println("评委1,品尝");
food.eat();
System.out.println("评委2,品尝");
food.eat();
System.out.println("评委3,品尝");
food.eat();
}
}

这里的框架中,只能传入工厂,因此在客户端我们对代码进行扩展时,也要给出对应的工厂对象,才能使用这个功能,同时我们扩展的功能可能也会被作为服务器端拿给别人使用。

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
// === 客户端 扩展出来的功能=== 
// 从这里扩展出了新的功能
class Lp implements Food {

@Override
public void eat() {
System.out.println("吃凉皮...");
}
}

// 新功能对应的工厂 - 通过工厂获得具体产品
class LpFactory implements FoodFactory {

@Override
public Food getFood() {
return new Lp();
}
}

public class AppTest {

public static void main(String[] args) {
// FoodFactory ff = new RiceNoodleFactory();
// Food food = ff.getFood();
// food.eat();

// 以下进行了替换,符合开闭原则
FoodFactory ff = new LpFactory();
Food food = ff.getFood();
food.eat();
Bussiness.taste(ff);
}
}
  1. 现在制作出LpFatory,是为了能把LpFactory传入给Bussiness.taste方法,所以,必须定义这个LpFactory。那么为什么不从一开始,就让Bussiness.taste方法就直接接受Food参数呢?而不是现在的FoodFactory作为参数呢?

解释:

如果是直接传入食物(具体产品),那么在客户端是直接new出食物的对象传入,那么就又会回到,类名修改,违反迪米特法则,客户端也要跟着一起进行修改。

缺点:

如果有多个产品等级,那么工厂类的数量,就会爆炸式增长。(并且每个产品等级对应一个抽象产品和抽象工厂,一个具体产品又对应一个工厂类)

image-20210710152442599

抽象工厂

针对工厂方法的问题,当有多个产品等级时(食物、饮料、甜品…),工厂类就会有很多。

例如:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// 抽象产品 - 食物产品等级
interface Food {
void eat();
}

// 具体产品
class Hamburger implements Food {

@Override
public void eat() {
System.out.println("吃汉堡包...");
}
}

class RiceNoodle implements Food {

@Override
public void eat() {
System.out.println("吃过桥米线...");
}
}

interface FoodFactory {
Food getFood();
}

class HamburgerFactory implements FoodFactory {
@Override
public Food getFood() {
return new Hamburger();
}
}

class RiceNoodleFactory implements FoodFactory {
@Override
public Food getFood() {
return new RiceNoodle();
}
}

// 饮料产品等级
interface Drink {
void drink();
}

class Cola implements Drink {

@Override
public void drink() {
System.out.println("喝可乐...");
}
}

class IcePeak implements Drink {

@Override
public void drink() {
System.out.println("冰峰饮料...");
}
}

interface DrinkFactory {
Drink getDrink();
}


class ColaFactory implements DrinkFactory {
@Override
public Drink getDrink() {
return new Cola();
}
}

public class IcePeakFactory implements DrinkFactory {
@Override
public Drink getDrink() {
return new IcePeak();
}
}


class Bussiness {
public static void taste(FoodFactory foodFactory) {
Food food = foodFactory.getFood();
System.out.println("评委1,品尝");
food.eat();
System.out.println("评委2,品尝");
food.eat();
System.out.println("评委3,品尝");
food.eat();
}
}

// ==============================服务端/客户端==============================================

// 从这里扩展出了新的功能
class Lp implements Food {

@Override
public void eat() {
System.out.println("吃凉皮...");
}
}

// 新功能对应的工厂 - 通过工厂获得具体产品
class LpFactory implements FoodFactory {

@Override
public Food getFood() {
return new Lp();
}
}

public class AppTest {

public static void main(String[] args) {

}
}

image-20210517114346075

这里多了一个Drink的产品等级,那么就需要多出具体产品的工厂实现类。

面对产品簇,使用工厂方法设计模式会造成类爆炸。上面的类图中存在两个产品等级,一个食物产品等级,一个饮料产品等级;其中一个产品等级对应一个工厂,一个工厂又存在多个产品工厂的实现类,用于生产产品。多一个产品等级,就会多出很多的类出来。

采用抽象工厂,将上面特定的工厂,进行抽象。即一个工厂能够生产多个产品等级(既能生产饮料,又能食物)。

代码改进如下:

服务端

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// 抽象产品 - 食物产品等级
interface Food {
void eat();
}

// 具体产品
class Hamburger implements Food {

@Override
public void eat() {
System.out.println("吃汉堡包...");
}
}

class RiceNoodle implements Food {

@Override
public void eat() {
System.out.println("吃过桥米线...");
}
}


interface Factory {
Food getFood();

Drink getDrink();
}

class KFCFactory implements Factory { // 一个抽象工厂的实现类对应一个产品簇

@Override
public Food getFood() {
return new Hamburger();
}

@Override
public Drink getDrink() {
return new Cola();
}
}

// 饮料产品等级
interface Drink {
void drink();
}

// 具体产品
class Cola implements Drink {

@Override
public void drink() {
System.out.println("喝可乐...");
}
}

class IcePeak implements Drink {

@Override
public void drink() {
System.out.println("冰峰饮料...");
}
}

class SanQiFactory implements Factory {

@Override
public Food getFood() {
return new RiceNoodle();
}

@Override
public Drink getDrink() {
return new IcePeak();
}
}

这里将Food和Drink进行抽象成一个工厂。而不是单纯是只生产一个产品的工厂。这里将食物工厂和饮料工厂进行了合并,从而减少了类。即:一个工厂生成多个产品等级,而不是一个产品等级。

优点:

  1. 仍然有简单工厂和工厂方法的优点
  2. 更重要的是,抽象工厂把工厂类的数量减少了!无论多少个产品等级,工厂就一套。从下面的抽象工厂的UML图中,可以看出,再多一个产品等级,工厂的数量却不会发生改变,多的就是那个产品等级自身的类,跟工厂数量无关。

吐槽点:

  1. 为什么三秦工厂时,就必须是米线搭配冰峰呢

解释:

抽象工厂中,可以生产多个产品,这多个产品之间,必须有内在的联系。同一个工厂中的产品都属于同一个产品簇!不能把不同产品簇的产品进行混合使用(该设计模式的特点)。

缺点:

当产品等级发生变化时(增加产品等级、删除产品等级),都要引起所有以前工厂代码的修改,这就违反了”开闭原则”。

image-20210517131940250

在上面的图中,6mm螺丝和8mm螺母就形成了一个产品簇,而螺丝和螺母为各自的产品等级。这里6mmFactory只能生产6mm螺丝和6mm螺母。所以,他们之间的多个产品之间存在着必要的内在联系。

image-20210517114311388

产品簇:多个有内在联系,或者是有逻辑关系的产品,这里FoodDrink可以组成一组产品簇。

产品等级:产品簇中的每一类产品,这里Food是一个产品等级,Drink是一个产品等级。

由下图说明:

image-20210517132412560

产品簇存在内在联系,这里中一个工厂5个get方法,分别返回洗衣机…笔记本。一个工厂有五个实现类,分别是格力、海尔、美的、华为、腾讯5个工厂实现类。

从图上也能看出,产品簇可以无限扩展,符合开闭原则,但是如果增加产品等级,就会去修改源代码,这样违反了开闭原则。也就是说,当产品等级发生变化时,就会引起原来抽象工厂的内部变化,这样就破坏了开闭原则。

结论:

  • 当产品等级比较固定时,可以考虑使用抽象工厂,否则不建议使用。
  • 解决方法:通过Spring,动态工厂加反射。

Comment