程序中的耦合
# 10.程序中的耦合
本文我们先讲讲使用 Spring 之前,程序中存在的耦合问题
# 环境搭建
我们创建一个 Maven 项目,假设叫 LearnSpring,并添加 JDBC 的依赖
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.peterjxl</groupId>
<artifactId>LearnSpring</artifactId>
<version>1.0-SNAPSHOT</version>
<name>LearnSpring</name>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
</dependencies>
</project>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 数据库准备
我们初始化下数据库:
- 创建数据库 learnSpring
- 创建用户 learnSpringUser,密码 learnSpringPassword
- 创建表 account,并 insect 几条记录
-- 创建数据库 learnSpring:
DROP DATABASE IF EXISTS learnSpring;
CREATE DATABASE learnSpring;
-- 创建登录用户 learnSpringUser / 口令 learnSpringPassword
CREATE USER IF NOT EXISTS learnSpringUser@'%' IDENTIFIED BY 'learnSpringPassword';
GRANT ALL PRIVILEGES ON learnspring.* TO learnSpringUser@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;
USE learnSpring;
create table account(
id int primary key auto_increment,
name varchar(40),
money float
)character set utf8 collate utf8_general_ci;
insert into account(name,money) values('aaa',1000);
insert into account(name,money) values('bbb',1000);
insert into account(name,money) values('ccc',1000);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
相关 SQL 已放在 src/main/resources/Database_Init.sql 里
# 使用 JDBC
接下来我们写一个 JDBC 程序,操作数据库,步骤如下:
注册驱动
获取连接
获取操作数据库的预处理对象
执行 SQL,得到结果集
遍历结果集
释放资源
package com.peterjxl.jdbc;
import java.sql.*;
/**
* 说明程序的耦合
*/
public class JdbcDemo1 {
public static void main(String[] args) throws SQLException {
//1. 注册驱动
DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
//2. 获取连接
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/learnSpring", "learnSpringUser", "learnSpringPassword");
//3. 获取操作数据库的预处理对象
PreparedStatement pstm = conn.prepareStatement("select * from account");
//4. 执行 SQL,得到结果集
ResultSet resultSet = pstm.executeQuery();
//5. 遍历结果集
while (resultSet.next()) {
System.out.println(resultSet.getString("name"));
}
//6. 释放资源
resultSet.close();
pstm.close();
conn.close();
}
}
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
运行结果:
aaa
bbb
ccc
2
3
# 耦合的问题
我们在注册 jar 包的时候,用到了这个代码:
DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
换句话说,我们必须先 import 这个 jar 包中的依赖,才能运行项目。
如果我们将依赖注释掉:
<dependencies>
<!-- <dependency>-->
<!-- <groupId>mysql</groupId>-->
<!-- <artifactId>mysql-connector-java</artifactId>-->
<!-- <version>8.0.28</version>-->
<!-- </dependency>-->
</dependencies>
2
3
4
5
6
7
毫无疑问,是会报错的:而且是在编译时就异常了,而不是运行期,这就是程序的一种耦合
D:\Projects\LearnSpring\src\main\java\com\peterjxl\jdbc\JdbcDemo1.java:11:59
java: 程序包com.mysql.cj.jdbc不存在
2
耦合的概念:程序间的依赖关系,包括类之间的依赖、方法间的依赖
解耦:降低程序间的依赖关系,注意是降低,而不是彻底解耦,因为无论如何,我们都是要用到 JDBC 的。
实际开发中,应该做到编译期不依赖,运行时才依赖。如果在编译时就依赖某个 jar 包,那么该类的独立性就很差
解耦的思路:
- 方法一:使用反射来创建对象,而避免使用 new 关键字,例如我们之前使用的是
Class.forName("com.mysql.cj.jdbc.Driver");
,这里我们仅仅是用字符串,不用再依赖某个具体的类了。即使我们没有导入 JDBC 的依赖,编译时也不会报错,而是运行时有异常 - 方法二:使用方法一的时候,我们全限定类名是写死在代码里的,如果要换数据库等,又得改代码;此时我们可以改为通过读取配置文件来获取要创建的对象全限定类名。
除了对第三方 jar 包的依赖,我们自己写的类之间也存在耦合。接下来我们来演示业务层和 Dao 层,存在的耦合问题
# 新建 Dao 层
我们新建一个 Dao 层,来演示。由于我们只是模拟,并不是真的要保存,因此代码以简单为主。
新建接口:
package com.peterjxl.dao;
public interface IAccountDao {
//模拟保存账户
void saveAccount();
}
2
3
4
5
6
7
8
新建实现类:
package com.peterjxl.dao.impl;
import com.peterjxl.dao.IAccountDao;
/**
* 账户的持久层实现类
*/
public class AccountDaoImpl implements IAccountDao {
@Override
public void saveAccount() {
System.out.println("保存了账户");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 新建 Service 层
新建接口:
package com.peterjxl.service;
public interface IAccountService {
//模拟保存账户
void saveAccount();
}
2
3
4
5
6
7
8
新建实现类
package com.peterjxl.service.impl;
import com.peterjxl.dao.IAccountDao;
import com.peterjxl.dao.impl.AccountDaoImpl;
import com.peterjxl.service.IAccountService;
/**
* 账户的业务层实现类
*/
public class AccountServiceImpl implements IAccountService {
private IAccountDao accountDao = new AccountDaoImpl();
@Override
public void saveAccount() {
accountDao.saveAccount();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
可以看到,service 层的实现类,也依赖于 dao 的实现类(也就是第 12 行的 new AccountDaoImpl()
)
# 新建表现层
不仅仅是 service 层,表现层也有同样的问题:
package com.peterjxl.ui;
import com.peterjxl.service.IAccountService;
import com.peterjxl.service.impl.AccountServiceImpl;
/**
* 模拟一个表现层,用于调用业务层
*/
public class Client {
public static void main(String[] args) {
IAccountService as = new AccountServiceImpl();
as.saveAccount();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
表现层,必须 new 一个实现类 AccountService 之后,才能调用方法。如果 service 层改名了,或者移动了下位置,那么表现层就得重新修改,独立性很差
# 工厂模式
如果我们将 AccountDaoImpl
,也就是 dao 的实现类,改个名,或者误删了,那么 service 层,表现层,都会报错!这和我们之前演示 JDBC 的案例的时候一样,在编译时就报错了,而不是运行时异常
我们可以使用工厂模式,来降低耦合度,由工厂来返回 service 层对象。
接下来我们创建一个类 BeanFactory
,这是一个创建 Bean 对象的工厂。
Bean:在计算机英语中,有可重用组件的含义。举个生活的例子,我们的电脑由很多个组件组成,例如 CPU,内存条,显卡;而在一个项目中,也有很多的组件,例如 dao 层的实现类,可以被很多个 service 层使用,也就是可以被重复使用(重用)
JavaBean:用 Java 语言编写的可重用组件。之前很多人认为 JavaBean 就是 实体类,其实 JavaBean 不仅仅包含了实体类,例如 dao 层的实现类。
因此,BeanFactory
就是我们用来创建 service 和 dao 对象的,为此我们需要做如下事情:
- 需要一个配置文件来配置我们的 service 和 dao。配置文件的内容:唯一标识 = 全限定类名(key = value)
- 通过读取配置文件中配置的内容,反射创建对象。配置文件可以是 XML 也可以是 properties
# 创建配置文件
我们在 resources 目录下创建一个配置文件,bean.properties
accountService=com.peterjxl.service.impl.AccountServiceImpl
accountDao=com.peterjxl.dao.impl.AccountDaoImpl
2
# 新建工厂类
public class BeanFactory {
private static Properties props;
// 使用静态代码块为 Properties 对象赋值
static {
props = new Properties();
try {
props.load(BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties"));
} catch (Exception e) {
throw new ExceptionInInitializerError("初始化properties失败");
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
虽然这里也有 new 关键字,但注意我们只能降低耦合,不能完全消除耦合。
如果读取配置文件出错,那么程序是不能运行的,因此我们直接抛出一个 Error
接下来我们就根据反射的原理,获取并创建 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;
}
2
3
4
5
6
7
8
9
10
11
# 改造 Client 类
至此,我们就可以通过工厂来创建对象了:
public class Client {
public static void main(String[] args) {
// IAccountService as = new AccountServiceImpl();
IAccountService as = (IAccountService) BeanFactory.getBean("accountService");
as.saveAccount();
}
}
2
3
4
5
6
7
# 改造 service 层
同理,我们也可以改造 service 层:
public class AccountServiceImpl implements IAccountService {
// private IAccountDao accountDao = new AccountDaoImpl();
private IAccountDao accountDao = (IAccountDao) BeanFactory.getBean("accountDao");
@Override
public void saveAccount() {
accountDao.saveAccount();
}
}
2
3
4
5
6
7
8
9
10
11
接下来我们测试下 Client 类,是可以正常运行的。
# 工厂类的问题
我们目前获取 bean 对象的时候,每次都会创建一个新的对象,这就不算重用了,我们可以演示下:
public class Client {
public static void main(String[] args) {
// IAccountService as = new AccountServiceImpl();
for (int i = 0; i < 5; i++) {
IAccountService as = (IAccountService) BeanFactory.getBean("accountService");
System.out.println(as);
}
}
}
2
3
4
5
6
7
8
9
运行结果:
com.peterjxl.service.impl.AccountServiceImpl@14ae5a5
com.peterjxl.service.impl.AccountServiceImpl@7f31245a
com.peterjxl.service.impl.AccountServiceImpl@6d6f6e28
com.peterjxl.service.impl.AccountServiceImpl@135fbaa4
com.peterjxl.service.impl.AccountServiceImpl@45ee12a7
2
3
4
5
可以看到每次都是不同的对象。此时我们使用的并不是单例模式,而是多例模式。使用多例模式效率较低。为此我们需要对工厂类进行改造,只创建一次对象。
虽然使用单例模式可能会有线程安全问题,也就是我们如果方法中有操作类的成员变量,就会有问题;但我们一般都不会这样做,所以使用单例模式效果会更好。
# 改造工厂类
具体怎么做呢?
- 我们可以用一个 map 来保存对象,我们称之为容器;
- 在初始化的时候,将所有 bean 对象初始化并存到 map 中;
- 获取 bean 对象的时候,只需将容器里的对象返回即可。
package com.peterjxl.factory;
import java.util.*;
public class BeanFactory {
private static Properties props;
// 定义一个 map,用于存放我们要创建的对象。我们把它称之为容器
private static Map<String, Object> beans;
static {
props = new Properties();
try {
props.load(BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties"));
beans = new HashMap<>();
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);
}
} catch (Exception e) {
throw new ExceptionInInitializerError("初始化properties失败");
}
}
/**
* 根据 bean 的名称获取 bean 对象
* @param beanName
* @return
*/
public static Object getBean(String beanName){
return beans.get(beanName);
}
}
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
此时我们再次运行 Client.main 方法,可以看到都是同一个对象:
com.peterjxl.service.impl.AccountServiceImpl@14ae5a5
com.peterjxl.service.impl.AccountServiceImpl@14ae5a5
com.peterjxl.service.impl.AccountServiceImpl@14ae5a5
com.peterjxl.service.impl.AccountServiceImpl@14ae5a5
com.peterjxl.service.impl.AccountServiceImpl@14ae5a5
2
3
4
5
# 源码
本项目已将源码上传到 GitHub (opens new window) 和 Gitee (opens new window) 上。并且创建了分支 demo1,读者可以通过切换分支来查看本文的示例代码。