10.1 辉煌工程 制造悍马

周三,9:00,我刚刚坐到位置上,打开电脑准备开始干活。

“小三,小三,叫一下其他同事,到会议室开会”,老大跑过来吼,带着坏笑。还没等大家坐稳,老大就开讲了:

“告诉大家一个好消息,昨天终于把××模型公司的口子打开了,要我们做悍马模型,虽然是第一个车辆模型,但是我们有能力、有信心做好,我们一定要……”(中间省略20分钟的讲话,如果你听过领导人的讲话,这个你应该能够续上)

动员工作做完了,那就开始压任务了。“这次时间是非常紧张的,只有一个星期的时间,小三,你负责在一个星期的时间把这批10万车模(注:车模是车辆模型的意思,不是香车美女那个车模)建设完成……”

“一个星期?这个……是真做不完,要做分析,做模板,做测试,还要考虑扩展性、稳定性、健壮性等,时间实在是太少了。”还没等老大说完,我就急了,再不急我的小命就折在上面了!

“那这样,只做最基本的实现,不考虑太多的问题,怎么样?”老大又把我弹回去了。

“只作基本实现?那……”

唉,领导已经布置任务了,那就开始拼命地做吧。然后就开始准备动手做,在做之前先介绍一下我们公司的背景,我们公司是做模型生产的,做过桥梁模型、建筑模型、机械模型,甚至是一些政府、军事的机密模型,这个不能细说,绝密。公司的主要业务就是把实物按照一定的比例缩小或放大,用于试验、分析、量化或者是销售,等等,上面提到的××模型公司是专门销售车辆模型的公司,自己没有生产企业,全部是代工。我们公司是第一次从×× 模型公司接单,那我怎么着也要把活干好,可时间有限,任务量又巨大,怎么办?

既然领导都说了,不考虑扩展性,那好办,先按照最一般的经验设计类图,如图10-1所示。

image-20210928150520226

图10-1 悍马车模型最一般的类图

非常简单的实现,悍马车有两个型号,H1和H2。按照需求,只需要悍马模型,那好我就给你悍马模型,先写个抽象类,然后两个不同型号的模型实现类,通过简单的继承就可以实现业务要求。我们先从抽象类开始编写,抽象悍马模型如代码清单10-1所示。

代码清单10-1 抽象悍马模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class HummerModel {
/** 首先,这个模型要能够被发动起来,别管是手摇发动,还是电力发动,反正
* 是要能够发动起来,那这个实现要在实现类里了
*/
public abstract void start();
//能发动,还要能停下来,那才是真本事
public abstract void stop();
//喇叭会出声音,是滴滴叫,还是哔哔叫
public abstract void alarm();
//引擎会轰隆隆地响,不响那是假的
public abstract void engineBoom();
//那模型应该会跑吧,别管是人推的,还是电力驱动的,总之要会跑
public abstract void run();
}

在抽象类中,我们定义了悍马模型都必须具有的特质:能够发动、停止,喇叭会响,引擎可以轰鸣,而且还可以停止。但是每个型号的悍马实现是不同的,H1型号的悍马如代码清单10-2所示。

代码清单10-2 H1型号悍马模型

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
public class HummerH1Model extends HummerModel {
//H1型号的悍马车鸣笛
public void alarm() {
System.out.println("悍马H1鸣笛...");
}
//引擎轰鸣声
public void engineBoom() {
System.out.println("悍马H1引擎声音是这样的...");
}
//汽车发动
public void start() {
System.out.println("悍马H1发动...");
}
//停车
public void stop() {
System.out.println("悍马H1停车...");
}
//开动起来
public void run(){
//先发动汽车
this.start();
//引擎开始轰鸣
this.engineBoom();
//然后就开始跑了,跑的过程中遇到一条狗挡路,就按喇叭
this.alarm();
//到达目的地就停车
this.stop();
}
}

大家注意看run()方法,这是一个汇总的方法,一个模型生产成功了,总要拿给客户检测吧,怎么检测?“是骡子是马,拉出去溜溜”,这就是一种检验方法,让它跑起来!通过run()这样的方法,把模型的所有功能都测试到了。

H2型号悍马如代码清单10-3所示。

代码清单10-3 H2型号悍马模型

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
public class HummerH2Model extends HummerModel {
//H2型号的悍马车鸣笛
public void alarm() {
System.out.println("悍马H2鸣笛...");
}
//引擎轰鸣声
public void engineBoom() {
System.out.println("悍马H2引擎声音是这样在...");
}
//汽车发动
public void start() {
System.out.println("悍马H2发动...");
}
//停车
public void stop() {
System.out.println("悍马H2停车...");
}
//开动起来
public void run(){
//先发动汽车
this.start();
//引擎开始轰鸣
this.engineBoom();
//然后就开始跑了,跑的过程中遇到一条狗挡路,就按喇叭
this.alarm();
//到达目的地就停车
this.stop();
}
}

好了,程序编写到这里,已经发现问题了,两个实现类的run()方法都是完全相同的,那这个run()方法的实现应该出现在抽象类,不应该在实现类上,抽象是所有子类的共性封装。


注意 在软件开发过程中,如果相同的一段代码复制过两次,就需要对设计产生怀疑, 架构师要明确地说明为什么相同的逻辑要出现两次或更多次。


好,问题发现了,我们就需要马上更改,修改后的类图如图10-2所示。

image-20210928151118530

图10-2 修改后的悍马车模类图
注意,抽象类HummerModel中的run()方法,由抽象方法变更为实现方法,其源代码如代码清单10-4所示。

代码清单10-4 修改后的抽象悍马模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public abstract class HummerModel {
/** 首先,这个模型要能发动起来,别管是手摇发动,还是电力发动,反正
* 是要能够发动起来,那这个实现要在实现类里了
*/
public abstract void start();
//能发动,还要能停下来,那才是真本事
public abstract void stop();
//喇叭会出声音,是滴滴叫,还是哔哔叫
public abstract void alarm();
//引擎会轰隆隆地响,不响那是假的
public abstract void engineBoom();
//那模型应该会跑吧,别管是人推的,还是电力驱动,总之要会跑
public void run(){
//先发动汽车
this.start();
//引擎开始轰鸣
this.engineBoom();
//然后就开始跑了,跑的过程中遇到一条狗挡路,就按喇叭
this.alarm();
//到达目的地就停车
this.stop();
}
}

在抽象的悍马模型上已经定义了run()方法的执行规则,先启动,然后引擎立刻轰鸣,中间还要按一下喇叭,制造点噪声(要不就不是名车了)。然后停车,它的两个具体实现类就不需要实现run()方法了,只要把代码清单10-2、代码清单10-3上的run()方法删除即可,不再赘述代码。

场景类实现的任务就是把生产出的模型展现给客户,其源代码如代码清单10-5所示。

代码清单10-5 场景类

1
2
3
4
5
6
7
8
public class Client {
public static void main(String[] args) {
//XX公司要H1型号的悍马
HummerModel h1 = new HummerH1Model();
//H1模型演示
h1.run();
}
}

运行结果如下所示。

1
2
3
4
悍马H1发动... 
悍马H1引擎声音是这样的...
悍马H1鸣笛...
悍马H1停车...

目前客户只要看H1型号的悍马车,没问题,生产出来,同时可以运行起来给他看看。 非常简单,那如果我告诉你这就是模板方法模式你会不会很不屑呢?就这模式,太简单了, 我一直在使用呀!是的,你经常在使用,但你不知道这是模板方法模式,那些所谓的高手就可以很牛地说:“用模板方法模式就可以实现”,你还要很崇拜地看着,哇,牛人,模板方法模式是什么呀?这就是模板方法模式。

9.4 最佳实践

一个模式在什么情况下才能够使用,是很多读者比较困惑的地方。抽象工厂模式是一个简单的模式,使用的场景非常多,大家在软件产品开发过程中,涉及不同操作系统的时候, 都可以考虑使用抽象工厂模式,例如一个应用,需要在三个不同平台(Windows、Linux、 Android(Google发布的智能终端操作系统))上运行,你会怎么设计?分别设计三套不同的应用?非也,通过抽象工厂模式屏蔽掉操作系统对应用的影响。三个不同操作系统上的软件功能、应用逻辑、UI都应该是非常类似的,唯一不同的是调用不同的工厂方法,由不同的产品类去处理与操作系统交互的信息。

9.3 抽象工厂模式的应用

9.3.1 抽象工厂模式的优点

  • 封装性,每个产品的实现类不是高层模块要关心的,它要关心的是什么?是接口,是抽象,它不关心对象是如何创建出来,这由谁负责呢?工厂类,只要知道工厂类是谁,我就能创建出一个需要的对象,省时省力,优秀设计就应该如此。
  • 产品族内的约束为非公开状态。例如生产男女比例的问题上,猜想女娲娘娘肯定有自己的打算,不能让女盛男衰,否则女性的优点不就体现不出来了吗?那在抽象工厂模式,就应该有这样的一个约束:每生产1个女性,就同时生产出1.2个男性,这样的生产过程对调用工厂类的高层模块来说是透明的,它不需要知道这个约束,我就是要一个黄色女性产品就可以了,具体的产品族内的约束是在工厂内实现的。

9.3.2 抽象工厂模式的缺点

抽象工厂模式的最大缺点就是产品族扩展非常困难,为什么这么说呢?我们以通用代码为例,如果要增加一个产品C,也就是说产品家族由原来的2个增加到3个,看看我们的程序有多大改动吧!抽象类AbstractCreator要增加一个方法createProductC(),然后两个实现类都要修改,想想看,这严重违反了开闭原则,而且我们一直说明抽象类和接口是一个契约。改变契约,所有与契约有关系的代码都要修改,那么这段代码叫什么?叫“有毒代码”,——只要与这段代码有关系,就可能产生侵害的危险!

9.3.3 抽象工厂模式的使用场景

抽象工厂模式的使用场景定义非常简单:一个对象族(或是一组没有任何关系的对象) 都有相同的约束,则可以使用抽象工厂模式。什么意思呢?例如一个文本编辑器和一个图片处理器,都是软件实体,但是*nix下的文本编辑器和Windows下的文本编辑器虽然功能和界面都相同,但是代码实现是不同的,图片处理器也有类似情况。也就是具有了共同的约束条件:操作系统类型。于是我们可以使用抽象工厂模式,产生不同操作系统下的编辑器和图片处理器。

9.3.4 抽象工厂模式的注意事项

在抽象工厂模式的缺点中,我们提到抽象工厂模式的产品族扩展比较困难,但是一定要清楚,是产品族扩展困难,而不是产品等级。在该模式下,产品等级是非常容易扩展的,增加一个产品等级,只要增加一个工厂类负责新增加出来的产品生产任务即可。也就是说横向扩展容易,纵向扩展困难。以人类为例子,产品等级中只有男、女两个性别,现实世界还有一种性别:双性人,既是男人也是女人(俗语就是阴阳人),那我们要扩展这个产品等级也是非常容易的,增加三个产品类,分别对应不同的肤色,然后再创建一个工厂类,专门负责不同肤色人的双性人的创建任务,完全通过扩展来实现需求的变更,从这一点上看,抽象工厂模式是符合开闭原则的。

9.2 抽象工厂模式的定义

抽象工厂模式(Abstract Factory Pattern)是一种比较常用的模式,其定义如下:

Provide an interface for creating families of related or dependent objects without specifying their concrete classes.(为创建一组相关或相互依赖的对象提供一个接口,而且无须指定它们的具体类。)

抽象工厂模式的通用类图如图9-3所示。

image-20210928145003473

图9-3 抽象工厂模式的通用类图

抽象工厂模式是工厂方法模式的升级版本,在有多个业务品种、业务分类时,通过抽象工厂模式产生需要的对象是一种非常好的解决方式。我们来看看抽象工厂的通用源代码,首先有两个互相影响的产品线(也叫做产品族),例如制造汽车的左侧门和右侧门,这两个应该是数量相等的——两个对象之间的约束,每个型号的车门都是不一样的,这是产品等级结构约束的,我们先看看两个产品族的类图,如图9-4所示。

image-20210928145156665

图9-4 抽象工厂模式的通用源码类图

注意类图上的圈圈、框框相对应,两个抽象的产品类可以有关系,例如共同继承或实现一个抽象类或接口,其源代码如代码清单9-11所示。

代码清单9-11 抽象产品类

1
2
3
4
5
6
7
public abstract class AbstractProductA {
//每个产品共有的方法
public void shareMethod(){
}
//每个产品相同方法,不同实现
public abstract void doSomething();
}

两个具体的产品实现类如代码清单9-12、代码清单9-13所示。

代码清单9-12 产品A1的实现类

1
2
3
4
5
public class ProductA1 extends AbstractProductA {
public void doSomething() {
System.out.println("产品A1的实现方法");
}
}

代码清单9-13 产品A2的实现类

1
2
3
4
5
public class ProductA2 extends AbstractProductA {
public void doSomething() {
System.out.println("产品A2的实现方法");
}
}

产品B与此类似,不再赘述。抽象工厂类AbstractCreator的职责是定义每个工厂要实现的功能,在通用代码中,抽象工厂类定义了两个产品族的产品创建,如代码清单9-14所示。

代码清单9-14 抽象工厂类

1
2
3
4
5
6
public abstract class AbstractCreator {
//创建A产品家族
public abstract AbstractProductA createProductA();
//创建B产品家族
public abstract AbstractProductB createProductB();
}

注意 有N个产品族,在抽象工厂类中就应该有N个创建方法。


如何创建一个产品,则是由具体的实现类来完成的,Creator1和Creator2如代码清单9-15 和代码清单9-16所示。

代码清单9-15 产品等级1的实现类

1
2
3
4
5
6
7
8
9
10
public class Creator1 extends AbstractCreator {
//只生产产品等级为1的A产品
public AbstractProductA createProductA() {
return new ProductA1();
}
//只生产产品等级为1的B产品
public AbstractProductB createProductB() {
return new ProductB1();
}
}

代码清单9-16 产品等级2的实现类

1
2
3
4
5
6
7
8
9
10
public class Creator2 extends AbstractCreator {
//只生产产品等级为2的A产品
public AbstractProductA createProductA() {
return new ProductA2();
}
//只生产产品等级为2的B产品
public AbstractProductB createProductB() {
return new ProductB2();
}
}

注意 有M个产品等级就应该有M个实现工厂类,在每个实现工厂中,实现不同产品族 的生产任务。


在具体的业务中如何产生一个与实现无关的对象呢?如代码清单9-17所示。

代码清单9-17 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Client {
public static void main(String[] args) {
//定义出两个工厂
AbstractCreator creator1 = new Creator1();
AbstractCreator creator2 = new Creator2();
//产生A1对象
AbstractProductA a1 = creator1.createProductA();
//产生A2对象
AbstractProductA a2 = creator2.createProductA();
//产生B1对象
AbstractProductB b1 = creator1.createProductB();
//产生B2对象
AbstractProductB b2 = creator2.createProductB();
/** 然后在这里就可以为所欲为了... */
}
}

在场景类中,没有任何一个方法与实现类有关系,对于一个产品来说,我们只要知道它的工厂方法就可以直接产生一个产品对象,无须关心它的实现类。

9.1 女娲的失误

第8章讲了女娲造人的故事。人是造出来了,世界也热闹了,可是低头一看,都是清一色的类型,缺少关爱、仇恨、喜怒哀乐等情绪,人类的生命太平淡了,女娲一想,猛然一拍脑袋,忘记给人类定义性别了,那怎么办?抹掉重来,于是人类经过一次大洗礼,所有的人种都消灭掉了,世界又是空无一物,寂静而又寂寞。

由于女娲之前的准备工作花费了非常大的精力,比如准备黄土、八卦炉等,从头开始建立所有的事物也是不可能的,那就想在现有的条件下重新造人,尽可能旧物利用嘛。人种 (Product产品类)应该怎么改造呢?怎么才能让人类有爱有恨呢?是神仙当然有办法了,定义互斥的性别,然后在每个个体中埋下一颗种子:异性相吸,成熟后就一定会去找个异性 (这就是我们说的爱情原动力)。从设计角度来看,一个具体的对象通过两个坐标就可以确定:肤色和性别,如图9-1所示。

image-20210928112307092

图9-1 肤色性别坐标图

产品类分析完毕了,生产的工厂类(八卦炉)该怎么改造呢?只有一个生产设备,要么生产出来的全都是男性,要么都是女性。那不行呀,这么翻天覆地的改造就是为了产生不同性别的人类。有办法了!把目前已经有的生产设备——八卦炉拆开,于是女娲就使用了“八卦复制术”,把原先的八卦炉一个变两个,并且略加修改,就成了女性八卦炉(只生产女性人种)和男性八卦炉(只生产男性人种),于是乎女娲就开始准备生产了,其类图如图9-2 所示。

这个类图虽然大,但是比较简单。Java的典型类图,一个接口,多个抽象类,然后是N 个实现类,每个人种都是一个抽象类,性别是在各个实现类中实现的。特别需要说明的是HumanFactory接口,在这个接口中定义了三个方法,分别用来生产三个不同肤色的人种,也就是我们在图9-1中的Y坐标,它的两个实现类分别是性别,也就是图9-1中的X坐标,通过X 坐标(性别)和Y坐标(肤色)唯一确定了一个生产出来的对象。我们来看看相关的实现,Human接口如代码清单9-1所示。

image-20210928112559760

图9-2 女娲重新生产人类

代码清单9-1 人种接口

1
2
3
4
5
6
7
8
public interface Human {
//每个人种都有相应的颜色
public void getColor();
//人类会说话
public void talk();
//每个人都有性别
public void getSex();
}

人种有三个抽象类,负责人种的抽象属性定义:肤色和语言。白色人种、黑色人种、黄色人种分别如代码清单9-2、代码清单9-3、代码清单9-4所示。

代码清单9-2 白色人种

1
2
3
4
5
6
7
8
9
10
11
public abstract class AbstractWhiteHuman implements Human {

//白色人种的皮肤颜色是白色的
public void getColor(){
System.out.println("白色人种的皮肤颜色是白色的!");
}
//白色人种讲话
public void talk() {
System.out.println("白色人种会说话,一般说的都是单字节。");
}
}

代码清单9-3 黑色人种

1
2
3
4
5
6
7
8
public abstract class AbstractBlackHuman implements Human {
public void getColor(){
System.out.println("黑色人种的皮肤颜色是黑色的!");
}
public void talk() {
System.out.println("黑人会说话,一般人听不懂。");
}
}

代码清单9-4 黄色人种

1
2
3
4
5
6
7
8
public abstract class AbstractYellowHuman implements Human {
public void getColor(){
System.out.println("黄色人种的皮肤颜色是黄色的!");
}
public void talk() {
System.out.println("黄色人种会说话,一般说的都是双字节。");
}
}

每个抽象类都有两个实现类,分别实现公共的最细节、最具体的事物:肤色和语言。具体的实现类实现肤色、性别定义,以黄色女性人种为例,如代码清单9-5所示。

代码清单9-5 黄色女性人种

1
2
3
4
5
6
public class FemaleYellowHuman extends AbstractYellowHuman {
//黄人女性
public void getSex() {
System.out.println("黄人女性");
}
}

黄色男性人种如代码清单9-6所示。

代码清单9-6 黄色男性人种

1
2
3
4
5
6
public class MaleYellowHuman extends AbstractYellowHuman {
//黄人男性
public void getSex() {
System.out.println("黄人男性");
}
}

其他的黑色人种、白色人种的男性和女性的代码与此类似,不再重复编写。到此为止, 我们已经把真实世界的人种都定义出来了,剩下的工作就是怎么制造人类。接口HumanFactory如代码清单9-7所示。

代码清单9-7 八卦炉定义

1
2
3
4
5
6
7
8
public interface HumanFactory {
//制造一个黄色人种
public Human createYellowHuman();
//制造一个白色人种
public Human createWhiteHuman();
//制造一个黑色人种
public Human createBlackHuman();
}

在接口中,我们看到八卦炉是可以生产出不同肤色人种的(当然了,女娲的失误嘛), 那它有多少个八卦炉呢?两个,分别生产女性和男性,女性和男性八卦炉分别如代码清单9-8和代码清单9-9所示。

代码清单9-8 生产女性的八卦炉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class FemaleFactory implements HumanFactory {
//生产出黑人女性
public Human createBlackHuman() {
return new FemaleBlackHuman();
}
//生产出白人女性
public Human createWhiteHuman() {
return new FemaleWhiteHuman();
}
//生产出黄人女性
public Human createYellowHuman() {
return new FemaleYellowHuman();
}
}

代码清单9-9 生产男性的八卦炉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MaleFactory implements HumanFactory {
//生产出黑人男性
public Human createBlackHuman() {
return new MaleBlackHuman();
}
//生产出白人男性
public Human createWhiteHuman() {
return new MaleWhiteHuman();
}
//生产出黄人男性
public Human createYellowHuman() {
return new MaleYellowHuman();
}
}

人种有了,八卦炉也有了,我们就来重现一下当年女娲造人的光景,如代码清单9-10所示。

代码清单9-10 女娲重造人类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class NvWa {
public static void main(String[] args) {
//第一条生产线,男性生产线
HumanFactory maleHumanFactory = new MaleFactory();
//第二条生产线,女性生产线
HumanFactory femaleHumanFactory = new FemaleFactory();
//生产线建立完毕,开始生产人了:
Human maleYellowHuman = maleHumanFactory.createYellowHuman();
Human femaleYellowHuman = femaleHumanFactory.createYellowHuman();
System.out.println("---生产一个黄色女性---");
femaleYellowHuman.getColor();
femaleYellowHuman.talk();
femaleYellowHuman.getSex();
System.out.println("\n---生产一个黄色男性---");
maleYellowHuman.getColor();
maleYellowHuman.talk();
maleYellowHuman.getSex();
/** ......
* 后面继续创建
*/
}
}

运行结果如下所示:

1
2
3
4
5
6
7
8
---生产一个黄色女性--- 
黄色人种的皮肤颜色是黄色的!
黄色人种会说话,一般说的都是双字节。
黄人女性
---生产一个黄色男性---
黄色人种的皮肤颜色是黄色的!
黄色人种会说话,一般说的都是双字节。
黄人男性

各种肤色的男性、女性都制造出来了,两性之间产生了相互吸引力,于是情感产生,这个世界就多了一种小说的题材“爱情”。回头来想想我们的设计,不知道大家有没有去过工厂,每个工厂分很多车间,每个车间又分多条生产线,分别生产不同的产品,我们可以把八卦炉比喻为车间,把八卦炉生产的工艺(生产白人、黑人还是黄人)称为生产线,如此来看就是一个女性生产车间,专门生产各种肤色的女性,一个是男性生产车间,专门生产各种肤色男性,生产完毕就可以在系统外组装,什么是组装?嘿嘿,自己思考!在这样的设计下, 各个车间和各条生产线的职责非常明确,在车间内各个生产出来的产品可以有耦合关系,你要知道世界上黑、黄、白人种的比例是:1∶4∶6,那这就需要女娲娘娘在烧制的时候就要做好比例分配,在一个车间内协调好。这就是抽象工厂模式。

8.5 最佳实践

工厂方法模式在项目中使用得非常频繁,以至于很多代码中都包含工厂方法模式。该模式几乎尽人皆知,但不是每个人都能用得好。熟能生巧,熟练掌握该模式,多思考工厂方法如何应用,而且工厂方法模式还可以与其他模式混合使用(例如模板方法模式、单例模式、 原型模式等),变化出无穷的优秀设计,这也正是软件设计和开发的乐趣所在。

8.4 工厂方法模式的扩展

工厂方法模式有很多扩展,而且与其他模式结合使用威力更大,下面将介绍4种扩展。

1. 缩小为简单工厂模式

我们这样考虑一个问题:一个模块仅需要一个工厂类,没有必要把它产生出来,使用静态的方法就可以了,根据这一要求,我们把上例中的AbstarctHumanFactory修改一下,类图如图8-3所示。

image-20210928110032112

图8-3 简单工厂模式类图

我们在类图中去掉了AbstractHumanFactory抽象类,同时把createHuman方法设置为静态类型,简化了类的创建过程,变更的源码仅仅是HumanFactory和NvWa类,HumanFactory如代码清单8-13所示。

代码清单8-13 简单工厂模式中的工厂类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class HumanFactory {
public static <T extends Human> T createHuman(Class<T> c) {
//定义一个生产出的人种
Human human = null;
try {
//产生一个人种
human = (Human) Class.forName(c.getName()).newInstance();
} catch (Exception e) {
System.out.println("人种生成错误!");
}
return (T) human;
}
}

HumanFactory类仅有两个地方发生变化:去掉继承抽象类,并在createHuman前增加static 关键字;工厂类发生变化,也同时引起了调用者NvWa的变化,如代码清单8-14示。

代码清单8-14 简单工厂模式中的场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class NvWa {
public static void main(String[] args) {
//女娲第一次造人,火候不足,于是白色人种产生了
System.out.println("--造出的第一批人是白色人种--");
Human whiteHuman = HumanFactory.createHuman(WhiteHuman.class);
whiteHuman.getColor();
whiteHuman.talk();
//女娲第二次造人,火候过足,于是黑色人种产生了
System.out.println("\n--造出的第二批人是黑色人种--");
Human blackHuman = HumanFactory.createHuman(BlackHuman.class);
blackHuman.getColor();
blackHuman.talk();
//第三次造人,火候刚刚好,于是黄色人种产生了
System.out.println("\n--造出的第三批人是黄色人种--");
Human yellowHuman = HumanFactory.createHuman(YellowHuman.class);
yellowHuman.getColor();
yellowHuman.talk();
}
}

运行结果没有发生变化,但是我们的类图变简单了,而且调用者也比较简单,该模式是工厂方法模式的弱化,因为简单,所以称为简单工厂模式(Simple Factory Pattern),也叫做静态工厂模式。在实际项目中,采用该方法的案例还是比较多的,其缺点是工厂类的扩展比较困难,不符合开闭原则,但它仍然是一个非常实用的设计模式。

2. 升级为多个工厂类

当我们在做一个比较复杂的项目时,经常会遇到初始化一个对象很耗费精力的情况,所有的产品类都放到一个工厂方法中进行初始化会使代码结构不清晰。例如,一个产品类有5 个具体实现,每个实现类的初始化(不仅仅是new,初始化包括new一个对象,并对对象设置一定的初始值)方法都不相同,如果写在一个工厂方法中,势必会导致该方法巨大无比, 那该怎么办?

考虑到需要结构清晰,我们就为每个产品定义一个创造者,然后由调用者自己去选择与哪个工厂方法关联。我们还是以女娲造人为例,每个人种都有一个固定的八卦炉,分别造出黑色人种、白色人种、黄色人种,修改后的类图如图8-4所示。

image-20210928110440380

图8-4 多个工厂类的类图

每个人种(具体的产品类)都对应了一个创建者,每个创建者都独立负责创建对应的产品对象,非常符合单一职责原则,按照这种模式我们来看看代码变化。

多工厂模式的抽象工厂类如代码清单8-15所示。

代码清单8-15 多工厂模式的抽象工厂类

1
2
3
public abstract class AbstractHumanFactory {
public abstract Human createHuman();
}

注意 抽象方法中已经不再需要传递相关参数了,因为每一个具体的工厂都已经非常明确自己的职责:创建自己负责的产品类对象


黑色人种的创建工厂如代码清单8-16所示。

代码清单8-16 黑色人种的创建工厂实现

1
2
3
4
5
public class BlackHumanFactory extends AbstractHumanFactory {
public Human createHuman() {
return new BlackHuman();
}
}

黄色人种的创建工厂如代码清单8-17所示。

代码清单8-17 黄色人种的创建类

1
2
3
4
5
public class YellowHumanFactory extends AbstractHumanFactory {
public Human createHuman() {
return new YellowHuman();
}
}

白色人种的创建工厂如代码清单8-18所示。

代码清单8-18 白色人种的创建类

1
2
3
4
5
public class WhiteHumanFactory extends AbstractHumanFactory {
public Human createHuman() {
return new WhiteHuman();
}
}

三个具体的创建工厂都非常简单,但是,如果一个系统比较复杂时工厂类也会相应地变复杂。场景类NvWa修改后的代码如代码清单8-19所示。

代码清单8-19 场景类NvWa

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class NvWa {
public static void main(String[] args) {
//女娲第一次造人,火候不足,于是白色人种产生了
System.out.println("--造出的第一批人是白色人种--");
Human whiteHuman = (new WhiteHumanFactory()).createHuman();
whiteHuman.getColor();
whiteHuman.talk();
//女娲第二次造人,火候过足,于是黑色人种产生了
System.out.println("\n--造出的第二批人是黑色人种--");
Human blackHuman = (new BlackHumanFactory()).createHuman();
blackHuman.getColor();
blackHuman.talk();
//第三次造人,火候刚刚好,于是黄色人种产生了
System.out.println("\n--造出的第三批人是黄色人种--");
Human yellowHuman = (new YellowHumanFactory()).createHuman();
yellowHuman.getColor();
yellowHuman.talk();
}
}

运行结果还是相同。我们回顾一下,每一个产品类都对应了一个创建类,好处就是创建类的职责清晰,而且结构简单,但是给可扩展性和可维护性带来了一定的影响。为什么这么说呢?如果要扩展一个产品类,就需要建立一个相应的工厂类,这样就增加了扩展的难度。 因为工厂类和产品类的数量相同,维护时需要考虑两个对象之间的关系。

当然,在复杂的应用中一般采用多工厂的方法,然后再增加一个协调类,避免调用者与各个子工厂交流,协调类的作用是封装子工厂类,对高层模块提供统一的访问接口。

3. 替代单例模式

第7章讲述了单例模式以及扩展出的多例模式,并且指出了单例和多例的一些缺点,我们是不是可以采用工厂方法模式实现单例模式的功能呢?单例模式的核心要求就是在内存中只有一个对象,通过工厂方法模式也可以只在内存中生产一个对象,类图如图8-5所示。

image-20210928111021371

图8-5 工厂方法模式替代单例模式类图

非常简单的类图,Singleton定义了一个private的无参构造函数,目的是不允许通过new的方式创建一个对象,如代码清单8-20所示。

代码清单8-20 单例类

1
2
3
4
5
6
7
8
public class Singleton {
//不允许通过new产生一个对象
private Singleton(){
}
public void doSomething(){
//业务处理
}
}

Singleton保证不能通过正常的渠道建立一个对象,那SingletonFactory如何建立一个单例对象呢?答案是通过反射方式创建,如代码清单8-21所示。

代码清单8-21 负责生成单例的工厂类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SingletonFactory {
private static Singleton singleton;
static{
try {
Class cl= Class.forName(Singleton.class.getName());
//获得无参构造
Constructor constructor=cl.getDeclaredConstructor();
//设置无参构造是可访问的
constructor.setAccessible(true);
//产生一个实例对象
singleton = (Singleton)constructor.newInstance();
}
catch (Exception e) {
//异常处理
}
}
public static Singleton getSingleton(){
return singleton;
}
}

通过获得类构造器,然后设置访问权限,生成一个对象,然后提供外部访问,保证内存中的对象唯一。当然,其他类也可以通过反射的方式建立一个单例对象,确实如此,但是一个项目或团队是有章程和规范的,何况已经提供了一个获得单例对象的方法,为什么还要重新创建一个新对象呢?除非是有人作恶。

以上通过工厂方法模式创建了一个单例对象,该框架可以继续扩展,在一个项目中可以产生一个单例构造器,所有需要产生单例的类都遵循一定的规则(构造方法是private),然后通过扩展该框架,只要输入一个类型就可以获得唯一的一个实例。

4. 延迟初始化

何为延迟初始化(Lazy initialization)?一个对象被消费完毕后,并不立刻释放,工厂类保持其初始状态,等待再次被使用。延迟初始化是工厂方法模式的一个扩展应用,其通用类图如图8-6所示。

image-20210928111629509

图8-6 延迟初始化的通用类图

ProductFactory负责产品类对象的创建工作,并且通过prMap变量产生一个缓存,对需要再次被重用的对象保留,Product和ConcreteProduct是一个示例代码,请参考代码清单8-8和代码清单8-9。ProductFactory如代码清单8-22所示。

代码清单8-22 延迟加载的工厂类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ProductFactory {
private static final Map<String,Product> prMap = new HashMap();
public static synchronized Product createProduct(String type) throws Exception{
Product product =null;
//如果Map中已经有这个对象
if(prMap.containsKey(type)){
product = prMap.get(type);
}
else{
if(type.equals("Product1")){
product = new ConcreteProduct1();
}
else{
product = new ConcreteProduct2();

}
//同时把对象放到缓存容器中
prMap.put(type,product);
}
return product;
}
}

代码还比较简单,通过定义一个Map容器,容纳所有产生的对象,如果在Map容器中已经有的对象,则直接取出返回;如果没有,则根据需要的类型产生一个对象并放入到Map容器中,以方便下次调用。

延迟加载框架是可以扩展的,例如限制某一个产品类的最大实例化数量,可以通过判断Map中已有的对象数量来实现,这样的处理是非常有意义的,例如JDBC连接数据库,都会要求设置一个MaxConnections最大连接数量,该数量就是内存中最大实例化的数量。

延迟加载还可以用在对象初始化比较复杂的情况下,例如硬件访问,涉及多方面的交互,则可以通过延迟加载降低对象的产生和销毁带来的复杂性。

8.3 工厂方法模式的应用

8.3.1 工厂方法模式的优点

首先,良好的封装性,代码结构清晰。一个对象创建是有条件约束的,如一个调用者需要一个具体的产品对象,只要知道这个产品的类名(或约束字符串)就可以了,不用知道创建对象的艰辛过程,降低模块间的耦合。

其次,工厂方法模式的扩展性非常优秀。在增加产品类的情况下,只要适当地修改具体的工厂类或扩展一个工厂类,就可以完成“拥抱变化”。例如在我们的例子中,需要增加一个棕色人种,则只需要增加一个BrownHuman类,工厂类不用任何修改就可完成系统扩展。

再次,屏蔽产品类。这一特点非常重要,产品类的实现如何变化,调用者都不需要关心,它只需要关心产品的接口,只要接口保持不变,系统中的上层模块就不要发生变化。因为产品类的实例化工作是由工厂类负责的,一个产品对象具体由哪一个产品生成是由工厂类决定的。在数据库开发中,大家应该能够深刻体会到工厂方法模式的好处:如果使用JDBC 连接数据库,数据库从MySQL切换到Oracle,需要改动的地方就是切换一下驱动名称(前提条件是SQL语句是标准语句),其他的都不需要修改,这是工厂方法模式灵活性的一个直接案例。

最后,工厂方法模式是典型的解耦框架。高层模块值需要知道产品的抽象类,其他的实现类都不用关心,符合迪米特法则,我不需要的就不要去交流;也符合依赖倒置原则,只依赖产品类的抽象;当然也符合里氏替换原则,使用产品子类替换产品父类,没问题!

8.3.2 工厂方法模式的使用场景

首先,工厂方法模式是new一个对象的替代品,所以在所有需要生成对象的地方都可以使用,但是需要慎重地考虑是否要增加一个工厂类进行管理,增加代码的复杂度。

其次,需要灵活的、可扩展的框架时,可以考虑采用工厂方法模式。万物皆对象,那万物也就皆产品类,例如需要设计一个连接邮件服务器的框架,有三种网络协议可供选择: POP3、IMAP、HTTP,我们就可以把这三种连接方法作为产品类,定义一个接口如IConnectMail,然后定义对邮件的操作方法,用不同的方法实现三个具体的产品类(也就是连接方式)再定义一个工厂方法,按照不同的传入条件,选择不同的连接方式。如此设计, 可以做到完美的扩展,如某些邮件服务器提供了WebService接口,很好,我们只要增加一个产品类就可以了。

再次,工厂方法模式可以用在异构项目中,例如通过WebService与一个非Java的项目交互,虽然WebService号称是可以做到异构系统的同构化,但是在实际的开发中,还是会碰到很多问题,如类型问题、WSDL文件的支持问题,等等。从WSDL中产生的对象都认为是一个产品,然后由一个具体的工厂类进行管理,减少与外围系统的耦合。

最后,可以使用在测试驱动开发的框架下。例如,测试一个类A,就需要把与类A有关联关系的类B也同时产生出来,我们可以使用工厂方法模式把类B虚拟出来,避免类A与类B 的耦合。目前由于JMock和EasyMock的诞生,该使用场景已经弱化了,读者可以在遇到此种情况时直接考虑使用JMock或EasyMock。

8.2 工厂方法模式的定义

工厂方法模式使用的频率非常高,在我们日常的开发中总能见到它的身影。其定义为:

Define an interface for creating an object,but let subclasses decide which class to instantiate.Factory Method lets a class defer instantiation to subclasses.(定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。

工厂方法模式的通用类图如图8-2所示。

image-20210928105030601

图8-2 工厂方法模式通用类图

在工厂方法模式中,抽象产品类Product负责定义产品的共性,实现对事物最抽象的定义;Creator为抽象创建类,也就是抽象工厂,具体如何创建产品类是由具体的实现工厂ConcreteCreator完成的。工厂方法模式的变种较多,我们来看一个比较实用的通用源码。

抽象产品类代码如代码清单8-8所示。

代码清单8-8 抽象产品类

1
2
3
4
5
6
7
8
public abstract class Product {
//产品类的公共方法
public void method1(){
//业务逻辑处理
}
//抽象方法
public abstract void method2();
}

具体的产品类可以有多个,都继承于抽象产品类,其源代码如代码清单8-9所示。

代码清单8-9 具体产品类

1
2
3
4
5
6
7
8
9
10
public class ConcreteProduct1 extends Product {
public void method2() {
//业务逻辑处理
}
}
public class ConcreteProduct2 extends Product {
public void method2() {
//业务逻辑处理
}
}

抽象工厂类负责定义产品对象的产生,源代码如代码清单8-10所示。

代码清单8-10 抽象工厂类

1
2
3
4
5
6
7
public abstract class Creator {
/**
* 创建一个产品对象,其输入参数类型可以自行设置
* 通常为String、Enum、Class等,当然也可以为空
*/
public abstract <T extends Product> T createProduct(Class<T> c);
}

具体如何产生一个产品的对象,是由具体的工厂类实现的,如代码清单8-11所示。

代码清单8-11 具体工厂类

1
2
3
4
5
6
7
8
9
10
11
12
public class ConcreteCreator extends Creator {
public <T extends Product> T createProduct(Class<T> c){
Product product=null;
try {
product = (Product)Class.forName(c.getName()).newInstance();
}
catch (Exception e) {
//异常处理
}
return (T)product;
}
}

场景类的调用方法如代码清单8-12所示。

代码清单8-12 场景类

1
2
3
4
5
6
7
8
9
public class Client {
public static void main(String[] args) {
Creator creator = new ConcreteCreator();
Product product = creator.createProduct(ConcreteProduct1.class);
/*
* 继续业务处理
*/
}
}

该通用代码是一个比较实用、易扩展的框架,读者可以根据实际项目需要进行扩展。

8.1 女娲造人的故事

东汉《风俗通》记录了一则神话故事:“开天辟地,未有人民,女娲搏黄土做人”,讲述的内容就是大家非常熟悉的女娲造人的故事。开天辟地之初,大地上并没有生物,只有苍茫大地,纯粹而洁净的自然环境,寂静而又寂寞,于是女娲决定创造一个新物种(即人类)来增加世界的繁荣,怎么制造呢?

别忘了女娲是神仙,没有办不到的事情,造人的过程是这样的:首先,女娲采集黄土捏成人的形状,然后放到八卦炉中烧制,最后放置到大地上生长,工艺过程是没有错的,但是意外随时都会发生:

第一次烤泥人,感觉应该熟了,往大地上一放,哇,没烤熟!于是一个白人诞生了! (这也是缺乏经验的最好证明。)

第二次烤泥人,上一次没烤熟,这次多烤一会儿,放到世间一看,嘿,熟过头了,于是黑人诞生了!

第三次烤泥人,一边烧制一边察看,直到表皮微黄,嘿,刚刚好,于是黄色人种出现了!

这个造人过程是比较有意思的,是不是可以通过软件开发来实现这个过程呢?古人云:“三人行,必有我师焉”,在面向对象的思维中,万物皆对象,是对象我们就可以通过软件设计来实现。首先对造人过程进行分析,该过程涉及三个对象:女娲、八卦炉、三种不同肤色的人。女娲可以使用场景类Client来表示,八卦炉类似于一个工厂,负责制造生产产品 (即人类),三种不同肤色的人,他们都是同一个接口下的不同实现类,都是人嘛,只是肤色、语言不同,对于八卦炉来说都是它生产出的产品。分析完毕,我们就可以画出如图8-1所示的类图。

image-20210928104233083

图8-1 女娲造人类图
类图比较简单,AbstractHumanFactory是一个抽象类,定义了一个八卦炉具有的整体功能,HumanFactory为实现类,完成具体的任务——创建人类;Human接口是人类的总称,其三个实现类分别为三类人种;NvWa类是一个场景类,负责模拟这个场景,执行相关的任务。

我们定义的每个人种都有两个方法:getColor(获得人的皮肤颜色)和talk(交谈),其源代码如代码清单8-1所示。

代码清单8-1 人类总称

1
2
3
4
5
6
public interface Human {
//每个人种的皮肤都有相应的颜色
public void getColor();
//人类会说话
public void talk();
}

接口Human是对人类的总称,每个人种都至少具有两个方法,黑色人种、黄色人种、白色人种的代码分别如代码清单8-2、代码清单8-3、代码清单8-4所示。

代码清单8-2 黑色人种

1
2
3
4
5
6
7
8
public class BlackHuman implements Human {
public void getColor(){
System.out.println("黑色人种的皮肤颜色是黑色的!");
}
public void talk() {
System.out.println("黑人会说话,一般人听不懂。");
}
}

代码清单8-3 黄色人种

1
2
3
4
5
6
7
8
public class YellowHuman implements Human {
public void getColor(){
System.out.println("黄色人种的皮肤颜色是黄色的!");
}
public void talk() {
System.out.println("黄色人种会说话,一般说的都是双字节。");
}
}

代码清单8-4 白色人种

1
2
3
4
5
6
7
8
public class WhiteHuman implements Human {
public void getColor(){
System.out.println("白色人种的皮肤颜色是白色的!");
}
public void talk() {
System.out.println("白色人种会说话,一般都是但是单字节。");
}
}

所有的人种定义完毕,下一步就是定义一个八卦炉,然后烧制人类。我们想象一下,女娲最可能给八卦炉下达什么样的生产命令呢?应该是“给我生产出一个黄色人种 (YellowHuman类)”,而不会是“给我生产一个会走、会跑、会说话、皮肤是黄色的人种”, 因为这样的命令增加了交流的成本,作为一个生产的管理者,只要知道生产什么就可以了, 而不需要事物的具体信息。通过分析,我们发现八卦炉生产人类的方法输入参数类型应该是Human接口的实现类,这也解释了为什么类图上的AbstractHumanFactory抽象类中createHuman 方法的参数为Class类型。其源代码如代码清单8-5所示。

代码清单8-5 抽象人类创建工厂

1
2
3
public abstract class AbstractHumanFactory {
public abstract <T extends Human> T createHuman(Class<T> c);
}

注意,我们在这里采用了泛型(Generic),通过定义泛型对createHuman的输入参数产生两层限制:

  • 必须是Class类型;
  • 必须是Human的实现类。

其中的”T”表示的是,只要实现了Human接口的类都可以作为参数,泛型是JDK 1.5中的一个非常重要的新特性,它减少了对象间的转换,约束其输入参数类型,对Collection集合下的实现类都可以定义泛型。有关泛型的详细知识,请参考相关的Java语法文档。

目前女娲只有一个八卦炉,其实现生产人类的方法,如代码清单8-6所示。

代码清单8-6 人类创建工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class HumanFactory extends AbstractHumanFactory {
public <T extends Human> T createHuman(Class<T> c){
//定义一个生产的人种
Human human=null;
try {
//产生一个人种
human = (T)Class.forName(c.getName()).newInstance();
}
catch (Exception e) {
System.out.println("人种生成错误!");
}
return (T)human;
}
}

人种有了,八卦炉也有了,剩下的工作就是女娲采集黄土,然后命令八卦炉开始生产, 其过程如代码清单8-7所示。

代码清单8-7 女娲类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class NvWa {
public static void main(String[] args) {
//声明阴阳八卦炉
AbstractHumanFactory YinYangLu = new HumanFactory();

//女娲第一次造人,火候不足,于是白人产生了
System.out.println("--造出的第一批人是白色人种--");
Human whiteHuman = YinYangLu.createHuman(WhiteHuman.class);
whiteHuman.getColor();
whiteHuman.talk();

//女娲第二次造人,火候过足,于是黑人产生了
System.out.println("\n--造出的第二批人是黑色人种--");
Human blackHuman = YinYangLu.createHuman(BlackHuman.class);
blackHuman.getColor();
blackHuman.talk();

//第三次造人,火候刚刚好,于是黄色人种产生了
System.out.println("\n--造出的第三批人是黄色人种--");
Human yellowHuman = YinYangLu.createHuman(YellowHuman.class);
yellowHuman.getColor();
yellowHuman.talk();
}
}

人种有了,八卦炉有了,负责生产的女娲也有了,激动人心的时刻到来了,我们运行一下,结果如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
--造出的第一批人是白色人种--
白色人种的皮肤颜色是白色的!
白色人种会说话,一般都是但是单字节。

--造出的第二批人是黑色人种--
黑色人种的皮肤颜色是黑色的!
黑人会说话,一般人听不懂。

--造出的第三批人是黄色人种--
黄色人种的皮肤颜色是黄色的!
黄色人种会说话,一般说的都是双字节。

哇,人类的生产过程就展现出来了!这个世界就热闹起来了,黑人、白人、黄人都开始活动了,这也正是我们现在的真实世界。以上就是工厂方法模式(没错,对该部分有疑问,请继续阅读下去)。