轻松理解 Java开发中的依赖注入(DI)和控制反转(IOC)

轻松理解 Java开发中的依赖注入(DI)和控制反转(IOC)

前言

关于这个话题, 网上有很多文章,这里, 我希望通过最简单的话语与大家分享.

依赖注入和控制反转两个概念让很多初学这迷惑, 觉得玄之又玄,高深莫测.

这里想先说明两点:

  1. 依赖注入和控制反转不是高级的,很初级,也很简单.
  2. 在JAVA世界,这两个概念像空气一样无所不在,彻底理解很有必要.

第一节 依赖注入 Dependency injection

这里通过一个简单的案例来说明.

在公司里有一个常见的案例: "把任务指派个程序员完成".

把这个案例用面向对象(OO)的方式来设计,通常在面向对象设计中,名词皆可设计为对象

这句话里"任务","程序员"是名词,所以我们考虑创建两个Class: Task 和 Phper (php 程序员)

Step1 设计

文件: Phper.java

package demo;
public class Phper {
 private String name;
 public Phper(String name){
 this.name=name;
 }
 public void writeCode(){
 System.out.println(this.name + " is writing php code");
 }
}

文件: Task.java

package demo;
public class Task {
 private String name;
 private Phper owner;
 public Task(String name){
 this.name =name;
 this.owner = new Phper("zhang3");
 }
 public void start(){
 System.out.println(this.name+ " started");
 this.owner.writeCode();
 }
}

文件: MyFramework.java, 这是个简单的测试程序.

package demo;
public class MyFramework {
 public static void main(String[] args) {
 Task t = new Task("Task #1");
 t.start();
 }
}

运行结果:

Task #1 started

hang3 is writing php code

我们看一看这个设计有什么问题?

如果只是为了完成某个临时的任务,程序即写即仍,这没有问题,只要完成任务即可.

但是如果同事仰慕你的设计,要重用你的代码.你把程序打成一个类库(jar包)发给同事.

现在问题来了,同事发现这个Task 类 和 程序员 zhang3 绑定在一起,他所有创建的Task,都是程序员zhang3负责,他要把一些任务指派给Lee4, 就需要修改Task的源程序, 如果没有Task的源程序,就无法把任务指派给他人. 而通常类库(jar包)的使用者通常不需要也不应该来修改类库的源码,如果大家都来修改类库的源码,类库就失去了重用的设计初衷.

我们很自然的想到,应该让用户来指派任务负责人. 于是有了新的设计.

Step2 设计:

文件: Phper.java 不变.

文件: Task.java

package demo;
public class Task {
 private String name;
 private Phper owner;
 public Task(String name){
 this.name =name;
 }
 public void setOwner(Phper owner){
 this.owner = owner;
 }
 public void start(){
 System.out.println(this.name+ " started");
 this.owner.writeCode();
 }
}

文件: MyFramework.java, 这是个简单的测试程序.

package demo;
public class MyFramework {
 public static void main(String[] args) {
 Task t = new Task("Task #1");
 Phper owner = new Phper("lee4");
 t.setOwner(owner);
 t.start();
 }
}

这样用户就可在使用时指派特定的PHP程序员.

我们知道,任务依赖程序员,Task类依赖Phper类,之前,Task类绑定特定的实例,现在这种依赖可以在使用时按需绑定,这就是依赖注入(DI).

这个例子,我们通过方法setOwner注入依赖对象,

另外一个常见的注入办法是在Task的构造函数注入:

public Task(String name,Phper owner){
 this.name = name;
 this.owner = owner;
 }

在Java开发中,把一个对象实例传给一个新建对象的情况十分普遍,通常这就是注入依赖.

Step2 的设计实现了依赖注入.

我们来看看Step2 的设计有什么问题.

如果公司是一个单纯使用PHP的公司,所有开发任务都有Phper 来完成,这样这个设就已经很好了,不用优化.

但是随着公司的发展,有些任务需要JAVA来完成,公司招了写Javaer (java程序员),现在问题来了,这个Task类库的的使用者发现,任务只能指派给Phper,

一个很自然的需求就是Task应该即可指派给Phper也可指派给Javaer.

Step3 设计

我们发现不管Phper 还是 Javaer 都是Coder(程序员), 把Task类对Phper类的依赖改为对Coder 的依赖即可.

这个Coder可以设计为父类或接口,Phper 或 Javaer 通过继承父类或实现接口 达到归为一类的目的.

选择父类还是接口,主要看Coder里是否有很多共用的逻辑代码,如果是,就选择父类

否则就选接口.

这里我们选择接口的办法:

  1. 新增Coder接口,
  2. 文件: Coder.java
package demo;
public interface Coder {
 public void writeCode();
}
  1. 修改Phper类实现Coder接口
  2. 文件: Phper.php
package demo;
public class Phper implements Coder {
 private String name;
 public Phper(String name){
 this.name=name;
 }
 public void writeCode(){
 System.out.println(this.name + " is writing php code");
 }
}
  1. 新类Javaer实现Coder接口
  2. 文件: Javaer.php
package demo;
public class Javaer implements Coder {
 private String name;
 public Javaer(String name){
 this.name=name;
 }
 public void writeCode(){
 System.out.println(this.name + " is writing java code");
 }
}
  1. 修改Task由对Phper类的依赖改为对Coder的依赖.
  2. 文件: Task.java
package demo;
public class Task {
 private String name;
 private Coder owner;
 public Task(String name){
 this.name =name;
 }
 public void setOwner(Coder owner){
 this.owner = owner;
 }
 public void start(){
 System.out.println(this.name+ " started");
 this.owner.writeCode();
 }
}
  1. 修改用于测试的类使用Coder接口:
package demo;
public class MyFramework {
 public static void main(String[] args) {
 Task t = new Task("Task #1");
 // Phper, Javaer 都是Coder,可以赋值
 Coder owner = new Phper("lee4");
 //Coder owner = new Javaer("Wang5");
 t.setOwner(owner);
 t.start();
 }
}

现在用户可以和方便的把任务指派给Javaer 了,如果有新的Pythoner加入,没问题.

类库的使用者只需让Pythoner实现(implements)了Coder接口,就可把任务指派给Pythoner, 无需修改Task 源码, 提高了类库的可扩展性.

回顾一下,我们开发的Task类,

在Step1 中与Task与特定实例绑定(zhang3 Phper)

在Step2 中与Task与特定类型绑定(Phper)

在Step3 中与Task与特定接口绑定(Coder)

虽然都是绑定, 从Step1,Step2 到 Step3 灵活性可扩展性是依次提高的.

Step1 作为反面教材不可取, 至于是否需要从Step2 提升为Step3, 要看具体情况.

如果依赖的类型是唯一的Step2 就可以, 如果选项很多就选Step3设计.

依赖注入(DI)实现了控制反转(IoC)的思想.

看看怎么反转的?

Step1 程序

this.owner = new Phper("zhang3");

Step1 设计中 任务Task 依赖负责人owner, 就主动新建一个Phper 赋值给owner,

这里是新建,也可能是在容器中获取一个现成的Phper,新建还是获取,无关紧要,关键是赋值, 主动赋值. 这里提一个赋值权的概念.

在Step2 和 Step3, Task 的 owner 是被动赋值的.谁来赋值,Task自己不关心,可能是类库的用户,也可能是框架或容器.

Task交出赋值权, 从主动赋值到被动赋值, 这就是控制反转.

轻松理解 Java开发中的依赖注入(DI)和控制反转(IOC)

第二节 控制反转 Inversion of control

什么是控制反转 ?

简单的说从主动变被动就是控制反转.

上文以依赖注入的例子,对控制反转做了个简单的解释.

控制反转是一个很广泛的概念, 依赖注入是控制反转的一个例子,但控制反转的例子还很多,甚至与软件开发无关.

这有点类似二八定律,人们总是用具体的实例解释二八定律,具体的实例不等与二八定律(不了解二八定律的朋友,请轻松忽略这个类比)

现在从其他方面谈一谈控制反转.

传统的程序开发,人们总是从main 函数开始,调用各种各样的库来完成一个程序.

这样的开发,开发者控制着整个运行过程.

而现在人们使用框架(Framework)开发,使用框架时,框架控制着整个运行过程.

对比以下的两个简单程序:

  1. 简单java程序
package demo;
public class Activity {
 public Activity(){
 this.onCreate();
 }
 public void onCreate(){
 System.out.println("onCreate called");
 }
 public void sayHi(){
 System.out.println("Hello world!");
 }
 public static void main(String[] args) {
 Activity a = new Activity();
 a.sayHi();
 }
}
  1. 简单Android程序
package demo;
import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends Activity
{
 @Override
 public void onCreate(Bundle savedInstanceState)
 {
 super.onCreate(savedInstanceState);
 TextView tv = new TextView(this);
 tv.append("Hello ");
 tv.append("world!");
 setContentView(tv);
 }
}

这两个程序最大的区别就是,前者程序的运行完全由开发控制,后者程序的运行由Android框架控制.

两个程序都有个onCreate方法.

前者程序中,如果开发者觉得onCreate 名称不合适,想改为Init,没问题,直接就可以改, 相比下,后者的onCreate 名称就不能修改.

因为,后者使用了框架,享受框架带来福利的同时,就要遵循框架的规则.

这就是控制反转.

可以说, 控制反转是所有框架最基本的特征.

也是框架和普通类库最大的不同点.

很多Android开发工程师在享用控制反转带来的便利,去不知什么是控制反转.

就有点像深海里的鱼不知到什么是海水一样.

通过框架可以把许多共用的逻辑放到框架里,让用户专注自己程序的逻辑.

这也是为什么现在,无论手机开发,网页开发,还是桌面程序, 也不管是Java,PHP,还是Python框架无处不在.

回顾下之前的文件: MyFramework.java

package demo;
public class MyFramework {
 public static void main(String[] args) {
 Task t = new Task("Task #1");
 Coder owner = new Phper("lee4");
 t.setOwner(owner);
 t.start();
 }
}

这只是简单的测试程序,取名为MyFramework, 是因为它拥有框架3个最基本特征

  1. main函数,即程序入口.
  2. 创建对象.
  3. 装配对象.(setOwner)

这里创建了两个对象,实际框架可能会创建数千个对象,可能通过工厂类而不是直接创建,

这里直接装配对象,实际框架可能用XML 文件描述要创建的对象和装配逻辑.

当然实际的框架还有很多这里没涉及的内容,只是希望通过这个简单的例子,大家对框架有个初步认识.

控制反转还有一个漂亮的比喻:

好莱坞原则(Hollywood principle)

"不要打电话给我们,我们会打给你(如果合适)" ("don't call us, we'll call you." )

这是好莱坞电影公司对面试者常见的答复.

事实上,不只电影行业,基本上所有公司人力资源部对面试者都这样说.

让面试者从主动联系转换为被动等待.

为了增加本文的趣味性,这里在举个比喻讲述控制反转.

人们谈恋爱,在以前通常是男追女,现在时代进步了,女追男也很常见.

这也是控制反转

体会下你追女孩和女孩追你的区别:

你追女孩时,你是主动的,你是标准制定者, 要求身高多少,颜值多少,满足你的标准,你才去追,追谁,什么时候追, 你说了算.

这就类似,框架制定接口规范,对实现了接口的类调用.

等女孩追你时,你是被动的,她是标准制定者,要求有车,有房等,你买车,买房,努力工作挣钱,是为了达到标准(既实现接口规范), 你万事具备, 处于候追状态, 但时谁来追你,什么时候追,你不知道.

这就是主动和被动的区别,也是为什么男的偏好主动的原因.

这里模仿好莱坞原则,提一个中国帅哥原则:"不要追哥, 哥来追你(如果合适)",

简称CGP.( Chinese gentleman principle: "don't court me, I will court you")

扩展话题

  1. 面向对象的设计思想
  2. 第一节 提到在面向对象设计中,名词皆对象,这里做些补充.
  3. 当面对一个项目,做系统设计时,第一个问题就是,系统里要设计哪些类?
  4. 最简单的办法就是,把要设计系统的名词提出来,通常,名词可设计为对象,
  5. 但是否所有名词都需要设计对应的类呢? 要具体问题具体分析.不是不可以,是否有必要.
  6. 有时候需要把一些动词名词化, 看看现实生活中, 写作是动词,所有写作的人叫什么? 没有合适的称呼,我们就叫作者, 阅读是动词,阅读的人就称读者. 中文通过加"者","手"使动词名词化,舞者,歌手,投手,射手皆是这类.
  7. 英语世界也类似,通过er, or等后缀使动词名词化, 如singer,writer,reader,actor, visitor.
  8. 现实生活这样, Java世界也一样.
  9. Java通过able,or后缀使动词名词化.如Runnable,Serializable,Parcelable Comparator,Iterator.
  10. Runnable即可以运行的东西(类) ,其他类似.
  11. 了解了动词名词化,对java里的很多类就容易理解了.
  12. 相关术语(行话)解释
  13. Java 里术语满天飞, 让初学者望而生畏. 如果你不想让很多术语影响学习,这一节可忽视.
  14. 了解了原理,叫什么并不重要. 了解些术语的好处是便于沟通和阅读外文资料,还有就是让人看起来很专业的样子.
  • 耦合(couple): 相互绑定就是耦合第一节 Step1,Step2,Step3 都是.
  • 紧耦合(Tight coupling) Step1 中,Task 和 zhang3 绑在一起; Step2中 Task 和 Phper 绑在一起, 都是.
  • 松耦合(Loose coupling) Step3 中,Task 和 Coder 接口绑在一起就是
  • 解耦(Decoupling): 从Step1 , Step2, 到 Step3 的设计就是Decoupling, 让对象可以灵活组合.
  • 上溯造型或称向上转型(Upcasting). 把一个对像赋值给自己的接口或父类变量就是.因为画类图时接口或父类在画在上面,所以是Upcasting. Step3中一下程序就是:

Coder owner = new Phper("lee4");

  • 下溯造型或称向下转型(Downcasting). 和Upcasting 相反,把Upcasting过后的对象转型为之前的对象. 这个上述程序不涉及,顺带说一下

Coder owner = new Phper("lee4");

Phper p = (Phper) owner;

  • 注入(Inject): 通过方法或构造函数把一个对象传递给另一个对象. Step3 中的setOwner 就是.
  • 装配(Assemble): 和上述注入是一个意思,看个人喜好使用.
  • 工厂(Factory): 如果一个类或对象专门负责创建(new) 对象,这个类或对象就是工厂
  • 容器(Container): 专门负责存放创建好的对象的东西. 可以是个Hash表或 数组.
  • 面向接口编程(Interface based programming) Step3 的设计就是.

轻松理解 Java开发中的依赖注入(DI)和控制反转(IOC)

相关推荐