欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

理解依赖注入和控制反转

程序员文章站 2022-06-19 17:49:08
从一个任务开始讲 某天,公司领导找到开发人员,说要开发一个微信支付宝的收款明细获取功能,我们把这个任务作为一个案例进行说明。 第一步:设计 案例精简:把任务指派给开发人员完成。本句话中,有两个名词:“任务”和“开发人员”,所以我们考虑设计两个对象(任务和开发人员)。 开发人员对象: 任务对象: 场景 ......
从一个任务开始讲

某天,公司领导找到开发人员,说要开发一个微信支付宝的收款明细获取功能,我们把这个任务作为一个案例进行说明。

第一步:设计

案例精简:把任务指派给开发人员完成。本句话中,有两个名词:“任务”和“开发人员”,所以我们考虑设计两个对象(任务和开发人员)。

开发人员对象:

package DependencyInjectionDemo;

public class Javaer {
    private String name;

    public Javaer(String name) {
        this.name = name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void WriteCode() {
        System.out.println(this.name + " writting java code...");
    }
}

任务对象:

package DependencyInjectionDemo;

public class NewTask {

    private String name;
    private Javaer javaer;

    public NewTask(String name) {
        this.name = name;
        this.javaer = new Javaer("张三");
    }

    public void Start() {
        System.out.println(this.name + " started ..");
        this.javaer.WriteCode();
    }
}

场景类:

package DependencyInjectionDemo;

public class DependencyInjectionDemo {

    public static void main(String[] args) {
        NewTask task = new NewTask("开发微信支付宝收款明细获取工具");
        task.Start();
    }
}

运行结果:

开发微信支付宝收款明细获取工具 started ..
张三 writting java code...

现在让我们来分析一下这个设计存在的问题。

如果不追求复用和耦合,只是临时完成任务,这么写倒也无可厚非; 如果再有别的任务指派给其他开发人员,我们需要去代码内部修改编码; 如果有很仰慕你的同事需要复用你的实现,你不能打包成jar文件给他直接用,因为他不能从jar文件外部修改任务和开发人员;

理解依赖注入和控制反转

所以,我们应当让用户来指派开发人员,改进一下:

package DependencyInjectionDemo;

public class NewTask {

    private String name;
    private Javaer javaer;

    public NewTask(String name) {
        this.name = name;
        //this.javaer = new Javaer("张三"); 删了啦
    }

    public void SetJavaer(Javaer javaer) {
        this.Javaer = javaer;
    }

    public void Start() {
        System.out.println(this.name + " started ..");
        this.javaer.WriteCode();
    }
}

场景类也要做一下修改:

package DependencyInjectionDemo;

public class DependencyInjectionDemo {

    public static void main(String[] args) {
        NewTask task = new NewTask("开发微信支付宝收款明细获取工具");
        task.SetJavaer(new Javaer("张三")); //加入这句
        task.Start();
    }
}

输出和前面的Demo是一样的:

开发微信支付宝收款明细获取工具 started ..
张三 writting java code...

现在,我们知道了一个事实,完成任务需要依赖特定的开发人员(NewTask类依赖Javaer类),开始时,NewTask类在构造时绑定开发人员,现在这种依赖可以在使用时按需要进行绑定。
这就是依赖注入

在上面的案例中,我们是通过Setter进行注入的,另外一种常用的注入方式是通过构造方法进行注入:

    public NewTask(String name, Javaer javaer) {
        this.name = name;
        this.javaer = javaer; //构造方法中进行注入
    }

这里联想一下,任务执行期间,任务执行者(本例中是张三)生病了,那么就需要另外安排一名开发人员继续任务的执行,怎么办呢?这个时候应该考虑的是Javaer这个对象的稳定性,如果开发人员这个对象稳定性非常高,我们可以考虑在NewTask的构造方法中进行注入,因为开发人员这个对象非常稳定,不会出现中途换帅的情况,但事实并非如此,张三生病了,就得允许不中断任务的情况下,重新指派另一名开发人员继续进行开发,很显然,在这个场景中,我们应该使用Setter注入,不需要重新New一个NewTask(也就是任务重新开始),直接使用Setter更换开发人员即可。

这里还有一种注入方式是配置文件注入,这就要求注入的对象稳定性非常高,甚至高到大于服务的生命周期(比如数据库连接)。

第二步:需求挖掘

我们知道,一个开发团队往往是多种开发语言并存的,有些任务适合用Java来完成,有些适合用C#,还有些任务适合用Python,现在问题来了,这个NewTask类库的使用者发现:任务只能指派给Javaer。

所以为了更好的复用,我们的需求应该变成:任务既能指派给Javaer,也能指派给Pythoner和CSharper,以及其他任何以后可能加入的开发语言。

很自然的,我想到了使用接口:

package DependencyInjectionDemo;

public interface Coder {
    void WriteCode();
}

修改原来的Javaer,实现Coder接口:

package DependencyInjectionDemo;

public class Javaer implements Coder {
    private String name;

    public Javaer(String name) {
        this.name = name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void WriteCode() {
        System.out.println(this.name + " writting java code...");
    }
}

Python开发人员实现Coder接口:

package DependencyInjectionDemo;

public class Pythoner implements Coder{
    private String name;
    public Pythoner(String name) {
        this.name = name;
    }
    @Override
    public void WriteCode() {
        System.out.println(this.name + " writting python code...");
    }
}

C# 开发人员实现Coder接口:

package DependencyInjectionDemo;

public class CSharper implements Coder {

    private String name;

    public CSharper(String name) {
        this.name = name;
    }

    @Override
    public void WriteCode() {
        System.out.println(this.name + " writting c# code...");
    }
}

修改任务类中的Javaer为Coder:

public class NewTask {

    private String name;
    private Coder coder;

    public NewTask(String name) {
        this.name = name;
    }

    public void SetCoder(Coder coder) {
        this.coder= coder;
    }

    public void Start() {
        System.out.println(this.name + " started ..");
        this.coder.WriteCode();
    }
}

修改场景类:

package DependencyInjectionDemo;

public class DependencyInjectionDemo {

    public static void main(String[] args) {
        NewTask task = new NewTask("开发微信支付宝收款明细获取工具");
        task.SetCoder(new Javaer("张三"));
        // 都是Coder,允许注入
        // task.SetCoder(new Pythoner("李四"));
        // task.SetCoder(new CSharper("王五"));
        task.Start();
    }
}

现在,我们可以指派任务给pythoner,CSharper和Javaer了,加入以后加入了Ruby或者Go语言开发人员,类库的使用者只需要实现Coder接口,就可以把任务指派给新来的开发人员了,不需要修改NewTask代码,实现了低耦合和可扩展性。

在讲下面的内容之前,我们先来熟悉一个名词:控制反转,四个字,拆成两个词,一个是控制,一个是反转。结合上面的例子,我们的NewTask开始的时候依赖开发人员,其在内部主动创建了开发人员对象,后来我们发现这样造成了强依赖,于是就把NewTask的主动创建开发人员这个操作撤销了,修改成了在外部实现开发人员实例并传入到NewTask内部,NewTask现在只能被动的接收我们创建的开发人员对象,从主动到被动,控制实现了反转。

概念

控制反转是原则,依赖注入是方式。

除了依赖注入(Dependency Injection, 简称DI),还有另外一种方式是“依赖查找(Dependency Locate)”, 场景类需要服务类时,从一个获取点主动取得指定的服务类。这种方式变被动接收注入为主动获取,使得场景类在需要时主动获取服务类,如我们向一个统管全局的Factory传入一个字符串,Factory返回给我一个相应服务类的实例。

不过,不论使用简单工厂(Simple Factory)还是抽象工厂(Abstract Factory),都避免不了判断服务类类型或工厂类型,这样系统中总要有一个地方存在不符合OCP的if…else或switch…case结构,这种缺陷是Simple Factory和Abstract Factory以及依赖获取本身无法消除的,而在某些支持反射的语言中(如Java和C#),通过将反射机制的引入彻底解决了这个问题。

反射与依赖注入

上面的例子中,假设我们再增加一个语言的分支(如Go)而且使用了工厂模式(简单或抽象工厂),我们需要实现Coder接口,虽然符合开闭原则(对扩展开放,对修改关闭),但最终,我们还是要回到工厂方法内部,去增加一个swith或ifelse分支,以完善我们的判断,这就破坏了开闭原则。依赖注入本身是没有能力解决这个问题的,但语言本身的反射机制(Reflection)却能从根本上解决这个问题。

现在的问题是,最终我们找到的这个对象,还是需要通过“new”操作来实例化,那么,我们如何通过不修改代码的方式,“new”出一个新的实例呢?

来试着实现一下:

package DependencyInjectionDemo;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class DependencyInjectionDemo {

    private static String taskName; //任务
    private static String coderName; //语言
    private static String devName; //开发人员

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {

        /*现在我可以把这些写到配置文件中了*/
        taskName = "新任务名称";
        coderName = "Pythoner";
        devName = "小明";

        NewTask task = new NewTask(taskName);
        Coder coder = getCoder(coderName, devName);
        task.SetCoder(coder);

        /* 以前这么写 */
        // task.SetCoder(new Pythoner("李四"));
        // task.SetCoder(new CSharper("王五"));

        task.Start();
    }

    /**
     * 根据类名获取类实例
     * @param coderName
     * @param name
     * @return 类的实例对象
     * @throws ClassNotFoundException
     * @throws NoSuchMethodException
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     * @throws InstantiationException
     */
    public static Coder getCoder(String coderName, String name) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor c = Class.forName("DependencyInjectionDemo."+coderName).getConstructor(String.class);
        Coder coder = (Coder)c.newInstance(new Object[] {name});
        return coder;
    }
}

输出:

新任务名称 started ..
小明 writting python code...

以上代码,实现了一个根据类名获取实例的getCoder方法,该方法有两个参数,一个是类名,另一个是Coder的构造参数name。在场景操作中,分别定义了任务名称,语言,开发人员三个变量,现在假设这些变量完全是从配置文件中读取的,那么,当我们以后增加新的语言,增加新的开发人员时,只需要新增加一个Coder接口的实现,然后修改配置文件即可。真正实现了OCP原则。怎么样?是不是感觉自己很牛逼?

理解依赖注入和控制反转

以下为摘录内容,来源:依赖注入那些事儿

IoC Container

说到依赖注入的话,就不能不提到IoC Container(IoC容器),那么到底什么是IoC容器?我们还是先来看看它的出现背景。

我们知道,软件开发领域有句著名的论断:不要重复发明*!因为软件开发讲求复用,所以,对于应用频繁的需求,总是有人设计各种通用框架和类库以减轻人们的开发负担。例如,数据持久化是非常频繁的需求,于是各种ORM框架应运而生;再如,对MVC的需求催生了Struts等一批用来实现MVC的框架。

随着面向对象分析与设计的发展和成熟,OOA&D被越来越广泛应用于各种项目中,然而,我们知道,用OO就不可能不用多态性,用多态性就不可能不用依赖注入,所以,依赖注入变成了非常频繁的需求,而如果全部手工完成,不但负担太重,而且还容易出错。再加上反射机制的发明,于是,自然有人开始设计开发各种用于依赖注入的专用框架。这些专门用于实现依赖注入功能的组件或框架,就是IoC Container。

从这点看,IoC Container的出现有其历史必然性。目前,最著名的IoC也许就是Java平台上的Spring框架的IoC组件,而.NET平台上也有Spring.NET和Unity等。

IoC Container 的分类

前面曾经讨论了三种依赖注入方式,但是,想通过方式对IoC Container进行分类很困难,因为现在IoC Container都设计很完善,几乎支持所有依赖注入方式。不过,根据不同框架的特性和惯用法,还是可以讲IoC Container分为两个大类。

重量级IoC Container
所谓重量级IoC Container,是指一般用外部配置文件(一般是XML)作为依赖源,并托管整个系统各个类的实例化的IoC Container。这种IoC Container,一般是承接了整个系统几乎所有多态性的依赖注入工作,并承接了所有服务类的实例化工作,而且这些实例化依赖于一个外部配置文件,这种IoC Container,很像通过一个文件,定义整个系统多态结构,视野宏大,想要很好驾驭这种IoC Container,需要一定的架构设计能力和丰富的实践经验。

Spring和Spring.NET是重量级IoC Container的例子。一般来说,这种IoC Container稳定性有余而活性不足,适合进行低活多态性的依赖注入。

轻量级IoC Container

还有一种IoC Container,一般不依赖外部配置文件,而主要使用传参的Setter或Construtor注入,这种IoC Container叫做轻量级IoC Container。这种框架很灵活,使用方便,但往往不稳定,而且依赖点都是程序中的字符串参数,所以,不适合需要大规模替换和相对稳定的低活多态性,而对于高活多态性,有很好的效果。

Unity是一个典型的轻量级IoC Container。

参考文献

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