程序的耦合及解耦
本文目录
1.什么是程序的耦合
简单来说耦合是程序间的依赖关系,像在一个类中创建另一个的类对象。
包括:
- 类之间的依赖
- 方法之间的依赖
对象之间的耦合越高,维护成本越高。因此对象的设计应使类和构件之间的耦合最小。
我们在开发中,有些依赖关系是必须的,有些依赖关系可以通过代码优化来解除。
早期我们的 JDBC 操作,注册驱动时,我们为什么不使用 DriverManager 的 register 方法,而是采用 Class.forName 的方式?
public class JdbcDemo1 {
public static void main(String[] args) throws Exception {
//1.注册驱动,加载驱动程序
//DriverManager.registerDriver(new Driver());
Class.forName("com.mysql.jdbc.Driver");//字符串写死
//3.获取连接对象
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/user?useSSL=false", "root", "201703457");
//4.编写SQL语句
String sql="select * from emp";
//5.获取操作数据库的预处理对象
PreparedStatement pstm = conn.prepareStatement(sql);
//6.执行sql,得到结果集
ResultSet rs = pstm.executeQuery();
//7.遍历结果集
while (rs.next()){
System.out.println(rs.getString("name"));
}
//8.释放资源
rs.close();
pstm.close();
conn.close();
}
}
原因就是:我们的类依赖了数据库的具体驱动类(MySQL),如果这时候更换了数据库品牌(比如 Oracle),需要修改源码来重新加载数据库驱动。这显然不是我们想要的,也做不了(修改jar包)。
Class.forName(“com.mysql.jdbc.Driver”);//此处只是一个字符串
是通过反射来注册驱动的。
此时的好处是,我们的类中不再依赖具体的驱动类,此时就算删除 mysql 的驱动 jar 包,依然可以编译(运行就不要想了,没有驱动不可能运行成功的)。
同时,也产生了一个新的问题,mysql 驱动的全限定类名字符串是在 java 类中写死的,一旦要改还是要修改源码。
解决这个问题也很简单,使用配置文件配置驱动。
2.解决程序耦合的思路
解耦:就是降低程序间的依赖关系。
实际开发中:应该做到编译期不依赖,运行时才依赖。
解耦的思路:
- 第一步:使用反射来创建对象,而避免使用new关键字。
- 第二步:通过读取配置文件来获取要创建的对象全限定类名
此处理解思维,解决上述jdbc耦合问题,使用配置文件,在文件中配置数据库的驱动程序的全限定类名和连接数据库的url,账户以及密码。然后在程序中解析配置文件,获取配置的内容。使用反射创建对象。
#db.properties
url=jdbc:mysql://localhost:3306/user?useSSL=false&characterEncoding=UTF-8
user=root
password=201703457
driver=com.mysql.jdbc.Driver
public class JdbcDemo2 {
public static void main(String[] args) throws Exception {
//1.注册驱动,加载驱动程序
//DriverManager.registerDriver(new Driver());
Properties pro = new Properties();
ClassLoader classLoader = JdbcDemo2.class.getClassLoader();//获取ClassLoader对象
URL res = classLoader.getResource("db.properties");//定位jdbc.properties绝对路径资源
String path = res.getPath();//获取资源的字符串路径
pro.load(new FileInputStream(path));
String url = pro.getProperty("url");
String user = pro.getProperty("user");
String password = pro.getProperty("password");
String driver = pro.getProperty("driver");
Class.forName(driver);
//3.获取连接对象
Connection conn = DriverManager.getConnection(url, user, password);
//4.编写SQL语句
String sql="select * from emp";
//5.获取操作数据库的预处理对象
PreparedStatement pstm = conn.prepareStatement(sql);
//6.执行sql,得到结果集
ResultSet rs = pstm.executeQuery();
//7.遍历结果集
while (rs.next()){
System.out.println(rs.getString("name"));
}
//8.释放资源
rs.close();
pstm.close();
conn.close();
}
}
3.工厂模式解耦
在实际开发中我们可以把三层(MVC模式)的对象都使用配置文件配置起来,当启动服务器应用加载的时候,让一个类中的方法通过读取配置文件,把这些对象创建出来并存起来。
在接下来的使用的时候,直接拿过来用就好了。那么,这个读取配置文件,创建和获取三层对象的类就是工厂。
下面使用工厂来对一个简单程序进行解耦。用户保存账号的例子。
他们之间存在的依赖关系Client→AccountServuceImpl→AccountDaoImpl,每个对象需要另一个对象的时候需要主动在这个类中new出来实例,这样写会造成很高的耦合度。程序之间就捆绑住了。
解决方案:使用一个创建Bean对象的工厂。
Bean:在计算机英语中,有可重用组件的含义。
JavaBean:用java语言编写的可重用组件。
理解:javabean > 实体类
这个工厂就是创建我们的service
和dao
对象的。使用工厂解耦需要
- 第一个:需要一个配置文件来配置我们的
service
和dao
的类
配置的内容:唯一标识=全限定类名(key=value) - 第二个:通过读取配置文件中配置的内容,使用反射创建对象
我们的配置文件可以是xml也可以是properties(数据较少的时候使用)
配置文件bean.properties
accountService=com.dab.service.Impl.AccountServiceImpl
accountDao=com.dab.Dao.Impl.AccountDaoImpl
工厂类BeanFactory
public class BeanFactory {
//定义一个Properties对象
private static Properties pros;
//使用静态代码块为Properties对象赋值
static {
try {
//实例化对象
pros=new Properties();
InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
pros.load(in);
}
} catch (Exception e) {
throw new ExceptionInInitializerError("初始化properties失败!");
}
}
/**
* 根据Bean的名称获取bean对象
* @param beanName
* @return
*/
public static Object getBean(String beanName){
Object bean = null;
try {
String beanPath = pros.getProperty(beanName);
//System.out.println(beanPath);
bean = Class.forName(beanPath).newInstance();//反射方式创建对象,newInstance每次都会调用默认构造函数创建对象
}catch (Exception e){
e.printStackTrace();
}
return bean;
}
}
其它代码
模拟View层的ui
/**
* 模拟一个表现层,用户调用业务层
*/
public class Client {
public static void main(String[] args) {
//new方式创建对象
//IAccountService as = new AccountServiceImpl();
//改造,使用工厂生产对象
for (int i = 0; i < 5; i++) {
IAccountService as = (IAccountService) BeanFactory.getBean("accountService");//向下转型
System.out.println(as);
/*
多例对象,每次都会初始化对象
效率没有单例对象高
com.dab.service.Impl.AccountServiceImpl@7f31245a
com.dab.service.Impl.AccountServiceImpl@6d6f6e28
com.dab.service.Impl.AccountServiceImpl@135fbaa4
com.dab.service.Impl.AccountServiceImpl@45ee12a7
com.dab.service.Impl.AccountServiceImpl@330bedb4
单例对象
com.dab.service.Impl.AccountServiceImpl@7f31245a
com.dab.service.Impl.AccountServiceImpl@7f31245a
com.dab.service.Impl.AccountServiceImpl@7f31245a
com.dab.service.Impl.AccountServiceImpl@7f31245a
com.dab.service.Impl.AccountServiceImpl@7f31245a
*/
as.saveAccount();
}
}
}
sevice业务层:
/**
* 账户业务层的接口
*/
public interface IAccountService {
void saveAccount();
}
/**
* 账户的业务层实现类
*/
public class AccountServiceImpl implements IAccountService {
//new方式创建对象
//private IAccountDao accountDao = new AccountDaoImpl();
//解耦改造,使用工厂模式生产对象
private IAccountDao accountDao = (IAccountDao) BeanFactory.getBean("accountDao");
//private int i=1;
public void saveAccount() {
int i=1;
accountDao.saveAccount();
System.out.println(i);
i++;
}
}
Dao持久层
/**
* 账户的持久层接口
*/
public interface IAccountDao {
/**
* 模拟保存账户
*/
void saveAccount();
}
/**
* 账户的持久层实现类
*/
public class AccountDaoImpl implements IAccountDao {
public void saveAccount() {
System.out.println("保存了账户.");
}
}
使用工厂生产对象的好处:降低了程序间的耦合,便于管理、维护。在使用的时候,直接拿过来用就好了。
单例对象与多例对象
单例:从始至终只有一个对象;
在多个线程、多个对象访问时,如果对象的实例只有一个,在操作类成员的时候,类成员就会发生变化,出现线程问题。多例对象就没有这个问题,每次都会初始化。
多例:每次会重新创建一个对象;
上述工厂类解耦中还存在一些问题:
在service
和dao
层不会存在单例对象的线程问题,因为可以将需要类成员变量,变成方法里的局部变量,单例对象每次也会初始化类成员了。由于很少会有需要改变的类成员,所以对象可以是单例对象,效率更高。
基于以上,可以对工厂类进行改造。下面这行代码:newInstance
每次都会调用默认构造函数创建对象,效率较低,我们并不需要。我们只需要使用一次newInstance
创建对象,将对象保存起来,如果不保存起来,容易出现长期不使用对象,被java的垃圾回收机制给回收,下次使用就找不到了。
bean = Class.forName(beanPath).newInstance();//反射方式创建对象,newInstance每次都会调用默认构造函数创建对象
4.控制反转-Inversion Of Control
根据以上单例、多例的分析,改造的工产类生产的对象保存在哪?
分析:由于我们是很多对象,肯定要找个集合来存。这时候有 Map
和 List
可以选择。
到底选 Map
还是 List
就看我们有没有查找需求。有查找需求,选 Map
。
答案就是在应用加载时,创建一个 Map,用于存放三层对象。我们把这个 map
称之为 容器。
public class BeanFactory {
//定义一个Properties对象
private static Properties pros;
//定义一个Map,用于存放我们要创建的对象。称为容器
private static Map<String,Object> beans;
//使用静态代码块为Properties对象赋值
static {
try {
//实例化对象
pros=new Properties();
InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
pros.load(in);
//实例化容器
beans = new HashMap<String, Object>();
//取出配置文件中所有的Key
Enumeration keys = pros.keys();
//遍历枚举
while (keys.hasMoreElements()){
//取出每个Key
String key = keys.nextElement().toString();
//根据key获取value
String beanPath = pros.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);
}
/**
* 根据Bean的名称获取bean对象
* @param beanName
* @return
public static Object getBean(String beanName){
Object bean = null;
try {
String beanPath = pros.getProperty(beanName);
//System.out.println(beanPath);
bean = Class.forName(beanPath).newInstance();//反射方式创建对象,newInstance每次都会调用默认构造函数创建对象
}catch (Exception e){
e.printStackTrace();
}
return bean;
}*/
}
到这里我们可以知道什么是工厂了:
工厂就是负责给我们从容器中获取指定对象的类。这时候我们获取对象的方式发生了改变。
- 在获取对象时,都是采用 new 的方式。是主动的。
- 现在我们获取对象时,跟工厂要,有工厂为我们查找或者创建对象。是被动的。
这种被动接收的方式获取对象的思想就是控制反转,它是 spring 框架的核心之一。
明确 ioc 的作用:
削减计算机程序的耦合(解除我们代码中的依赖关系)。
5.使用 spring 的 的 IOC解决程序的耦合
还是解决用户保存账户的例子的耦合关系。
5.1 新建Maven项目
创建项目Next->Finish
5.2 导入spring框架的基础包
导入maven坐标:
<dependencies>
<dependency>
<groupId>org.springframwork</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
</dependencies>
导入成功后,可以看到spring的Maven资源
5.3 编写代码
1.创建业务层接口和实现类
/**
* 账户业务层的接口
*/
public interface IAccountService {
void saveAccount();
}
/**
* 账户的业务层实现类
*/
public class AccountServiceImpl implements IAccountService {
//不可能完全消除依赖
private IAccountDao accountDao = new AccountDaoImpl();//此处的依赖关系后续依靠注入方式解决
public AccountServiceImpl(){
System.out.println("对象创建了");
}
public void saveAccount() {
accountDao.saveAccount();
}
}
2.创建持久层接口和实现类
/**
* 账户的持久层接口
*/
public interface IAccountDao {
/**
* 模拟保存账户
*/
void saveAccount();
}
/**
* 账户的持久层实现类
*/
public class AccountDaoImpl implements IAccountDao {
public void saveAccount() {
System.out.println("保存了账户.");
}
}
3.模拟一个表现层,用户调用业务层
/**
* 模拟一个表现层,用户调用业务层
*/
public class Client {
/**
* 获取spring的Ioc核心容器,并根据id获取对象
*/
public static void main(String[] args) {
//1.使用 ApplicationContext 接口,获取spring核心容器对象
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//ApplicationContext ac = new FileSystemXmlApplicationContext("C:\\Users\\zhy\\Desktop\\bean.xml");
//2.根据id获取Bean对象
IAccountService as = (IAccountService)ac.getBean("accountService");//方式1,使用强转(多态向下转型)
IAccountDao adao = ac.getBean("accountDao",IAccountDao.class);//方式2,不使用强转
System.out.println(as);
System.out.println(adao);
as.saveAccount();
}
}
5.4 创建spring的XML配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- bean 标签:用于配置让 spring 创建对象,并且存入 ioc 容器之中
id 属性:对象的唯一标识。
class 属性:指定要创建对象的全限定类名
-->
<!-- 创建的对象交给spring来管理:通过id获取对象-->
<!-- 配置 service -->
<bean id="accountService" class="com.dab.service.Impl.AccountServiceImpl"></bean>
<!-- 配置 dao -->
<bean id="accountDao" class="com.dab.Dao.Impl.AccountDaoImpl"></bean>
</beans>
5.5 运行Clien.java
通过Spring的IOC容器,在不需要自己去实例化的情况下,完成了对两个类的实例化和控制。
5.6 小结
在程序执行时,对象的创建并不是通过 new一个类完成的,而是由 Spring 容器管理实现的。这就是 Spring IoC 容器思想的工作机制。
6.spring基于XML的IOC细节(1)
6.1 spring中工厂的类结构图
蓝底:接口;
(浅)绿底:实现类;
6.2 Ioc核心容器ApplicationContext接口
ApplicationContext的三个常用实现类:
ClassPathXmlApplicationContext
:它可以加载类路径下的配置文件,要求配置文件必须在类路径下。不在的话,加载不了。(更常用)FileSystemXmlApplicationContext
:它可以加载磁盘任意路径下的配置文件(必须有访问权限)AnnotationConfigApplicationContext
:它是用于读取注解创建容器的。
核心容器的两个接口引发出的问题:
ApplicationContext
:单例对象适用 。常采用此接口
它在构建核心容器时,创建对象采取的策略是采用立即加载的方式。也就是说,只要一读取完配置文件马上就创建配置文件中配置的对象。BeanFactory
:多例对象使用。
它在构建核心容器时,创建对象采取的策略是采用延迟加载的方式。也就是说,什么时候根据id获取对象了,什么时候才真正的创建对象。
//--------BeanFactory----------
Resource resource = new ClassPathResource("bean.xml");
BeanFactory factory = new XmlBeanFactory(resource);
IAccountService as = (IAccountService)factory.getBean("accountService");
System.out.println(as);
推荐阅读
- Spring学习(1)-了解spring的各个模块.
- Spring学习(2)-程序间耦合和工厂模式解耦.
- Spring学习(3)-spring核心思想IOC(控制反转)和DI(依赖注入).
- Spring学习(4)-spring IOC中bean标签,常用属性,注入方法.
- Spring学习(5)-spring Bean的作用范围和生命周期.
- Spring学习(6)-spring简单例子.
欢迎点赞评论,指出不足,笔者由衷感谢o!