0%

通过解耦合来了解IOC

这段时间在学习SSM框架 , 今天开始弄Spring , 看了一下Spring的一些相关介绍 , 了解到Spring的核心是IOC(控制反转) 和 AOP (切面编程).

  1. IOC(控制反转) 能干什么东西 ?
    第一次看到这个有点懵逼 , 晚上找了点资料 , 有点理解, 这个东西 , 简单来说就是解决程序之间的强耦合关系 , 也就是松耦.

  2. 代码的耦合又是什么 ?
    代码的耦合度过高就会造成 , 项目维护困难 , 在某一个点上进行修改, 会对一系列的代码造成影响 , 这个就是代码的耦合

耦合解读

举一个比较简单的例子

我们在使用JDBC连接数据库 :

1
2
3
4
5
6
7
// 1. 首先注册驱动
DriverManager.registerDriver(new com.mysql.jdbc.Driver());
// 2. 获取连接
Connection conn = DriverManager.getConnection("url","root","root");
// 3. 获取预处理sql语句对象
// 4. 获取查询结果集
// 5. 关闭连接

这个步骤下来 , 没错吧 . 可是 , 如果mysql-jdbc这个包没有导入的情况下 , 直接在预编译阶段就会直接显示异常 . 程序直接运行不了.

这个就是我们所说的代码的耦合性 , 在一个地方修改后 , 会引起一系列的问题.

可能有些人看到到这里会有些奇怪 , 为什么我这里写注册驱动是用的DriverManager.registerDriver(new com.mysql.jdbc.Driver());
这个就是以前注册驱动的方法 , 至于为什么会修改 , 想必原因也都清楚了吧 . 然后我们换用现在的类加载器的方法来注册驱动重新跑一遍试试:

1
2
3
4
5
6
7
// 1. 首先注册驱动
Class.forName("com.mysql.jdbc.Driver");
// 2. 获取连接
Connection conn = DriverManager.getConnection("url","root","root");
// 3. 获取预处理sql语句对象
// 4. 获取查询结果集
// 5. 关闭连接

大家都发现了 , 换成这种后 , 由于我们注册驱动的那串关键字换成了字符串 , 所以在编译阶段必不会出现异常了 , 然后我们跑一遍会发现 , 其实还是会报错的 ClassNotFoundException , 这是由于我们把jar包给删除的原因

但是这样 , 也就解决我们代码的耦合 , 修改代码 , 并不会造成其他代码块的问题 (预编译阶段)

也就是说 , 通过new关键字 , 会使我们的代码的耦合程度提高 , 需要解耦合 使用 反射就可以了

但是 , 我们使用Class.forName()类加载器来反射创建的时候 , 由于字符串固定!! 代码的变通性又降低

一旦 ,我们更换数据库 , 就又需要修改源码
其实这个问题挺好解决 , 我们可以增加一个配置文件 , 然后通过读取配置文件的方式来修改.

自定义工厂模式解耦合

这里我们来简单模拟一下我们平时不用框架来开发web .

假定需要完成一个用户注册

这里我们创建一个主函数模拟前台页面 , 创建一个services业务层 , 创建一个Dao 持久层.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

/**
* 模拟前台注册页面
*/
Class register{

// services全局变量
private UserServices userservices = new UserServicesImpl();

public static void main(String[] args){
User user = new User();
user.setName("weison");
// 调用事务层保存
userservices.save(user);
}
}

/**
* User事务层
*/
Class UserServicesImpl implements UserServices{

// userDao 全局变量
private UserDao userdao = new UserDaoImpl();

@Override
public void save(User user){
userdao.save(user);
}
}

/**
* User 持久层
*/
Class UserDaoImpl implements UserDao{

@Override
public void save(User user){
System.out.println(user.getName()+" 注册成功");
}

}

可以发现 , 在显示层 , 和 事务层中, 我们都创建了一个私有全局变量 , 来完成接下来的事务操作.

那么这里的问题就显而易见了 , 假定我们修改了Dao或者修改了services , 都会造成在前台页面中无法运行的情况 , 也就是编译期的错误.

解决办法也很简单 , 就和前面分析的一样 , 使用 反射和配置文件来完成.

创建一个配置文件bean.properties , 用key=value的形式配置 , key为变量名 , value为全限定类名

1
2
userservices=com.weison.services.impl.UserServicesImpl
userdao=com.weison.dao.impl.UserDaoImpl

创建一个工厂类 , 用于生产获取对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

/**
* 工厂类 用于获取变量
*/
public class BeanFactory{
// 创建静态变量 , 以便于在静态代码块中执行
private static Properties prop;

// 使用静态代码块完成读取配置
static {
try {
//实例化对象
prop = new Properties();
//获取properties文件的流对象
InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
prop.load(in);
}catch(Exception e){
throw new ExceptionInInitializerError("初始化properties失败!");
}
}

/**
* 根据传入的变量名获取对象
*/
public static Object getBean(String beanName){
Object bean = null;
try {
String beanPath = prop.getProperty(beanName);
// System.out.println(beanPath);
bean = Class.forName(beanPath).newInstance();//每次都会调用默认构造函数创建对象
}catch (Exception e){
e.printStackTrace();
}
return bean;
}
}

好了 , 这里就完成工厂类的创建 , 然后我们就可以在之前的代码中做修改

1
2
3
4
5
6
7
8

// 显示层 main中
private UserServices userservices = (UserServices)BeanFactory.getBean("userservices");


// 事务层 services中
private UserDao userdao = (UserDao)BeanFactory.getBean("userdao");

可以看到 , 这样操作后 , 我们就算进行修改或者删除 , 都不会对我们的代码预编译的结果造成影响.

单例模式

但是这里又有一个问题出现 , 这里我在main中进行修改 :

1
2
3
4
for(int i=0;i<5;i++) {
UserServices us = (UserServices) BeanFactory.getBean("userservices");
System.out.println(as);
as.saveAccount();

运行结果

runtest

可以看到 , 这不是我们所希望的结果 ,因为循环五次获取对象, 获取到了五个不同的对象, 这对我们的内存是一个很大的浪费, 由于java的内存回收机制, 这些对象的内存需要在很长一段时间后才会被回收.

并且, 我们可以判断出 , 我们内部维持的这些变量都是极少或者不会出现更改的.

这里我们就可以对工厂类进行修改 , 给对象的获取给定一个固定的值 . 让我们传入参数 , 工厂每次返回的都是同一个对象 .

那么 , 我们需要怎么给工厂类进行修改 ? 直接添加成员变量么 , 这个是可行的 , 但是 , 一旦工厂类需要生产的对象多了起来 , 或者后期需要添加新的生产对象的时候 , 我们就需要再次修改源码进行成员变量的添加 , 这并没有从根本上解决问题 .

其实 , 我们可以在工厂类中 , 创建一个Map集合 , 然后以索引名(变量名)也就是配置文件的key , 作为key , 通过反射创建出来的对象作为value , 这样 , 在工厂类中 , 每个对应的对象都只会维持一份, 再次获取都会从集合中进行获取 .

对 工厂类进行修改 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//定义一个Map,用于存放我们要创建的对象。我们把它称之为容器
private static Map<String,Object> beans;

// 重构初始化静态代码块
//使用静态代码块为Properties对象赋值
static {
try {
//实例化对象
props = new Properties();
//获取properties文件的流对象
InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
props.load(in);
//实例化容器
beans = new HashMap<String,Object>();
//取出配置文件中所有的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的名称获取对象
* @param beanName
* @return
*/
public static Object getBean(String beanName){
return beans.get(beanName);
}

我们再次进行测试 :
beantest

可以发现 , 这里输出的就全都是同一个变量了 .

到这里 , 如果有用过Spring的同学可以发现 , 这个和Spring的对象获取很相似了

没错 , 其实spring内部也就是靠工厂模式和单例设计模式来完成IOC的 .

IOC解读

像我们家里如果有用不着的电器商品啊 什么的 , 如果想卖掉 , 肯定不是我们直接去找卖家吧 , 我们都是直接去找 中介出售 , 或者挂在二手交易网 , 让这个三方平台来完成销售 , 然后需要买的人, 他们就会通过这些三方平台来完成购买 , 而我们(卖家) 和 买家 之间 从始至终都没有进行过联系 .
这个和我们开发设计代码有一定到的相似性 .

  • 在最开始我们编写代码 :
    我们(卖家)就像声明变量 , 然后要去自己找对应的 对象引用(买家).
  • 然后有了二手交易平台(工厂类)介入:
    我们(卖家)就只要直接去找二手交易平台(工厂类) , 把我们需要或者出售的商品告诉它 , 然后工厂类就会帮我们完成操作 , 最后把需要的东西给我们.

IOC的实现原理

通过上文我们知道,IOC更多的是思想性的,具体的实现可以说是DI,或者换句话说:在系统运行的过程中,如何动态的向某个对象提供它所需要的对象,就是IOC容器通过依赖注入的方式来实现的。

在Spring中,Spring是一个对象工厂,当对象A需要对象B的时候,会有容器提供,程序员不用关心对象的创建只需要说明对象之间的互相依赖关系即可,之间的协作均有容器控制。

Spring容器或者工厂通过反射来创建需要的对象,这里我们需要了解的就是工厂设计模式和反射。我们通过配置文件将需要生产的对象的信息进行定义,在运行时候,Spring工厂会通过反射生成配置文件定义的类的对应的对象。