0%

Java工厂模式实战

前言

本篇主要涉及如下知识点:

  1. 工厂模式设计概念
  2. Java实现一个简单的工厂类
  3. 对工厂类实现优化,使其能够支持单例和多例

工厂模式

概念

·  工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。简而言之,工厂模式就是改变了我们创建对象的方法。

  • 传统创建对象的方法是:new 出一个实例。
1
Person student = new Student();
  • 而工厂模式创建实例的方式可能如下:
1
Student student = (Student)BeanFactory.getPerson("student");

·  这意味着两点:其一,工厂模式创建的实例是隐藏细节的,我们并不完全知道工厂创建的到底是什么实例(上述代码可能返回的是一个Teacher对象,而被强转成了Student);其二,工厂模式降低了类之间的耦合(依赖)。使用传统模式来创建实例,一旦实例出现了问题(比如Student类的构造方法被改变),则代码不可能编译成功,整个项目就会面临崩溃;而工厂模式创建对象,其依赖仅仅只是一个字符串而已,即使不能运行成功,至少其编译是没问题的,类之间的依赖程度大大减少。

工厂模式创建对象实战

工厂模式创建对象原理

·  最常见的工厂模式创建对象的流程如下:“创建工厂——读取配置文件——获取配置属性对应的值(全限定类名)——根据类名使用反射创建实例对象”。对应的知识点为读取XML文件 以及 Java 反射讲解与案例实战,具体的知识点可以参考链接的这两篇博客。

实战

·  目标:使用工厂PersonFactory得到Student和Teacher类的两个对象。
·  需要导入支持xml读取的dom4j包和jaxen包;以及用于测试的testng包。

  • 两个类实现了Person接口,具体如下:
1
2
3
4
5
package com.memoforward;

public interface Person {
void code();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.memoforward;

public class Student implements Person{
private String name;
private String number;
public Student(){
System.out.println("Student对象被创建了....");
}
@Override
public void code() {
System.out.println("学生码代码...");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.memoforward;

public class Teacher implements Person{
private String name;
private Integer age;
public Teacher(){
System.out.println("Teacher对象被创建了...");
}

@Override
public void code() {
System.out.println("老师码代码....");
}
}
  • 写配置文件persons.xml
1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<persons>
<person id="student" class="com.memoforward.Student"/>
<person id="teacher" class="com.memoforward.Teacher"/>
</persons>
  • 编写工厂
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
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

public class PersonFactory01 {
private static Document document = null;
//静态代码块读取配置文件
static{
SAXReader reader = new SAXReader();
try {
document = reader.read(PersonFactory01.class.getClassLoader().getResourceAsStream("persons.xml"));
} catch (DocumentException e) {
e.printStackTrace();
}
}

public Object getPerson(String id){
Object obj = null;
//获取到配置文件中需要找到的标签
Element element = (Element) document.selectSingleNode("/persons/person[@id='"+ id +"']");
try {
//通过该标签的class属性值获取类名,并通过反射创建实例
obj = Class.forName(element.attributeValue("class")).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
}
  • 测试代码如下:
1
2
3
4
5
6
7
8
9
10
public class FactoryTest {
@Test
public void testPersonFactory01(){
PersonFactory01 personFactory = new PersonFactory01();
Student student = (Student)personFactory.getPerson("student");
student.code();
Teacher teacher = (Teacher)personFactory.getPerson("teacher");
teacher.code();
}
}
  • 测试结果如下
1
2
3
4
5
6
7
8
Student对象被创建了....
学生码代码...
Teacher对象被创建了...
老师码代码....
===============================================
Default Suite
Total tests run: 1, Passes: 1, Failures: 0, Skips: 0
===============================================

·  可见学生对象和老师对象都被BeanFactory成功的创建了。

上例的工厂模式改进

上例的问题

·  理论上,上例已经可以解决很多的问题了,但是会面临一个问题:当我每一次构造对象,我都会用newInstance创建一个新的实例对象。 如果某个类我只希望创建一个实例对象(每次getPerson都获得同一个对象),上述的工厂就不再适用了。举个简单的例子:一个班只有一个老师,我每次找老师都只想找到同一个老师。
·  上面的问题就是我们熟知的单例多例的问题,有的时候我们并不想用多例的模式去操作Bean,因此对上例工厂模式的改进就是:把多例模式的工厂转换为单例模式的工厂。

  • 多例测试代码:如果连续获取5次Teacher对象,得到5个不同的实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for(int i = 0; i < 5; i++){
Teacher teacher = (Teacher)personFactory.getPerson("teacher");
System.out.println(teacher);
}
//测试结果如下:
Teacher对象被创建了...
com.memoforward.Teacher@11758f2a
Teacher对象被创建了...
com.memoforward.Teacher@e720b71
Teacher对象被创建了...
com.memoforward.Teacher@1b26f7b2
Teacher对象被创建了...
com.memoforward.Teacher@491cc5c9
Teacher对象被创建了...
com.memoforward.Teacher@74ad1f1f

工厂类的改写

·  之前我们在工厂类中,使用了静态代码块去加载了配置文件,创造了单例的静态document树对象。因此,创造单例模式的工厂的思路就是:构建单例的静态Map集合去存储不同类的实例对象。很显然,对Map对象的赋值操作也是在静态代码块中实现的,这意味着,每个类的实例对象都是在配置文件被加载的同时被创建的。

  • 单例Person工厂的代码如下:
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
package com.memoforward.factory;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class PersonFactory02 {
private static Document document;
private static Map<String, Object> beanMap = new HashMap<>();
static{
SAXReader reader = new SAXReader();
try {
String beanId = null;
Object obj;
document = reader.read(PersonFactory02.class.getClassLoader().getResourceAsStream("persons.xml"));
//选取persons的所有子元素
List<Element> elements = document.selectNodes("/persons/person");
for(Element element : elements){
beanId = element.attributeValue("id");
obj = Class.forName(element.attributeValue("class")).newInstance();
beanMap.put(beanId,obj);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public Object getPerson(String id){
return beanMap.get(id);
}
}
  • 测试如下:分别获取3个Student和3个Teacher对象
1
2
3
4
5
6
7
8
9
10
11
@Test
public void testPersonFactory02(){
PersonFactory02 pf = new PersonFactory02();
for(int i = 0; i < 3; i++){
Student student = (Student)pf.getPerson("student");
System.out.println(student);
Teacher teacher = (Teacher)pf.getPerson("teacher");
System.out.println(teacher);
System.out.println("---------------------------------");
}
}
  • 测试结果如下:每次得到的都是同一个对象,且对象只创建一次。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Student对象被创建了....
Teacher对象被创建了...
com.memoforward.Student@77ec78b9
com.memoforward.Teacher@1a3869f4
---------------------------------
com.memoforward.Student@77ec78b9
com.memoforward.Teacher@1a3869f4
---------------------------------
com.memoforward.Student@77ec78b9
com.memoforward.Teacher@1a3869f4
---------------------------------
===============================================
Default Suite
Total tests run: 1, Passes: 1, Failures: 0, Skips: 0
===============================================
  • 补充:单例模式对象的初始化操作在加载配置文件的时候就完成了,即在new PersonFactory02()的时候,两个对象就被创建了吗,而静态代码块只会执行一次,因此这两个对象永远不会改变。

工厂的再改进

·  以上的两个工厂类明显都不太行,一个只能支持多例,一个只能支持单例。因此最理想的工厂应该根据用户的需求自己判断创建多例和单例的对象。这当然呀没有什么难度,但是需要在配置文件中声明其对象的作用范围。

  • 配置文件修改:加入scope属性,值为singleton的时候为单例,值为prototype的时候为多例;如果不写此属性,则默认为单例。
1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<persons>
<person id="student" class="com.memoforward.Student" scope="prototype"/>
<person id="teacher" class="com.memoforward.Teacher" scope="singleton"/>
</persons>
  • 工厂类如下
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
package com.memoforward.factory;

import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class PersonFactory {
private static Document document;
private static Map<String, Object> beanMap = new HashMap<>();
static{
SAXReader reader = new SAXReader();
try {
String beanId = null;
Object obj;
document = reader.read(PersonFactory.class.getClassLoader().getResourceAsStream("persons.xml"));
//选取persons的所有子元素
List<Element> elements = document.selectNodes("/persons/person");
for(Element element : elements){
Attribute attr = element.attribute("scope");
String scope = "";
if(attr != null){
scope = attr.getStringValue();
}
if(scope.equals("singleton") || scope.equals("") || scope == null){
beanId = element.attributeValue("id");
obj = Class.forName(element.attributeValue("class")).newInstance();
beanMap.put(beanId,obj);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public Object getPerson(String id){
if(beanMap.get(id) != null)
return beanMap.get(id);
else{
Object obj = null;
Element element = (Element) document.selectSingleNode("/persons/person[@id='"+ id +"']");
try {
obj = Class.forName(element.attributeValue("class")).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
}
}
  • 测试如下:分别获取两次Teacher和两个Student。
1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testPersonFactory(){
PersonFactory pf = new PersonFactory();
System.out.println("****************");
for(int i = 0; i < 2; i++){
Student student = (Student)pf.getPerson("student");
System.out.println(student);
Teacher teacher = (Teacher)pf.getPerson("teacher");
System.out.println(teacher);
System.out.println("---------------------------------");
}
}
  • 测试结果如下:可以看到:配置加载时,只有Teacher对象被创建;每次获得Teacher和Student时,都会创建新的Studnet对象,而Teacher的对象不变。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Teacher对象被创建了...
****************
Student对象被创建了....
com.memoforward.Student@1b26f7b2
com.memoforward.Teacher@491cc5c9
---------------------------------
Student对象被创建了....
com.memoforward.Student@74ad1f1f
com.memoforward.Teacher@491cc5c9
---------------------------------
===============================================
Default Suite
Total tests run: 1, Passes: 1, Failures: 0, Skips: 0
===============================================

单例和多例对象的生命周期

·  这个问题只要弄懂了原理就特别简单:单例对象是在工厂类被加载时被创建的,因此生命周期与工厂类是一样的;而多例对象本质与被new出来的对象没有区别,因此当没有任何引用指向这个对象时,会被垃圾回收器回收。

  • 单例生命周期
    1. 出生:当工厂被创建时被创建
    2. 存在:工厂类存在,单例对象一直存在
    3. 死亡:工厂类销毁或关闭时,单例对象被释放
  • 多例生命周期
    1. 出生:当要调用该对象时被创建
    2. 存在:只要该对象还在使用,就一直存在
    3. 死亡:没有任何引用指向该对象时(该对象不再使用),被垃圾回收器回收

总结

·  Java的工厂模式算是一种有名的设计模式了,个人十分喜欢这种设计思路,而且Spring的IOC也是这样来实现的,笔者的下一篇文章会在这篇文章的基础上讲一讲Spring的IOC。

交流

请联系邮箱:chenxingyu@bupt.edu.cn