本文用来记录工厂模式中,简单工厂、工厂方法和抽象工厂。
一、前置知识
上图中,每个功能可以看成一个模块,每个模块要想能正常、更好的使用,一定也会依赖其他的模块,所以需要满足:
- 每个模块负责自己的职责(单一职责),各个模块之间通过接口隔离原则对外暴露功能的使用(接口隔离原则)。
- 每个模块都应该”承诺”自己对外暴露的接口是不变的。当模块内部发生变化时,其他模块是不需要知道的。这便是依赖于抽象而不依赖于实现(依赖倒置原则)
- 上层模块只需要知道下层模块暴露出的接口即可,至于实现细节不需要也不应该知道。(迪米特法则)
为了对下面的实例代码进行演示,先明确两个概念:
- 产品:对应着类。
- 抽象产品:抽象类或接口
需求:
设计一个食物的项目,便于食物种类的扩展,且便于维护。
- 食物存在各种各样的种类
- 客户端可以对其进行扩展自己所需要的食物。
二、简单工厂
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(); }
class Hamburger implements Food {
@Override public void eat() { System.out.println("吃汉堡包..."); } }
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对应米线。
- 如果具体产品特别多,则简单工厂,就会变得十分臃肿。比如有100个具体产品,则需要在简单工厂的swich中写出100个case!。
- 最重要的是,变化来了:客户端需要扩展具体产品的时候,势必要修改简单工厂中的代码(修改工厂中的映射关系),这样便违反了”开闭原则”。
三、工厂方法
根据简单工厂中所提出的确定,接下来采用工厂方法。
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); } }
|
上面代码,工厂方法的思路:
每个抽象产品对应一个工厂接口,每个具体产品都有其对应的具体工厂的实现类对其进行返回。总的思路,工厂用于生产用户所需的产品,每个具体工厂实现抽象工厂的接口,即每个产品对应一个工厂实现,由这个具体的工厂实现返回所需的产品。
优点:
- 仍然具有简单工厂的优点,服务器端修改了具体产品的类名以后,客户端不知道
- 当客户端需要扩展一个新的产品时,不需要修改作者原来的代码,只是扩展一个新的工厂而已。
吐槽点:
- 我们已经知道,简单工厂也好,工厂方法也好,都有一个优点,就是服务器端的具体产品类名变换了以后,客户端不知道,但是,反观我们现在的代码,客户端依然依赖于具体的工厂的类名啊!此时,如果服务器端修改了具体工厂的类名,那么客户端也要随之一起修改。
解释:
工厂的名字,是视为接口的。作者有责任、义务,保证工厂的名字是稳定的。也就是说,虽然客户端依赖于工厂的具体类名,可是IT业内,所有工厂的名字都是趋向于稳定的(并不是说100%不会变的)。至少工厂类的名字,要比具体产品类的名字更加稳定!
- 既然产品是我们自己客户端扩展出来的,那为什么不直接自己实例化呢?毕竟这个扩展出来的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 LpFactory(); Food food = ff.getFood(); food.eat(); Bussiness.taste(ff); } }
|
- 现在制作出LpFatory,是为了能把LpFactory传入给Bussiness.taste方法,所以,必须定义这个LpFactory。那么为什么不从一开始,就让Bussiness.taste方法就直接接受Food参数呢?而不是现在的FoodFactory作为参数呢?
解释:
如果是直接传入食物(具体产品),那么在客户端是直接new出食物的对象传入,那么就又会回到,类名修改,违反迪米特法则,客户端也要跟着一起进行修改。
缺点:
如果有多个产品等级,那么工厂类的数量,就会爆炸式增长。(并且每个产品等级对应一个抽象产品和抽象工厂,一个具体产品又对应一个工厂类)
抽象工厂
针对工厂方法的问题,当有多个产品等级时(食物、饮料、甜品…),工厂类就会有很多。
例如:
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) {
} }
|
这里多了一个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进行抽象成一个工厂。而不是单纯是只生产一个产品的工厂。这里将食物工厂和饮料工厂进行了合并,从而减少了类。即:一个工厂生成多个产品等级,而不是一个产品等级。
优点:
- 仍然有简单工厂和工厂方法的优点
- 更重要的是,抽象工厂把工厂类的数量减少了!无论多少个产品等级,工厂就一套。从下面的抽象工厂的UML图中,可以看出,再多一个产品等级,工厂的数量却不会发生改变,多的就是那个产品等级自身的类,跟工厂数量无关。
吐槽点:
- 为什么三秦工厂时,就必须是米线搭配冰峰呢
解释:
抽象工厂中,可以生产多个产品,这多个产品之间,必须有内在的联系。同一个工厂中的产品都属于同一个产品簇!不能把不同产品簇的产品进行混合使用(该设计模式的特点)。
缺点:
当产品等级发生变化时(增加产品等级、删除产品等级),都要引起所有以前工厂代码的修改,这就违反了”开闭原则”。
在上面的图中,6mm螺丝和8mm螺母就形成了一个产品簇,而螺丝和螺母为各自的产品等级。这里6mmFactory只能生产6mm螺丝和6mm螺母。所以,他们之间的多个产品之间存在着必要的内在联系。
产品簇:多个有内在联系,或者是有逻辑关系的产品,这里Food
和Drink
可以组成一组产品簇。
产品等级:产品簇中的每一类产品,这里Food
是一个产品等级,Drink是一个产品等级。
由下图说明:
产品簇存在内在联系,这里中一个工厂5个get方法,分别返回洗衣机…笔记本。一个工厂有五个实现类,分别是格力、海尔、美的、华为、腾讯5个工厂实现类。
从图上也能看出,产品簇可以无限扩展,符合开闭原则,但是如果增加产品等级,就会去修改源代码,这样违反了开闭原则。也就是说,当产品等级发生变化时,就会引起原来抽象工厂的内部变化,这样就破坏了开闭原则。
结论:
- 当产品等级比较固定时,可以考虑使用抽象工厂,否则不建议使用。
- 解决方法:通过
Spring
,动态工厂加反射。