文章目录
- 1. 编写 jdbc 的工程代码用于分析程序的耦合
- 1.1 什么是程序的耦合
- 1.2 耦合有如下分类:
- 1.3 总结:
- 1.4 内聚与耦合:
- 1.5 耦合的例子
- 2. 对于网页项目的分析和解耦
- 2.1 项目的搭建
- 2.2 通过编写工厂类和独立配置文件来解耦
- 2.2.1 独立出配置文件
- 2.2.2 使用工厂模式进行解耦
1. 编写 jdbc 的工程代码用于分析程序的耦合
1.1 什么是程序的耦合
耦合性(Coupling),也叫耦合度,是对模块间关联程度的度量。耦合的强弱取决于模块间接口的负载型、调用模块的方式以及通过界面传输数据的多少。模块间的耦合度是指模块之间的依赖关系,包括控制关系、调用关系、数据传递关系。模块间联系越多,其耦合性越强,同时表明其独立性越差(降低耦合性,可以提高其独立性)。耦合性存在于各个领域,而非软件设计中独有的,但是我们只讨论软件工程中的耦合。
在软件工程中,耦合指的就是对象之间的依赖性。对象之间的耦合越高,为何成本越高。因此对象的设计应使类和构建之间的耦合最小。软件设计中通常用耦合度和内聚度作为衡量模块独立程度的标注。划分模块的一个准则就是高内聚低耦合。
1.2 耦合有如下分类:
- 内容耦合。当一个模块直接修改或操作另一个模块的数据时,或一个模块不通过正常入口而转入另一个模块时,这样的耦合被成为内容耦合。内容耦合是最高程度的耦合,应该避免使用之。
- 公共耦合。两个或者两个以上的模块共同引用了一个全局数据项,这种耦合被称为全局耦合。在具有大量公共耦合的结构中,确定究竟是哪个模块给全局变量赋了一个特定的值是非常困难的。
- 外部耦合。一个模组都访问同一全局简单变量而不是同一全局数据结构,而且不是通过参数表达传递该全局变量的信息,则称之为外部耦合。
- 控制耦合。一个模块通过接口向另一个模块传递一个控制信号,接受信号的模块根据信号值而进行适当的动作,这种耦合成为控制耦合。
- 标记耦合。若一个模块A通过接口向两个模块B和C传递一个公共参数,那么称模块B和C之间存在一个标记耦合。
- 数据耦合。模块之间通过参数来传递数据,那么被称之为数据耦合。数据耦合是最低的一种耦合形式,系统中一般都存在这种类型的耦合,因为为了完成一些具有意义的功能,旺旺需要将某些陌路爱的输出数据作为另一些模块的输入数据。
- 非直接耦合。两个某块之间没有直接关系,它们之间的联系完全是通过主模块的控制和调用来实现的。
1.3 总结:
耦合是影响软件复杂程度和设计质量的一个重要因素,在设计上我们应采用以下原则:如果模块间必须存在耦合,就尽量使用数据耦合,少用控制耦合,限制公共耦合的范围,尽量避免使用内容耦合。
1.4 内聚与耦合:
内聚标志一个模块内各个元素彼此结合的紧密程度,他是信息隐蔽和局部化概念的自然扩展。内聚是从功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事情。它描述的是模块内的功能联系。耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及通过接口的数据。陈谷研究的低耦合,高内聚。就是同一模块内的各个元素之间要高度紧密,但是各个模块之间的相互依存度却不要那么紧密。
内聚和耦合是密切相关的,同其他模块存在高耦合的模块意味着低内聚,而高内聚的模块意味着该模块同其他模块之间是低耦合的。在进行软件设计时,应尽力做到高内聚,低耦合。
1.5 耦合的例子
在 Java 中,耦合意味着程序间的依赖关系。
它包括:1. 类之间的依赖。 2。 方法间的依赖
以下是 jdbc 连接的代码:
package com.runoob.test;
import java.sql.*;
public class MySQLDemo {
public static void main(String[] args) throws Exception{
// 1. 注册驱动
DriverManager.registerDriver(new com.mysql.jdbc.Driver());
// 2. 获取连接
Connection conn = DriverManager.getConnection("");
// 3. 获取操作预处理对象
PreparedStatement pstm = conn.preparedStatement("");
// 4. 执行 sql 语句,得到结果集
ResultSet rs = pstm.executeQuery();
// 5. 遍历结果集
while(rs.next) {
System.out.println(rs.getString(""));
}
// 6. 释放资源
rs.close();
pstm.close();
conn.closer();
}
}
类之间的依赖,比如,我们在使用 mysql-connection-java 这个mysql连接工具的时候,我们必须导入 com.mysql.jdbc.Driver 包。如果我们完全放弃这个jar包,我们就无法使用相关的工具,所以,不可能完全放弃类之间的依赖。我们能做的就是降低程序之间的依赖关系。这就是解耦。
我们现在这个情况,(比如以上在注册驱动的时候)在编译期就开始具体依赖某个类或者某个jar包(com.mysql.jdbc.Driver)。那么,这个类的独立性会非常差。当我们想将其中的连接数据库的代码抽取出来的时候,会发现,在解除(com.mysql.jdbc.Driver)依赖之前,很难抽取出来。
所以,我们在实际开发中,应该做到编译期不依赖,运行时才依赖。
上面的代码,可以使用以下方法,来做到解耦。
我们在创建连接的时候,注册驱动一般使用的以下的方法:
// 1. 注册驱动
//DriverManager.registerDriver(new com.mysql.jdbc.Driver());
Class.forName("com.mysql.jdbc.Driver");
在这里,com.mysql.jdbc.Driver 仅仅是一个字符串,也就是说,不再依赖具体某个驱动类。这个好处是,我们可以对这一段代码进行独立。但是,这个是无法运行的。因为,没有这个驱动类(并没有导入)。当我们运行时,会报出异常(Exception)。这是一个运行时异常,意味着在编译期是没有问题的。
这就完成对mysql连接驱动类的解耦。但是,Class.forName中的驱动变成了一段写死的字符串,以后如果更换了数据,意味着我们需要进入项目内部,全部重写,这又增加了一段耦合。所以,我们需要将配置信息写进配置文件中,通过配置文件读取配置。
所以,解耦的思路如下:
- 使用反射来创建对象,而避免使用 new 关键字。
- 通过读取配置文件,来获取要创建的对象全限定类名。
2. 对于网页项目的分析和解耦
这里我们选择使用一个普通的 java maven 项目来模拟一个网页项目。
2.1 项目的搭建
- 首先是我们的模拟持久层的dao接口:
package com.selflearning.spring.dao;
/**
* 账户的持久层接口
*/
public interface IAccountDao {
/**
* 模拟保存账户
*/
void saveAccount();
}
- 接下里是持久层的dao接口的实现类:
package com.selflearning.spring.dao.impl;
import com.selflearning.spring.dao.IAccountDao;
/**
* 账户的持久层实现类
*/
public class AccountDaoImpl implements IAccountDao {
@Override
public void saveAccount() {
System.out.println("保存了账户");
}
}
- 然后是我们的模拟业务层的dao接口:
package com.selflearning.spring.service;
/**
* 业务层的接口 - 操作账户
*/
public interface IAccountService {
/**
* 模拟保存账户
*/
void saveAccount();
}
- 业务层dao接口的实现类:
package com.selflearning.spring.service.impl;
import com.selflearning.spring.dao.IAccountDao;
import com.selflearning.spring.dao.impl.AccountDaoImpl;
import com.selflearning.spring.service.IAccountService;
/**
* 模拟账户的业务层实现类
*/
public class AccountServiceImpl implements IAccountService {
private IAccountDao accountDao = new AccountDaoImpl();
@Override
public void saveAccount() {
accountDao.saveAccount();
}
}
- 最后是模拟显示层(在后面的项目中一般是 servlet ):
package com.selflearning.spring.ui;
import com.selflearning.spring.service.IAccountService;
import com.selflearning.spring.service.impl.AccountServiceImpl;
/**
* 模拟一个表现层,用于调用业务层
*/
public class Client {
public static void main(String[] args) {
IAccountService as = new AccountServiceImpl();
as.saveAccount();
}
}
- 运行:
保存了账户
但是,这里我们业务层和显示层都选择通过 new 一个对象来调用其中的方法,这无疑增加了类之间的耦合度(这样编写的代码,具有很强的耦合性,独立性非常差)。
2.2 通过编写工厂类和独立配置文件来解耦
工厂模式,实现了创建者和调用者之间的解耦。意味着调用者并不需要直接参与对象的创建,对象是通过工厂模式来创建,然后提供给调用者使用。这样,就解决了之前业务层会实例化持久层,显示层会创建业务层对象导致类和类之间耦合度变高的情况。
这里,由于之前编写的模拟业务层和模拟显示层都是可以反复被利用的,所以这里选择编写一个 BeanFactory 来创建可重用的组件。
这里提一下计算机语言中的 Bean:
在计算机语言中,Bean 指可以重用的组件。
JavaBean 值用 Java 语言编写的可重用的类。
2.2.1 独立出配置文件
由于是使用工厂类创建业务层和持久层的可重用组件,我们将业务层和持久层的实现类写入配置文件:
accountService=com.selflearning.spring.service.impl.AccountServiceImpl
accountDao=com.selflearning.spring.dao.impl.AccountDaoImpl
2.2.2 使用工厂模式进行解耦
- 创建好配置文件之后,创建 factory 文件,在该文件夹下创建工厂类:
package com.selflearning.spring.factory;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* 一个创建 Bean 对象的工厂
* <p>
* Bean:在计算机用语中,有可重用组件的意思。
* JavaBean:用Java语言编写的可重用组件。
* JavaBean > 实体类(可重用组件的一部分)
* 它就是创建我们的 service 和 dao 对象的。
* <p>
* 第一个:需要一个配置文件来配置我们的service 和dao
* 配置内容:唯一标志=全限定类名 (key=value)
* 第二个:通过读取配置文件中配置文件的内容,反射创建对象
* 配置文件可以是 xml 也可以是 properties
*/
public class BeanFactory {
// 定义一个 properties 对象
private static Properties props;
// 定义一个 Map,用于存放我们要创建的对象。成为容器。
private static Map<String, Object> beans;
// 使用静态代码为 Properties 对象赋值
static {
try {
// 实例化 Properties 对象
props = new Properties();
// 获取 Properties 文件的流对象
InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("Bean.properties");
props.load(in);
// 实例化容器
beans = new HashMap<>();
// 取出配置文件中所有的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);
}
} catch (Exception e) {
throw new ExceptionInInitializerError("初始化 properties 失败");
}
}
/**
* 根据 Bean 的名称获取 Bean 对象
* @param beanName
* @return
public static Object getBean(String beanName) {
Object bean = null;
try {
String beanPath = props.getProperty(beanName);
bean = Class.forName(beanPath).newInstance();
} catch (InstantiationException | ClassNotFoundException | IllegalAccessException e) {
e.printStackTrace();
}
return bean;
}
**/
/**
* 这里我们发现,我们每调用一次工厂类,都会创建一个对象。由于并没有在类中
* 创建静态的属性,所以不会出现多线程共同调用同一个对象而导致数据发生改变的
* 情况,所以可以将工厂类创建的对象改成单例。
* 由于创建的单例对象,一段时间不使用,会被 java 的垃圾回收机制回收,所以
* 当单例对象创建完毕,必须将其存起来。
*/
public static Object getBean(String name) {
return beans.get(name);
}
}
- 然后,修改业务层和显示层的调用者代码:
- 业务层
package com.selflearning.spring.service.impl;
import com.selflearning.spring.dao.IAccountDao;
import com.selflearning.spring.dao.impl.AccountDaoImpl;
import com.selflearning.spring.factory.BeanFactory;
import com.selflearning.spring.service.IAccountService;
/**
* 账户的业务层实现类
*/
public class AccountServiceImpl implements IAccountService {
// private IAccountDao accountDao = new AccountDaoImpl();
private IAccountDao accountDao = (IAccountDao) BeanFactory.getBean("accountDao");
@Override
public void saveAccount() {
accountDao.saveAccount();
}
}
- 显示层
package com.selflearning.spring.ui;
import com.selflearning.spring.factory.BeanFactory;
import com.selflearning.spring.service.IAccountService;
import com.selflearning.spring.service.impl.AccountServiceImpl;
/**
* 模拟一个表现层,用于调用业务层
*/
public class Client {
public static void main(String[] args) {
// IAccountService as = new AccountServiceImpl();
IAccountService as = (IAccountService) BeanFactory.getBean("accountService");
as.saveAccount();
}
}