文章目录
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(); } }
-