耦合与解耦Coupling and Decoupling2020-02-20

什么是耦合

JAVA中的异常,主要分为编译时异常和运行时异常两大类,先看一段简单的JDBC代码

public class JdbcDemo{
    public static void main(String[]args)throws Exception{
        //1.注册驱动
        DriverManager.registerDriver(new com.mysql.jdbc.Driver());
        //2.获取连接
        Connection conn = DriverManager.getconnection("jdbc:mysql://localhost:3306/jdbcDemo","root","yourPwd");
        //3.获取操作数据库的预处理对象
        PreparedStatement pstm = conn.prepareStatement("select*from account");
        //4.执行SOL,得到结果集
        ResultSet res = pstm.executeQuery();
        //5.遍历结果集
        while(res.next()){
            System.out.println(res.getString("name"));
        }
        //6.释放资源
        res.close();
        pstm.close();
        conn.close();
    }
}

平常我们在注册驱动这里,更多用的是导包的形式,而不是New一个对象,假如我们这里没有mysql的jar包,点击运行,会报一个编译时异常,这样会使得程序的独立性非常之差,得依赖于另一个类的存在才能运行。

这就是程序的耦合,简单来讲,耦合可以理解为程序间的依赖,除了上面的类之间的依赖,还有方法间的依赖

  1. 类之间的依赖
  2. 方法间的依赖

降低这种现象,则称为解耦,比如上面的注册驱动可以写成这样:

Class.forName("com.mysql.jdbc.Driver");

吧mysql这个jar包变成一个字符串,在运行时在forName这个函数去注册对应的类,这样即使没有该jar包,也不会报编译时异常,从而大大增加了程序的独立性

实际开发中我们要尽可能做到:编译时不依赖,运行时才依赖,因为程序里肯定有类之间的相互调用,所以我们避免不了耦合,消除不了耦合,只能做到降低

如何进行解耦

这里先讲一下类之间的解耦,方法的解耦后续在写一篇

解耦的思路:

  1. 使用映射来创建对象,而避免使用new关键字。

    采用映射来创建对象,依赖的是字符串,而用new来创建,则需要依赖具体的驱动类,但这样又产生了新的问题:

     当我们程序换驱动类时,必须一个个去修改字符串,修改的地方一多,就很繁琐了,这就引出了第二步
  2. 通过读取配置文件,来获取创建对象全限定类名

来通过一个小案例了解一下

在IDEA中创建一个maven工程,导入对应的maven依赖,然后创建对应的dao和service,同时包括servlet,像这样:

xTjRol

package com.karasawa.dao;

/**
 * @program: SpringJieOu
 * @description: 账户持久层实现类
 * @author: karasawa
 * @create: 2020-02-20 22f:17
 **/
public class AccountDaoImpl implements IAccountDao {

  public void saveAccount() {
    System.out.println("保存的账户");
  }
}
package com.karasawa.service;

import com.karasawa.dao.AccountDaoImpl;
import com.karasawa.dao.IAccountDao;

/**
 * @program: SpringJieOu
 * @description: 账户业务层实现类
 * @author: karasawa
 * @create: 2020-02-20 22:14
 **/
public class AccountServiceImpl implements IAccountService {

  private IAccountDao accountDao = new AccountDaoImpl();

  public void saveAccount(){
    accountDao.saveAccount();
  }
}
package com.karasawa.servlet;

import com.karasawa.service.AccountServiceImpl;
import javax.servlet.http.HttpServlet;

/**
 * @program: SpringJieOu
 * @description: 表现层测试调用业务层
 * @author: karasawa
 * @create: 2020-02-20 22:33
 **/

public class TestSaveAccount extends HttpServlet {

  public static void main(String[] args) {
    AccountServiceImpl s = new AccountServiceImpl();
    s.saveAccount();
  }
}

很明显,这里表现层依赖着业务层,业务层又依赖着持久层,整个代码有着很强的依赖性,我们按照上面的步骤来解决一下

第一步:需要一个配置文件来配置我们的service和dao配置的内容:以key = value形式

在名为factory的包下创建一个类BeanFactory,像这样:

OyImQv

配置文件可以是XML和properties,这里就用propertise,因为解析起来相比XML简单一些,可以减少不必要的知识学习成本

在resource下面创建一个Bean.propertise文件,像这样:

IcWT6P

accountService=com.karasawa.service.AccountServiceImpl
accountDao=com.karasawa.dao.AccountDaoImpl

第二步:通过读取配置文件中配置的内容,反射创建对象

先读取配置文件

package com.karasawa.factory;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/**
 * @program: SpringJieOu
 * @description: 一个创建Bean对象的工厂,Bean在计算机行业中有类似「重用」的意思
 * @author: karasawa
 * @create: 2020-02-20 22:43
 **/
public class BeanFactory {

  private static Properties prop;

  static {

    try {
      prop = new Properties();
      InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.propertise");
      assert in != null;
      prop.load(in);
    } catch (IOException e) {
      throw new ExceptionInInitializerError("初始化propertise失败");
    }
  }
}

嗯?你不是刚说不要用new关键字嘛,怎么还用new Propertise?

上面都说了,我们只能尽量减少耦合,并不能消除,依赖关系定会存在

注意一下在获取InputStream的时候我并没有使用FileInputStream(),这是因为我们web应用在上传到服务器的时候,绝对路径和相对路径(相对当前的开发环境)将变得不靠谱,这点在前面的博客讲过了,所以我在这里使用了类加载器。在try/catch里面,如果捕捉到异常,则初始化失败,ExceptionInInitializerError是一个linkageError,这个错误一出场,后面程序就不会再运行了

我们来创建反射对象,接着上面的文件

  public static Object getBean(String beanName){
    Object bean = null;
    try {
      String beanPath = prop.getProperty(beanName);
      bean = Class.forName(beanPath)
                  .getDeclaredConstructor().newInstance();
    } catch (
      ClassNotFoundException
      | NoSuchMethodException
      | InstantiationException
      | IllegalAccessException
      | InvocationTargetException e) {
      e.printStackTrace();
    }
    return bean;
  }

因为对象类型的不确定性,所以这里返回类型用了一个Object,整个过程呢,就叫工厂模式来创建对象,OK,我们工厂写好了,返回上面的小案例,做一下修改:

package com.karasawa.service;

import com.karasawa.dao.IAccountDao;
import com.karasawa.factory.BeanFactory;

/**
 * @program: SpringJieOu
 * @description: 账户业务层实现类
 * @author: karasawa
 * @create: 2020-02-20 22:14
 **/
public class AccountServiceImpl implements IAccountService {

//  private IAccountDao accountDao = new AccountDaoImpl();

  private IAccountDao accountDao = (IAccountDao) BeanFactory.getBean("accountDao");

  public void saveAccount() {
    accountDao.saveAccount();
  }
}
package com.karasawa.servlet;

import com.karasawa.factory.BeanFactory;
import com.karasawa.service.AccountServiceImpl;
import javax.servlet.http.HttpServlet;

/**
 * @program: SpringJieOu
 * @description: 表现层测试调用业务层
 * @author: karasawa
 * @create: 2020-02-20 22:33
 **/

public class TestSaveAccount extends HttpServlet {

  public static void main(String[] args) {

//    AccountServiceImpl s = new AccountServiceImpl();

    AccountServiceImpl s = (AccountServiceImpl) BeanFactory.getBean("accountService");
    s.saveAccount();
  }
}

运行后和之前的结果一样,是否能解耦呢?尝试看看

把IAccountService删掉(记得copy一份),然后运行servlet,可以看到运行起来了,当然,还是会报错,不过这会报的错是runtimeException了,也就是运行时的异常了,成功解耦!

可以了吗?其实代码还可以更好!!

我们知道,多例模式执行效率低,而单例模式有线程问题

回顾我们上面的案例,并没有线程问题,那我们案例用单例会比较好,执行效率高,修改一下servlet,循环多执行几次,再把对象输出,来看看三个是同一个对象还是不同对象

public class TestSaveAccount extends HttpServlet {

  public static void main(String[] args) {
    for (int i = 0; i < 3; i++) {
      AccountServiceImpl s = (AccountServiceImpl) BeanFactory.getBean("accountService");
      System.out.println(s);
    }


  }
}

运行的时候查看控制台

8kXi9E

看到三个首地址都是不一样的,确定了我们此时是多例模式

继续优化,思路是在BeanFactory初始化的时候,就把所有的类加载完成,放进一个Map里面,每次拿的时候再从map里返回实例就好了,修改代码如下:

public class BeanFactory {

  private static Properties prop;
  private static Map<String,Object> beans;

  static {

    try {
      prop = new Properties();
      beans = new HashMap<String,Object>();
      InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.propertise");
      assert in != null;
      prop.load(in);
      Enumeration keys = prop.keys();
      while (keys.hasMoreElements()){
        String key = keys.nextElement().toString();
        String beanPath = prop.getProperty(key);
        Object value = Class.forName(beanPath).getDeclaredConstructor().newInstance();
        beans.put(key,value);
      }
    } catch (IOException | ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
      throw new ExceptionInInitializerError("初始化propertise失败");
    }
  }

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

运行,查看控制台,输出三个首地址一样

0VuGUx

到这就初始了程序的耦合与解耦了,明天开始学习Spring,听说IOC和AOP是核心,得先看看耦合和解耦的知识,学Spring起来会轻松很多,就有了此笔记,冲冲冲,明天冲Spring!!