9.3.2 简单工厂

9.3.2 简单工厂

依赖关系

对于一个典型的Java应用而言,应用之中各实例之间存在复杂的调用关系(Spring把这种调用关系称为依赖关系,例如A实例调用B实例的方法,则称为A依赖于B)。

硬编码耦合

当A对象需要调用B对象的方法时,许多初学者会选择使用new关键字来创建一个B实例,然后调用B实例的方法。从语法的角度来看,这种做法没有任何问题,这种做法的坏处在于:A类的方法实现直接调用了B类的类名(这种方式也被称为硬编码耦合),一旦系统需要重构:需要使用C类来代替B类时,程序不得不改写A类代码。如果应用中有100个或10000个类以硬编码方式耦合了B类,则需要重新改写100个、10000个地方……这显然是一种非常可怕的事情。

换一个角度来看这个问题:对于A对象而言,它只需要调用B对象的方法,并不是关心B对象的实现、创建过程。考虑让B类实现一个IB接口,而A类只需要IB接口耦合。具体做法是:
定义一个工厂类:IBFactory,IBFactory工厂类来负责创建IB实例;而A类通过调用IBFactory工厂的方法来得到IB的实例。这样A类就不用直接使用new关键字来创建B实例。

通过改用上面设计,则A类不但需要与IBFactory耦合,还需要与IB接口耦合;如果系统需要重构:需要使用C类代替B类,则只需要让C类也实现IB接口,并改写IBFactory工厂中创建IB实例的实现代码,让该工厂产生实现了IB接口的C实例即可。由于所有依赖IB实例的对象都是通过工厂来获取IB实例的,所以它们都将改为获得C实例,这就完成了系统重构。

这种将多个类对象交给工厂类来生成的设计方式被称为简单工厂模式

程序示例

1
2
3
4
5
6
7
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\SimpleFactory
└─src\
├─BetterPrinter.java
├─Computer.java
├─Output.java
├─OutputFactory.java
└─Printer.java

下面以一个简单的场景来介绍简单工厂模式。假设程序中有个Computer对象需要依赖一个输出设备,现在有两个选择:直接让Computer对象依赖一个Printer(实现类)对象,或者让Computer依赖Output(接口)属性。
在这种应用场景下,使用简单工厂模式可以让系统具有更好的可维护性、可扩展性。根据工厂模式,程序应该让Computer依赖一个Output属性,将Computer类与Printer实现类分离开来。 Computer对象只需面向Output接口编程即可。

下面是这个Computer类定义的代码。

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 Computer
{
private Output out;

public Computer(Output out)
{
this.out = out;
}
// 定义一个模拟获取字符串输入的方法
public void keyIn(String msg)
{
out.getData(msg);
}
// 定义一个模拟打印的方法
public void print()
{
out.out();
}
public static void main(String[] args)
{
// 创建OutputFactory
OutputFactory of = new OutputFactory();
// 将Output对象传入,创建Computer对象
Computer c = new Computer(of.getOutput());
c.keyIn("轻量级Java EE企业应用实战");
c.keyIn("疯狂Java讲义");
c.print();
}
}

从上面粗体字代码可以看出,该Computer类已经完全与Output实现类分离了,它只与该接口耦合。而且, Computer不再负责创建Output对象,系统将提供一个Output工厂来负责生成Output对象。这个OutputFactory工厂类代码如下。

1
2
3
4
5
6
7
public class OutputFactory
{
public Output getOutput()
{
return new Printer();
}
}

在该OutputFactory类中包含了一个getOutput()方法,该方法返回一个Output实现类的实例。该方法负责创建Output实例,具体创建哪一个实现类的对象由该方法决定,
Printer类的代码如下:

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
// 让Printer类实现Output
public class Printer implements Output
{
private String[] printData = new String[MAX_CACHE_LINE];
// 用以记录当前需打印的作业数
private int dataNum = 0;
public void out()
{
// 只要还有作业,继续打印
while(dataNum > 0)
{
System.out.println("打印机打印:" + printData[0]);
// 把作业队列整体前移一位,并将剩下的作业数减1
System.arraycopy(printData , 1, printData, 0, --dataNum);
}
}
public void getData(String msg)
{
if (dataNum >= MAX_CACHE_LINE)
{
System.out.println("输出队列已满,添加失败");
}
else
{
// 把打印数据添加到队列里,已保存数据的数量加1。
printData[dataNum++] = msg;
}
}
}

上面的Printer类模拟了一个简单的打印机,如果系统需要重构,需要使用BetterPrinter来代替Printer类,则只需要让BetterPrinter实现Output接口,并改写OutputFactory类的getOutput()方法即可。
下面是BetterPrinter实现类的代码。 BetterPrinter只是对原有的Printer进行简单修改,这里用来模拟系统重构后的改进。

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
public class BetterPrinter implements Output
{
private String[] printData = new String[MAX_CACHE_LINE * 2];
// 用以记录当前需打印的作业数
private int dataNum = 0;
public void out()
{
// 只要还有作业,继续打印
while(dataNum > 0)
{
System.out.println("高速打印机正在打印:" + printData[0]);
// 把作业队列整体前移一位,并将剩下的作业数减1
System.arraycopy(printData , 1, printData, 0, --dataNum);
}
}
public void getData(String msg)
{
if (dataNum >= MAX_CACHE_LINE * 2)
{
System.out.println("输出队列已满,添加失败");
}
else
{
// 把打印数据添加到队列里,已保存数据的数量加1。
printData[dataNum++] = msg;
}
}
}

上面程序中的BetterPrinter类与Printer并无太大区别,仅仅略微改变了Printer实现,且BetterPrinter也实现了Output接口,因此也可当成Output对象使用,因此只要把OutputFactory工厂类的getOutput()方法中改为如下代即可:

1
2
3
4
5
6
7
public class OutputFactory
{
public Output getOutput()
{
return new BetterPrinter();
}
}

再次运行前面的Computer.java程序,发现Computer所依赖的Output对象已改为Better Printer对象,而不再是原来的Printer对象。

通过这种方式,可以把所有生成Output对象的逻辑集中在OutputFactory工厂类中管理,而所有需要使用Output对象的类只需与Output接口耦合,而不是与具体的实现类耦合。即使系统中有很多类依赖了Printer对象,只要OutputFactory类的getOutput()方法返回BetterPrinter对象,则它们全部将会改为依赖BetterPrinter对象,而其他程序无须修改,只需要修改OutputFactory工厂的getOutput()的方法实现即可。

简单工厂模式的优势

使用简单工厂模式的优势是:让对象的调用者和对象创建过程分离,当对象调用者需要对象时,直接向工厂请求即可;从而避免了对象的调用者与对象的实现类以硬编码方式耦合,以提高系统的可维护性、可扩展性。

工厂模式的缺陷

工厂模式也有一个小小的缺陷:当产品修改时,工厂类也要做相应的修改。

Spring容器而言,它首先是一个巨大的工厂,它负责创建所有Bean实例,整个应用的所有组件都由Spring容器负责创建。不仅如此, Spring容器扩展了这种简单工厂模式,它还可以管理Bean实例之间的依赖关系;而且,如果容器中Bean实例具有singleton行为特征,则Spring容器还会缓存该Bean实例,从而保证程序通过Spring工厂来获取该Bean实例时, Spring工厂将会返回同一个Bean实例。

实现简单的IoC容器功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\IoC
├─beans.xml
└─src\
├─lee\
│ ├─BetterPrinter.java
│ ├─Computer.java
│ ├─IoCTest.java
│ ├─Output.java
│ └─Printer.java
└─org\
└─crazyit\
└─ioc\
├─ApplicationContext.java
└─CrazyitXmlApplicationContext.java

下面的示例提供一份类似于Spring配置文件的XML文件,程序提供一个扩展的工厂类,该工厂类也可提供类似于Spring loc容器的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="GBK"?>
<beans>
<bean id="computer" class="lee.Computer">
<!-- 为name注入基本类型的值 -->
<property name="name" value="孙悟空的电脑" />
<!-- 为out注入容器中其他Bean -->
<property name="out" ref="betterPrinter" />
</bean>
<!-- 配置两个Bean实例 -->
<bean id="printer" class="lee.Printer" />
<bean id="betterPrinter" class="lee.BetterPrinter" />
<!-- 配置一个prototype行为的Bean实例 -->
<bean id="now" class="java.util.Date" scope="prototype" /> <!--① -->
</beans>

细心的读者可能已经发现:该配置文件和Spring配置文件如此相似。实际上这个配置文件只是通过简单修改了Spring配置文件得到的。上面的配置文件一样配置了computer Bean,且为该Bean依赖注入了两个属性:nameout除此之外,上面的配置文件中①号代码处还配置了一个prototype行为的Bean实例。
本程序中也提供了一个简化的ApplicationContext接口,该接口仅包含一个getBean()方法。

1
2
3
4
5
6
7
package org.crazyit.ioc;

public interface ApplicationContext
{
// 获取指定Bean实例的方法
Object getBean(String name) throws Exception;
}

本示例将为该接口提供一个简单的实现类,该实现类就是一个功能强大的工厂。它使用Dom4j来解析XML配置文件,并根据配置文件来创建工厂中的Bean实例。下面是该实现类的代码。

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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
package org.crazyit.ioc;

import java.lang.reflect.*;
import java.util.*;
import java.io.*;
import org.dom4j.*;
import org.dom4j.io.*;

public class CrazyitXmlApplicationContext implements ApplicationContext
{
// 保存容器中所有单例模式的Bean实例
private Map<String, Object> objPool = Collections
.synchronizedMap(new HashMap<String, Object>());
// 保存配置文件对应的Document对象
private Document doc;
// 保存配置文件里的根元素
private Element root;
public CrazyitXmlApplicationContext(String filePath) throws Exception
{
SAXReader reader = new SAXReader();
doc = reader.read(new File(filePath));
root = doc.getRootElement();
initPool();
initProp();
}

public Object getBean(String name) throws Exception
{
Object target = objPool.get(name);
// 对于singleton Bean,容器已经初始化了所有Bean实例,直接返回即可
if (target.getClass() != String.class)
{
return target;
} else
{
String clazz = (String) target;
// 对于prototype对象并未注入属性值
return Class.forName(clazz).getConstructor().newInstance();
}
}
// 初始化容器中所有singleton Bean
private void initPool() throws Exception
{
// 遍历配置文件里的每个<bean.../>元素
for (Object obj : root.elements())
{
Element beanEle = (Element) obj;
// 取得<bean.../>元素的id属性
String beanId = beanEle.attributeValue("id");
// 取得<bean.../>元素的class属性
String beanClazz = beanEle.attributeValue("class");
// 取得<bean.../>元素的scope属性
String beanScope = beanEle.attributeValue("scope");
// 如果<bean.../>元素的scope属性不存在,或为singleton
if (beanScope == null || beanScope.equals("singleton"))
{
// 以默认构造器创建Bean实例,并将其放入objPool中
objPool.put(beanId, Class.forName(beanClazz).getConstructor()
.newInstance());
} else
{
// 对于非singlton Bean,存放该Bean实现类的类名。
objPool.put(beanId, beanClazz);
}
}
}
// 初始化容器中singleton Bean的属性
private void initProp() throws Exception
{
// 遍历配置文件里的每个<bean.../>元素
for (Object obj : root.elements())
{
Element beanEle = (Element) obj;
// 取得<bean.../>元素的id属性
String beanId = beanEle.attributeValue("id");
// 取得<bean.../>元素的scope属性
String beanScope = beanEle.attributeValue("scope");
// 如果<bean.../>元素的scope属性不存在,或为singleton
if (beanScope == null || beanScope.equals("singleton"))
{
// 取出objPool的指定的Bean实例
Object bean = objPool.get(beanId);
// 遍历<bean.../>元素的每个<property.../>子元素
for (Object prop : beanEle.elements())
{
Element propEle = (Element) prop;
// 取得<property.../>元素的name属性
String propName = propEle.attributeValue("name");
// 取得<property.../>元素的value属性
String propValue = propEle.attributeValue("value");
// 取得<property.../>元素的ref属性
String propRef = propEle.attributeValue("ref");
// 将属性名的首字母大写
String propNameCamelize = propName.substring(0, 1)
.toUpperCase()
+ propName.substring(1, propName.length());
// 如果<property.../>元素的value属性值存在
if (propValue != null && propValue.length() > 0)
{
// 获取设值注入所需的setter方法
Method setter = bean.getClass().getMethod(
"set" + propNameCamelize, String.class);
// 执行setter注入
setter.invoke(bean, propValue);
}
if (propRef != null && propRef.length() > 0)
{
// 取得需要被依赖注入的Bean实例
Object target = objPool.get(propRef);
// objPool池中不存在指定Bean实例
if (target == null)
{
// 此处还应处理Singleton Bean依赖prototype Bean的情形
}
// 定义设值注入所需的setter方法
Method setter = null;
// 遍历target对象所所实现的所有接口
for (Class<?> superInterface : target.getClass()
.getInterfaces())
{
try
{
// 获取设值注入所需的setter方法
setter = bean.getClass().getMethod(
"set" + propNameCamelize,
superInterface);
// 如果成功取得该接口对应的方法,直接跳出循环
break;
} catch (NoSuchMethodException ex)
{
// 如果没有找到对应的setter方法,继续下次循环
continue;
}
}
// 如果setter方法依然为null,
// 则直接取得target实现类对应的setter方法
if (setter == null)
{
setter = bean.getClass().getMethod(
"set" + propNameCamelize,
target.getClass());
}
// 执行setter注入
setter.invoke(bean, target);
}
}
}
}
}
}

上面的CrazyitXmlApplicationContext类当然不能与SpringApplicationContext实现类相比,该容器类仅仅实现了简单的IoC功能,而且并未为prototype行为的Bean的属性提供依赖注入功能。读者可以通过该工厂类大致了解Spring底层的实现原理。
下面是测试该工厂类的主类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package lee;

import org.crazyit.ioc.*;

public class IoCTest
{
public static void main(String[] args) throws Exception
{
// 创建IoC容器
ApplicationContext ctx = new CrazyitXmlApplicationContext("beans.xml");
// 从IoC容器中取出computer Bean
Computer c = (Computer) ctx.getBean("computer");
// 测试Computer对象
c.keyIn("轻量级Java EE企业应用实战");
c.keyIn("疯狂Java讲义");
c.print();
System.out.println(ctx.getBean("now"));
}
}

从上面程序中的粗体字代码可以看出,本程序的IoC容器具有和Spring容器类似的功能,同样可以创建并管理容器中所有的Bean实例
与简单工厂模式类似的还有工厂方法和抽象工厂模式,下面将进一步讲解工厂方式和抽象工厂模式的设计方式.