Spring学习(2)-程序间耦合和工厂模式解耦

程序的耦合及解耦


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 > 实体类

这个工厂就是创建我们的servicedao对象的。使用工厂解耦需要

  • 第一个:需要一个配置文件来配置我们的servicedao的类
    配置的内容:唯一标识=全限定类名(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("保存了账户.");
    }
}

使用工厂生产对象的好处:降低了程序间的耦合,便于管理、维护。在使用的时候,直接拿过来用就好了。

在这里插入图片描述

单例对象与多例对象

单例:从始至终只有一个对象;

在多个线程、多个对象访问时,如果对象的实例只有一个,在操作类成员的时候,类成员就会发生变化,出现线程问题。多例对象就没有这个问题,每次都会初始化。

多例:每次会重新创建一个对象;

上述工厂类解耦中还存在一些问题:

在这里插入图片描述

servicedao层不会存在单例对象的线程问题,因为可以将需要类成员变量,变成方法里的局部变量,单例对象每次也会初始化类成员了。由于很少会有需要改变的类成员,所以对象可以是单例对象,效率更高。

基于以上,可以对工厂类进行改造。下面这行代码:newInstance每次都会调用默认构造函数创建对象,效率较低,我们并不需要。我们只需要使用一次newInstance创建对象,将对象保存起来,如果不保存起来,容易出现长期不使用对象,被java的垃圾回收机制给回收,下次使用就找不到了。

bean  = Class.forName(beanPath).newInstance();//反射方式创建对象,newInstance每次都会调用默认构造函数创建对象

4.控制反转-Inversion Of Control

根据以上单例、多例的分析,改造的工产类生产的对象保存在哪?

分析:由于我们是很多对象,肯定要找个集合来存。这时候有 MapList 可以选择。
到底选 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:它是用于读取注解创建容器的。

核心容器的两个接口引发出的问题:

  1. ApplicationContext:单例对象适用 。常采用此接口
    它在构建核心容器时,创建对象采取的策略是采用立即加载的方式。也就是说,只要一读取完配置文件马上就创建配置文件中配置的对象。
  2. BeanFactory:多例对象使用。
    它在构建核心容器时,创建对象采取的策略是采用延迟加载的方式。也就是说,什么时候根据id获取对象了,什么时候才真正的创建对象。
//--------BeanFactory----------
Resource resource = new ClassPathResource("bean.xml");
BeanFactory factory = new XmlBeanFactory(resource);
IAccountService as  = (IAccountService)factory.getBean("accountService");
System.out.println(as);

推荐阅读


欢迎点赞评论,指出不足,笔者由衷感谢o!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值