前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >非Java程序员竟鲜有人真正理解DI和IOC

非Java程序员竟鲜有人真正理解DI和IOC

作者头像
后端技术探索
发布2018-10-18 10:40:46
8670
发布2018-10-18 10:40:46
举报

前言

小编在后端圈也算是阅人无数了, 发现一个现象,Java程序员对于面向对象语言的基础知识整体掌握比较扎实,而类似PHP,Python的初级甚至中级程序员就比较薄弱,比如说DI和IOC,很少有PHP程序员能理解的很准确。

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

  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里是否有很多共用的逻辑代码,如果是,就选择父类, 否则就选接口. 这里我们选择接口的办法: 新增Coder接口, 文件: Coder.java

package demo;
public interface Coder {
    public void writeCode();
}

修改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");
    }
}

新类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");
    }
}

修改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();
    }
}

修改用于测试的类使用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)开发,使用框架时,框架控制着整个运行过程.

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

简单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();
     }
}

简单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." )这是好莱坞电影公司对面试者常见的答复.事实上,不只电影行业,基本上所有公司人力资源部对面试者都这样说. 让面试者从主动联系转换为被动等待.

为了增加本文的趣味性,这里在举个比喻讲述控制反转.人们谈恋爱,在以前通常是男追女,现在时代进步了,女追男也很常见.这也是控制反转。体会下你追女孩和女孩追你的区别:

  • 你追女孩时,你是主动的,你是标准制定者, 要求身高多少,颜值多少,满足你的标准,你才去追,追谁,什么时候追, 你说了算. 这就类似,框架制定接口规范,对实现了接口的类调用.
  • 等女孩追你时,你是被动的,她是标准制定者,要求有车,有房等,你买车,买房,努力工作挣钱,是为了达到标准(既实现接口规范), 你万事具备, 处于候追状态, 但时谁来追你,什么时候追,你不知道.

这就是主动和被动的区别,也是为什么男的偏好主动的原因. 这里模仿好莱坞原则,提一个中国帅哥原则:"不要追哥, 哥来追你(如果合适)"。

第三节 总结
  • 控制反转是一种在软件工程中解耦合的思想,调用类只依赖接口,而不依赖具体的实现类,减少了耦合。控制权交给了容器,在运行的时候才由容器决定将具体的实现动态的“注入”到调用类的对象中。
  • 依赖注入是一种设计模式,可以作为控制反转的一种实现方式。依赖注入就是将实例变量传入到一个对象中去(Dependency injection means giving an object its instance variables)。
  • 通过IoC框架,类A依赖类B的强耦合关系可以在运行时通过容器建立,也就是说把创建B实例的工作移交给容器,类A只管使用就可以。
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-09-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 nginx 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 第一节 依赖注入 Dependency injection
  • 第二节 控制反转 Inversion of control
  • 第三节 总结
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档