Spring笔记03 工厂模式
coconutnut

https://www.bilibili.com/video/av47952931
p9~14


程序的耦合与解耦 (以jdbc注册驱动为例)

jdbc操作中,注册数据库驱动时,有2种方法

1
2
3
4
// 方法1
DriverManager.registerDriver(new com.mysql.jdbc.Driver());
// 方法2
Class.forName("com.mysql.jdbc.Driver");

在pom.xml中添加了依赖的情况下都可以正常运行

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
</dependencies>

但是,如果去掉这段依赖

方法1报Error,无法通过编译

1
Error:(26, 56) java: 程序包com.mysql.jdbc不存在

而方法2报Exception,可以通过编译(无法运行)

1
Exception in thread "main" java.lang.ClassNotFoundException: com.mysql.jdbc.Driver

因为方法1依赖一个具体的驱动类,而方法2用反射,依赖的只是一个字符串
但是这个字符串仍是写死在代码里的。应该写到配置文件里去,进一步减少耦合

实际开发中应该做到:

编译期不依赖,运行时才依赖

解耦的思路:

第一步:使用反射来创建对象,而避免使用new关键字

第二步:通过读取配置文件来获取要创建的对象全限定类名


工厂模式

原始的分层实现方法

  • Client : 模拟一个表现层,用于调用业务层
  • IAccountService : 账户业务层的接口
  • AccountServiceImpl : 账户的业务层实现类
  • IAccountDao : 账户的持久层接口
  • AccountDaoImpl : 账户的持久层实现类

其中,有两个依赖关系

  1. 表现层调用业务层时
    1
    2
    3
    4
    5
    6
    public class Client {
    public static void main(String[] args) {
    IAccountService as = new AccountServiceImpl();
    as.saveAccount();
    }
    }
  2. 业务层调用持久层时
    1
    2
    3
    4
    5
    6
    public class AccountServiceImpl implements IAccountService {
    private IAccountDao accountDao = new AccountDaoImpl();
    public void saveAccount(){
    accountDao.saveAccount();
    }
    }
    都用到了new,耦合度高
    如果此时把AccountDaoImpl的代码删了,Service就报错了。和前面jdbc中编译期的错误一样

如何解除这种依赖?

Bean & BeanFactory

Bean —— 可重用组件
eg:一个Dao可能被多个Service使用,一个Service可能被多个Servlet使用,它们是可重用的

JavaBean —— 用java语言编写的可重用组件

BeanFactory —— 创建Bean对象的工厂
eg:创建Dao和Service对象

要实现这个工厂,类似前面jdbc,

1.需要一个配置文件来配置service和dao
内容:唯一标识=全限定类名(key=value)
配置文件可以是xml也可以是properties

2.读取配置文件中配置的内容,通过==反射==创建对象

工厂模式解耦

在resources中新建beans.properties配置文件(此处用properties因为简单,Spring中用的是xml)

1
2
accountService = com.itheima.service.impl.AccountServiceImpl
accountDao = com.itheima.dao.impl.AccountDaoImpl

创建BeanFactory类读取properties文件

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
public class BeanFactory {
// 定义一个Properties对象
private static Properties props;

// 使用静态代码块为Properties对象赋值
static {
try {
// 实例化对象
props = new Properties();
// 获取properties文件的流对象
InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
props.load(in);
}catch(Exception e){
// 抛一个Error,没有获取配置信息后面想都不要想
throw new ExceptionInInitializerError("初始化properties失败!");
}
}

// 根据Bean的名称获取bean对象
public static Object getBean(String beanName){
Object bean = null;
try {
String beanPath = props.getProperty(beanName);
bean = Class.forName(beanPath).newInstance();
}catch (Exception e){
e.printStackTrace();
}
return bean;
}
}

注意:

  1. 读配置文件时不要用FileInputStream,Web工程不好找路径。用类加载器。
  2. getBean()返回的是Object类型

把两处使用new创建对象的改为用反射创建

Client中

1
2
// IAccountService as = new AccountServiceImpl();
IAccountService as = (IAccountService) BeanFactory.getBean("accountService");

AccountServiceImpl中

1
2
// private IAccountDao accountDao = new AccountDaoImpl();
private IAccountDao accountDao = (IAccountDao) BeanFactory.getBean("accountDao");

Object类强转为对应的类

改进后程序的UML类图长这样

此时如果把AccountServiceImpl删了,程序可以运行,抛ClassNotFoundException

工厂模式的问题与改进

如果要在Client中多次调用Service?

1
2
3
4
5
for(int i=0;i<5;i++) {
IAccountService as = (IAccountService) BeanFactory.getBean("accountService");
System.out.println(as);
as.saveAccount();
}

并在AccountServiceImpl中加一个成员变量i

1
2
3
4
5
6
7
8
9
public class AccountServiceImpl implements IAccountService {
private IAccountDao accountDao = (IAccountDao) BeanFactory.getBean("accountDao");
private int i = 1;
public void saveAccount(){
accountDao.saveAccount();
System.out.println(i);
i++;
}
}

运行

com.itheima.service.impl.AccountServiceImpl@610455d6
保存了账户
1
com.itheima.service.impl.AccountServiceImpl@511d50c0
保存了账户
1
com.itheima.service.impl.AccountServiceImpl@60e53b93
保存了账户
1
com.itheima.service.impl.AccountServiceImpl@5e2de80c
保存了账户
1
com.itheima.service.impl.AccountServiceImpl@1d44bcfa
保存了账户
1

Process finished with exit code 0

可以看到,AccountServiceImpl创建了5次,每次都是一个新的对象
此时的对象是多例,效率没有单例高

可以在BeanFactory中把创建出的对象都存起来

1
2
//定义一个Map,用于存放我们要创建的对象。我们把它称之为容器
private static Map<String,Object> beans;

在静态代码块中,得到配置文件的输入流后,实例化这个容器。取出配置文件中所有的key-value,创建并保存它们

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 实例化容器
beans = new HashMap<String,Object>();
// 取出配置文件中所有的Key
Enumeration keys = props.keys();
// 遍历枚举
while (keys.hasMoreElements()){
// 取出每个Key
String key = keys.nextElement().toString();
// 根据key获取value
String beanPath = props.getProperty(key);
// 反射创建对象
Object value = Class.forName(beanPath).newInstance();
// 把key和value存入容器中
beans.put(key,value);
}

获取Beans,不用newInstance(),直接从容器中取即可

1
2
3
public static Object getBean(String beanName){
return beans.get(beanName);
}

这样修改后,调用5次Service打印的结果是

com.itheima.service.impl.AccountServiceImpl@610455d6
保存了账户
1
com.itheima.service.impl.AccountServiceImpl@610455d6
保存了账户
2
com.itheima.service.impl.AccountServiceImpl@610455d6
保存了账户
3
com.itheima.service.impl.AccountServiceImpl@610455d6
保存了账户
4
com.itheima.service.impl.AccountServiceImpl@610455d6
保存了账户
5

Process finished with exit code 0

此时的AccountServiceImpl就是单例的了

但有一个问题是,这个i在多线程时是不安全的
应该把它移到方法里面,就没有这个问题了(实际使用一般也是这样)

1
2
3
4
5
6
7
8
//    private int i = 1;

public void saveAccount(){
int i = 1;
accountDao.saveAccount();
System.out.println(i);
i++;
}

工厂模式使用套路总结

1.创建BeanFactory

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
// BeanFactory.java

public class BeanFactory {
//定义一个Properties对象
private static Properties props;

//定义一个Map,用于存放我们要创建的对象。称之为容器
private static Map<String,Object> beans;

//使用静态代码块为Properties对象赋值
static {
try {
// 1.实例化对象
props = new Properties();
// 2.获取properties文件的流对象
InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
props.load(in);
// 3.实例化容器
beans = new HashMap<String,Object>();
// 4.取出配置文件中所有的Key
Enumeration keys = props.keys();
// 5.遍历枚举
while (keys.hasMoreElements()){
// 取出每个Key
String key = keys.nextElement().toString();
// 根据key获取value
String beanPath = props.getProperty(key);
// 反射创建对象
Object value = Class.forName(beanPath).newInstance();
// 把key和value存入容器中
beans.put(key,value);
}
}catch(Exception e){
throw new ExceptionInInitializerError("初始化properties失败!");
}
}

// 根据bean的名称获取对象
public static Object getBean(String beanName){
return beans.get(beanName);
}
}

2.配置文件

1
2
3
4
// beans.properties

accountService=com.itheima.service.impl.AccountServiceImpl
accountDao=com.itheima.dao.impl.AccountDaoImpl

3.获取Bean

1
2
3
4
5
6
7
8
9
10
// Client.java

public class Client {

public static void main(String[] args) {
IAccountService as = (IAccountService) BeanFactory.getBean("accountService");
as.saveAccount();
}

}