由浅入深理解 IOC 和 DI
2020-08-31 14:22
281 查看
[TOC]
由浅入深理解 IOC 和 DI
开闭原则 OCP(Open Closed Principle)
- 对扩展开放,对修改封闭。
修改一处代码可能会引起其他地方的
bug
,最好的方式就是新增业务模块/类代替原来的业务模块/类,使出现bug
的几率变小。
面向抽象编程
- 只有面向抽象编程,才能够逐步实现开闭原则。 面临的两个问题: 统一方法的调用。
- 统一对象的实例化。
-
接口(
interface)
abstract)
-
面向抽象 -> OCP -> 可维护的代码
逐步理解实现 IOC 和 DI 的过程(LOL Demo 示例)
比较尴尬的编写程序添加需求/更改需求的做法
程序示例:
各英雄类
/** * <p> * Camille 英雄 * </p> * * @author 踏雪彡寻梅 * @version 1.0 * @date 2020/7/28 - 10:21 * @since JDK1.8 */ public class Camille { public void q() { System.out.println("Camille Q"); } public void w() { System.out.println("Camille W"); } public void e() { System.out.println("Camille E"); } public void r() { System.out.println("Camille R"); } } /** * <p> * Diana 英雄 * </p> * * @author 踏雪彡寻梅 * @version 1.0 * @date 2020/7/28 - 10:00 * @since JDK1.8 */ public class Diana { public void q() { System.out.println("Diana Q"); } public void w() { System.out.println("Diana W"); } public void e() { System.out.println("Diana E"); } public void r() { System.out.println("Diana R"); } } /** * <p> * Irelia 英雄 * </p> * * @author 踏雪彡寻梅 * @version 1.0 * @date 2020/7/28 - 10:16 * @since JDK1.8 */ public class Irelia { public void q() { System.out.println("Irelia Q"); } public void w() { System.out.println("Irelia W"); } public void e() { System.out.println("Irelia E"); } public void r() { System.out.println("Irelia R"); } }
选择英雄释放技能 main 函数
/** * <p> * 传统编写程序添加需求/更改需求的做法 * </p> * * @author 踏雪彡寻梅 * @version 1.0 * @date 2020/7/28 - 10:01 * @since JDK1.8 */ public class Main { public static void main(String[] args) { // 选择英雄 String name = Main.getPlayerInput(); // 新增英雄时需要改此处代码 switch (name) { case "Diana": Diana diana = new Diana(); diana.r(); break; case "Irelia": Irelia irelia = new Irelia(); irelia.r(); break; case "Camille": Camille camille = new Camille(); camille.r(); break; default: break; } } private static String getPlayerInput() { Scanner scanner = new Scanner(System.in); System.out.println("请输入一个英雄的名称: "); return scanner.nextLine(); } }
从上面的代码,可以看出以下几点:
-
当增加新的英雄时,需要修改
switch处的代码,增加新的
case。
case中的代码都存在着
new一个某某英雄,并且调用了释放技能的方法。
new是不好的,因为真实项目中类和类的依赖是非常之多的,这个类依赖那个类,那个类又依赖了另一个类。
new操作,代码间的耦合度将变得非常高,当某个类的需求产生变化的时候,一旦修改代码,其他依赖这个类的地方就很有可能引起很多
bug,同时依赖的地方也可能需要修改大量的代码。
bug的原因。
IOC和
DI的目的。
使用 interface 接口统一方法的调用
程序示例:
英雄技能接口类
/** * <p> * 英雄技能接口类 * </p> * * @author 踏雪彡寻梅 * @version 2.0 * @date 2020/7/28 - 10:31 * @since JDK1.8 */ public interface ISkill { void q(); void w(); void e(); void r(); }
各英雄类
/** * <p> * Camille 英雄 * </p> * * @author 踏雪彡寻梅 * @version 2.0 * @date 2020/7/28 - 10:21 * @since JDK1.8 */ public class Camille implements ISkill { @Override public void q() { System.out.println("Camille Q"); } @Override public void w() { System.out.println("Camille W"); } @Override public void e() { System.out.println("Camille E"); } @Override public void r() { System.out.println("Camille R"); } } /** * <p> * Diana 英雄 * </p> * * @author 踏雪彡寻梅 * @version 2.0 * @date 2020/7/28 - 10:00 * @since JDK1.8 */ public class Diana implements ISkill { @Override public void q() { System.out.println("Diana Q"); } @Override public void w() { System.out.println("Diana W"); } @Override public void e() { System.out.println("Diana E"); } @Override public void r() { System.out.println("Diana R"); } } /** * <p> * Irelia 英雄 * </p> * * @author 踏雪彡寻梅 * @version 2.0 * @date 2020/7/28 - 10:16 * @since JDK1.8 */ public class Irelia implements ISkill { @Override public void q() { System.out.println("Irelia Q"); } @Override public void w() { System.out.println("Irelia W"); } @Override public void e() { System.out.println("Irelia E"); } @Override public void r() { System.out.println("Irelia R"); } }
选择英雄释放技能 main 函数
/** * <p> * 使用 interface 统一方法的调用 * </p> * * @author 踏雪彡寻梅 * @version 2.0 * @date 2020/7/28 - 10:29 * @since JDK1.8 */ public class Main { public static void main(String[] args) throws Exception { ISkill iSkill; // 选择英雄 String name = Main.getPlayerInput(); // 新增英雄时也需要改此处代码 // 这个 switch 提取成一个方法(例如工厂模式)之后,这里的代码就会变得简单 // 只有一段代码不负责对象的实例化,即没有 new 的出现,才能保持代码的相对稳定,才能逐步实现 OCP switch (name) { case "Diana": iSkill = new Diana(); break; case "Irelia": iSkill = new Irelia(); break; case "Camille": iSkill = new Camille(); break; default: throw new Exception(); } // 调用技能,现在这个版本使用接口统一了方法的调用,但还不能统一对象的实例化 // 统一了方法的调用是意义非常重大的 // 真实项目中,方法的调用可能非常的多或者复杂,这种情况下把方法的调用统一起来,集中在一个接口的方法上面,这个意义非常重大 iSkill.r(); } private static String getPlayerInput() { Scanner scanner = new Scanner(System.in); System.out.println("请输入一个英雄的名称: "); return scanner.nextLine(); } }
从以上代码示例可得出以下几点:
-
单纯的
interface可以统一方法的调用,但是它不能统一对象的实例化。 统一了方法的调用是意义非常重大的。 真实项目中,方法的调用可能非常的多或者复杂,这种情况下把方法的调用统一起来,集中在一个接口的方法上面,这个意义非常重大。
new对象这个操作变得更加的抽象,而不是具体。
-
所以仅仅达到统一方法的调用还不足够,还需要达到统一对象的实例化。
new的出现,才能保持代码的相对稳定,才能逐步实现
OCP。(表象)
-
实质: 一段代码如果要保持稳定,就不应该负责对象的实例化。
使用工厂模式把对象实例化的过程隔离
三种子模式:
简单工厂模式 对生产的对象的一种抽象。
-
对工厂的一种抽象。
使用简单工厂模式把对象实例化的过程转移到其他的代码片段里:
-
程序示例:
英雄技能接口类
/** * <p> * 英雄技能接口类 * </p> * * @author 踏雪彡寻梅 * @version 3.0 * @date 2020/7/28 - 10:31 * @since JDK1.8 */ public interface ISkill { void q(); void w(); void e(); void r(); }
各英雄类
/** * <p> * Camille 英雄 * </p> * * @author 踏雪彡寻梅 * @version 3.0 * @date 2020/7/28 - 10:21 * @since JDK1.8 */ public class Camille implements ISkill { @Override public void q() { System.out.println("Camille Q"); } @Override public void w() { System.out.println("Camille W"); } @Override public void e() { System.out.println("Camille E"); } @Override public void r() { System.out.println("Camille R"); } } /** * <p> * Diana 英雄 * </p> * * @author 踏雪彡寻梅 * @version 3.0 * @date 2020/7/28 - 10:00 * @since JDK1.8 */ public class Diana implements ISkill { @Override public void q() { System.out.println("Diana Q"); } @Override public void w() { System.out.println("Diana W"); } @Override public void e() { System.out.println("Diana E"); } @Override public void r() { System.out.println("Diana R"); } } /** * <p> * Irelia 英雄 * </p> * * @author 踏雪彡寻梅 * @version 3.0 * @date 2020/7/28 - 10:16 * @since JDK1.8 */ public class Irelia implements ISkill { @Override public void q() { System.out.println("Irelia Q"); } @Override public void w() { System.out.println("Irelia W"); } @Override public void e() { System.out.println("Irelia E"); } @Override public void r() { System.out.println("Irelia R"); } }
生产英雄的工厂类
/** * <p> * 英雄工厂类,生产或实例化英雄类,把对象实例化的过程隔离 * </p> * * @author 踏雪彡寻梅 * @version 3.0 * @date 2020/7/28 - 21:11 * @since JDK1.8 */ public class HeroFactory { /** * 简单工厂实例化英雄类 * * @param name 英雄名称 * @return 返回英雄名称对应的实例 */ public static ISkill getHero(String name) throws Exception { ISkill iSkill; // 变化是导致代码不稳定的本质原因 // 所有的变化最终其实都要交给不同的对象去处理,当业务或用户的输入有了变化的时候,必须要创建不同的对象去响应这些变化 // 这里的变化: 用户的输入,选择英雄导致的不稳定,根据用户的输入实例化不同的对象 // 也例如改动程序使用的数据库,从 MySQL 更改为 Oracle // 如何消除这个变化? // 思考: // 1. 这里是用户只能够输入一个字符串,把输入的字符串转换为一个对象 // 2. 但是如果用户能够直接输入一个对象传给程序,这个 switch 就可以被干掉(使用反射解决,把输入的字符串转换为一个对象) switch (name) { case "Diana": iSkill = new Diana(); break; case "Irelia": iSkill = new Irelia(); break; case "Camille": iSkill = new Camille(); break; default: throw new Exception(); } return iSkill; } }
选择英雄释放技能 main 函数
/** * <p> * 使用简单工厂模式把对象实例化的过程转移到其他的代码片段里(IOC 的雏形) * </p> * * @author 踏雪彡寻梅 * @version 3.0 * @date 2020/7/28 - 21:06 * @since JDK1.8 */ public class Main { public static void main(String[] args) throws Exception { // 选择英雄 String name = Main.getPlayerInput(); // 调用工厂方法,这里把 new 的操作干掉了,这里的代码已经相对稳定了,新增英雄时这里的代码不需要再更改,只需要更改工厂方法的代码 // 对于 main 方法而言,它实现了 OCP,而工厂方法中的代码还没有实现 OCP // 虽然这里的代码已经相对稳定了,但是还引用着 HeroFactory 工厂类,对于这行代码,还不是非常稳定,还存在着可能更换修改的可能 // 例如说: HeroFactory 的 getHero 方法是个实例方法,那么 HeroFactory 也需要 new 出来,这种情况下会存在着修改代码的可能 // 如果业务逻辑足够复杂,可能存在很多这种工厂类,这样看起来对于工厂类而言,需求变更时还是需要改动很多代码 // 当然也可以使用抽象工厂将工厂抽象化使这里变得稳定起来,但这里不演示了,这里只是演示一个如何隔离变化的过程,所以假设这里是稳定的,是一个超级工厂,能够生产项目的各种对象 // 当假设有一个超级的工厂之后,这个工厂可以兼容整个项目的工厂,把整个项目的所有的变动都封装到一起,从而保证除了这个超级工厂之外的代码都是稳定的,这样这个工厂就有了意义 // 其实 IOC 也就是相当于一个非常大的容器一样,把所有的变化都集中到了一个地方,写其他的代码就会变得非常容易,不再需要在整个项目中到处更改代码,如果出现了变化,只需要让容器去负责改变即可 // spring ioc 中的 ApplicationContext 就类似于这个超级工厂,通过 ApplicationContext 可以获取各种各样的对象,不过 ApplicationContext 在 spring 中给的是一个接口,即抽象工厂模式 // 需要注意的是: 生产对象只是 IOC 的一部分,不是 IOC 的全部 ISkill iSkill = HeroFactory.getHero(name); // 调用技能 iSkill.r(); } private static String getPlayerInput() { Scanner scanner = new Scanner(System.in); System.out.println("请输入一个英雄的名称: "); return scanner.nextLine(); } }
从以上例子可得出以下几点:
-
代码中总是会存在不稳定,要尽可能地隔离这些不稳定,保证其他的代码是稳定的。隔离不稳定其实就是在隔离变化。
其实
IOC就是将这些不稳定(变化)给封装、隔离到了一块,保证其他地方的代码是稳定的。
MySQL更换到
Oracle,如果将这个变化提取到配置文件中,那么配置文件的变化是允许的,并不违反
OCP。 配置文件是属于系统外部的,而不属于代码本身。(这里的配置文件也可以理解为用户的输入,把需求的变化隔离到了配置文件中)
-
那么如何消除这些变化呢?
在上面的示例中,用户只能输入一个字符串,然后在工厂方法内去判断用户的输入的变化,创建不同的对象去响应这些变化。
switch代码。
switch,使这里的代码变得更加简单,更加稳定。
使用反射隔离工厂中的变化,让用户直接输入一个对象
对于这个版本,只有工厂类发生了变动,所以只展示工厂类的代码和 main 函数的代码
程序示例:
生产英雄的工厂类
/** * <p> * 英雄工厂类,生产或实例化英雄类,把对象实例化的过程隔离 * </p> * * @author 踏雪彡寻梅 * @version 4.0 * @date 2020/7/28 - 21:11 * @since JDK1.8 */ public class HeroFactory { /** * 简单工厂实例化英雄类 * * @param name 英雄名称 * @return 返回英雄名称对应的实例 */ public static ISkill getHero(String name) throws Exception { // 使用反射隔离工厂中的变化,让用户直接输入一个对象,即把用户输入的字符串转换为一个对象 // 反射的作用: 动态的创建对象 // 根据输入的英雄名称获取元类 // 类是对象的抽象,描述对象 // 元类是类的抽象,是对类的描述 // 需要注意在名称前加上包路径 cn.xilikeli.lol.v4.hero. name = "cn.xilikeli.lol.v4.hero." + name; Class<?> classA = Class.forName(name); // 通过元类实例化对应的实例对象 // 注意点: java8 之后 newInstance 已经废弃 // 新版本中使用 classA.getDeclaredConstructor().newInstance() Object obj = classA.newInstance(); // 强制转型返回 return (ISkill) obj; } }
选择英雄释放技能 main 函数
/** * <p> * 使用反射隔离工厂中的变化,让用户直接输入一个对象,即把用户输入的字符串转换为一个对象 * </p> * * @author 踏雪彡寻梅 * @version 4.0 * @date 2020/7/28 - 22:26 * @since JDK1.8 */ public class Main { public static void main(String[] args) throws Exception { // 选择英雄 String name = Main.getPlayerInput(); ISkill iSkill = HeroFactory.getHero(name); // 调用技能 iSkill.r(); } private static String getPlayerInput() { Scanner scanner = new Scanner(System.in); System.out.println("请输入一个英雄的名称: "); return scanner.nextLine(); } }
从以上示例可得出以下几点:
-
在使用了反射机制之后,已经消除了所有的变化,不管输入的是什么,代码都不需要再做更改了,代码已经变得非常稳定了。
Spring内部其实也是使用了类似现在这种工厂模式 + 反射的机制,但是要比现在实现的这种形式更加地完善更加地聪明。 现在这种形式,每次输入都进行一次反射是性能比较低的,因为频繁地反射会使性能变低。
Spring中取到或实例化一个对象之后,会把这个对象放到它的缓存中去,下次要再取或创建相同的对象的时候,不会进行反射,而是从缓存中拿(和
DI有关系)。
IOC和
DI。
-
现在这个版本并没有运用到任何
IOC和
DI的原理,只是让代码变得非常稳定。
IOC是控制反转,现在这里并没有反转,同时也没有注入,现在只是实现了
OCP。
OCP,那么还需要
IOC和
DI干嘛?
-
因为现在的实现使用起来还不方便,是一个正向的思维。每次创建一个对象都需要引入这个工厂类调用其方法。也就是说工厂的方式在实现的逻辑中是正向的创建对象,而
IOC是反向的,是容器根据需求主动注入的。
IOC和
DI需要做的事情。
IOC和
DI的雏形至此也就出来了。
IOC和
DI的体现,把对象的控制权交给了容器,即控制反转;获取对象时只需要直接使用它即可,容器会自动把这个对象创建好给传入进来,即依赖注入。
IOC简单理解就是
工厂模式 + 反射机制 + 配置文件组成了一个大的容器,我们需要什么就配置什么,容器就会创建对象,把创建好的对象提供给我们,这个过程中我们没有感受到创建对象的正向过程,而是感受到使用的对象是容器给我们的,这是一个反向的过程,也就是控制权由我们反转到了容器中,容器掌控着创建对象的权利,即控制反转。
IOC和
DI到底是个什么东西了,接下来再对这两个东西解释一下,以便理解地更加深刻。
IOC/DI/DIP
DIP(Dependency Inversion Principle,依赖倒置)
高层模块不应该依赖低层模块,两者都应该依赖抽象。 高层: 抽象,抽象也就是站在更高的角度概括具体。- 低层: 具体的实现。
new一个对象,即依赖了一个具体;而倒置就是不依赖具体而是反过来依赖一个接口(抽象)。
DI
-
对象与对象之间的相互作用必定是要产生依赖的,这个是不可避免的,关键是产生依赖的方式是多种多样的,比如
new的方式,不过这个方法不好,因为是一个具体实例化的过程,如果依赖的类的代码改变了,使用这个依赖的类的地方就会变得不稳定。
new的方式,而是让容器(容器可以理解为在系统的最上层)把需要依赖的类的抽象(例如接口)的实现给注入进来,虽然也产生了依赖,但是依赖的形式是不同的,是注入进来的,并且注入进来的类是抽象的实现,依赖只是依赖抽象的接口,相比
new来说产生的依赖不这么具体。
属性注入
public class A { private IC ic; // 属性注入,容器在实例化 A 的时候会 set 一个 ic 进来 public void setIc(IC ic) { this.ic = ic; } }
构造注入
public class A { private IC ic; // 构造注入,容器在实例化 A 的时候会给构造函数传一个 ic 进来 public A(IC ic) { this.ic = ic; } }
接口注入(用的较少)
-
容器在创建一个对象实例的时候可以把这个对象依赖的对象实例注入进去。相当于我们自己写代码的时候
new对象时把一个对象传进去或赋值以及可以调用
set方法传入一个对象或赋值。
-
容器其实是在装配这一个个的对象。
即各个类依赖了各个类,他们彼此之间的装配不是由他们在代码中
new出来的。而是全部交给容器,由容器来装配。
IOC
IOC本身概念非常抽象和模糊,只是展示了一种思想,并没有给出具体的实现。
DI可以看做
IOC的一个具体的实现。
DI的角度理解
IOC当在一个类(这里用
A表示)中使用
new来创建一个需要的对象时,主控类为
A类。
DI之后,有了容器的概念,此时主控方为容器,主控的地方不再是
A类了,这个其实就是实现了控制反转,全部交给了容器去控制。
-
整个程序运行的控制权是谁的?
其实本质上还是由程序员决定的。
如果需求固定不变,就没有什么问题。
new了一个什么,要改成
new一个新的什么)。
MySQL,产品经理改成了
Oracle,程序员肯定要负责新增代码实现
Oracle的功能,但是至于应用程序用的是原来的
MySQL还是新加的
Oralce,现在不再由程序员去控制,而是产品经理去控制,控制权由产品经理决定,即控制反转。
-
积木生产厂家(程序员)
只负责生产一个个积木,不再负责积木的搭建。
相关文章推荐
- 关于IOC和DI的理解
- 对IOC/DI的理解(转载自开涛的博客)
- 我对IoC/DI的理解
- 深入理解DIP、IoC、DI以及IoC容器
- 关于IOC和DI的理解
- 深入理解DIP、IoC、DI以及IoC容器
- spring的IOC,DI,AOP理解
- Spring - IOC和DI的理解
- 深入理解DIP、IoC、DI以及IoC容器
- 对IOC于DI的一些理解
- 深入理解DIP、IoC、DI以及IoC容器
- 你怎么理解ioc/DI?
- 控制反转(IoC)与依赖注入(DI)——自己的理解
- IOC/DI的理解
- 关于IoC/DI的理解
- Spring IoC,DI,AOP的一点理解
- spring入门程序以及spring的优点是什么?spring的Ioc和DI的概念理解?
- IOC和DI本质理解
- Spring IoC(控制反转)和DI(依赖注入)的理解
- 浅谈对Spring IOC以及DI的理解