美文网首页JAVA EE那些事儿Java与Android开发JAVA
轻松理解 Java开发中的依赖注入(DI)和控制反转(IOC)

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

作者: 黄洪清 | 来源:发表于2016-01-08 23:03 被阅读11433次

    黄洪清 497915580atqq.com
    简书首发

    前言

    关于这个话题, 网上有很多文章,这里, 我希望通过最简单的话语与大家分享.
    依赖注入和控制反转两个概念让很多初学这迷惑, 觉得玄之又玄,高深莫测.
    这里想先说明两点:

    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接口,
      文件: Coder.java
    package demo;
    public interface Coder {
        public void writeCode();
    }
    
    1. 修改Phper类实现Coder接口
      文件: 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接口
      文件: 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的依赖.
      文件: 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交出赋值权, 从主动赋值到被动赋值, 这就是控制反转.

    第二节 控制反转 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. 面向对象的设计思想
      第一节 提到在面向对象设计中,名词皆对象,这里做些补充.
      当面对一个项目,做系统设计时,第一个问题就是,系统里要设计哪些类?
      最简单的办法就是,把要设计系统的名词提出来,通常,名词可设计为对象,
      但是否所有名词都需要设计对应的类呢? 要具体问题具体分析.不是不可以,是否有必要.
      有时候需要把一些动词名词化, 看看现实生活中, 写作是动词,所有写作的人叫什么? 没有合适的称呼,我们就叫作者, 阅读是动词,阅读的人就称读者. 中文通过加"者","手"使动词名词化,舞者,歌手,投手,射手皆是这类.
      英语世界也类似,通过er, or等后缀使动词名词化, 如singer,writer,reader,actor, visitor.
      现实生活这样, Java世界也一样.
      Java通过able,or后缀使动词名词化.如Runnable,Serializable,Parcelable Comparator,Iterator.
      Runnable即可以运行的东西(类) ,其他类似.
      了解了动词名词化,对java里的很多类就容易理解了.

    2. 相关术语(行话)解释
      Java 里术语满天飞, 让初学者望而生畏. 如果你不想让很多术语影响学习,这一节可忽视.
      了解了原理,叫什么并不重要. 了解些术语的好处是便于沟通和阅读外文资料,还有就是让人看起来很专业的样子.

    • 耦合(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 的设计就是.

    希望上述内容, 对大家有所帮助, 谢谢.

    进一段广告

    快才助手, 在电脑上操作手机, Android屏幕同步软件
    本文作者手工打造,热情推荐,网址: http://www.kwaicai.com

    推荐阅读

    本文系原创,转载需包含作者,出处和广告。

    2016年1月于成都

    相关文章

      网友评论

      • foreverspr_b700:通俗易懂,赞!
      • 04593fca42e4:通俗易懂,赞:+1:
      • Alan_Mo:文章写得深入浅出GOOD !
      • 逸云天:学习了,信息
      • e3bdd202c2f6:通俗易懂,例子恰当,不粗俗。
      • 达则兼济天下:简单易懂,风趣幽默,高人大概就是这样样子吧!
      • isepien:看后明白了两个概念,多谢分享
      • _Ruby:提个问题呀,前面说到将Task修改为绑定接口的方式,从一开始的Task主动新建对象,到后面的由使用者来注入对象,对于Task来说,这是个主动到被动的过程,并以此引出了控制反转这个概念,但是从使用者来说,这是个被动到主动的过程。后面提到了,控制反转是框架最基本的特征,那么使用者在使用框架的时候,需要遵循框架的一些规则,这不就从主动变成了被动吗,感觉与前面的互相矛盾了。希望博主能解答下我的疑惑,非常感谢!
      • h2coder:通俗易懂,赞👍
      • b379f7132b1b:写得很明白
      • 满船清梦___:写的非常好 ,感激
      • 不流畅:点赞 ,清晰明了
      • carlonelong:JAVA就会故弄玄虚
      • DeppWang:写得非常好,学习
      • 梦华芳秋:写的最简单,但让人最明白的一篇好文!谢谢,老司机!
      • b4e06d5e1620:大学老师要有这种授课水平,我也不用上那么多自习。 能把自己的理解通过深入浅出的方式讲出来,不仅是需要自己理解的够深,更需要考虑听众如何能够循序渐进的接受,这就非常需要智慧了。 现在的大学老师,真的非常缺少智慧啊😂
      • 递归循环迭代:非常感谢!太适合新手理解了!
      • ba6811627852:深入浅出就应该是这个样子吧!
      • 南朝小木瓜:风趣易懂,一看就是老司机,赞!
      • 哈喽jv:写的真清楚,并且语言很严谨!大赞大赞!! 受益匪浅~~~~
      • RavenX:很好的文章。简单易懂
      • dba2ebfecc80:谢谢,支持
      • mingkg21:希望有更多类似的文章,谢谢
      • mingkg21:写得不错,一下子明白了
      • lovexiaov:谈对象的例子感觉不是特别合适,被追的话还要完成追求者提出的要求~
        銷夨吥見:都能被追了,说明已经符合自己的要求了嘛
        黄洪清:@lovexiaov
        被追的话,已经达标.
        达标在前,被追在后.
      • 曾樑:赞吖

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

      本文链接:https://www.haomeiwen.com/subject/rfyfkttx.html