控制反转和依赖注入

依赖

控制反转(Inversion of Control)、依赖反转(Dependency Inversion Principle)、依赖注入(Dependency Injection)、控制反转容器(Inversion of Control Container)等等,这些术语有的描述的是一种思想,有的描述的是一种技术实现,但是目的都是为了解决依赖关系而产生的。

什么是依赖?其实可以理解为产品设计之初各个零部件之间的耦合关系。
举个例子,一台笔记本电脑我们可以理解这是一个完整的产品,它是由cpu、主板、硬盘、内存等等零部件构成的(这里的主板就相当于我们程序设计里的主类或者说主方法)。有些主板兼容性强,支持各种各样的协议接口,因此我们可以在内存接口上插DDR3 、DDR4,可以在显卡接口上接入amd显卡也可以接入NVIDIA显卡,可以接入sata硬盘可以接入NGFF硬盘等等。那假如我们拿到了一台定制电脑,他的协议和内置规则限制了各个接口只能使用指定的零件,例如只能用三星的ddr3内存条,只能用日立的sata3硬盘,只能使用amd的显卡,那是不是立马就感觉这台机器的升级扩展能力、维护能力非常差了?

程序上的依赖也是类似的道理,在面向对象编程里,我们通常会在一个类中调用其他的类及其方法,避免重复造轮子的问题。但是在早期的编程或编程思路设计中,往往会忽视自己写出来的类是否具有高扩展性高维护性,使得自己写出的模块十分依赖于某一些特定的模块,即模块的耦合较高而内聚较差。

public class MyServer{
    private _db = new MysqlDB ();
    public void main(){
        _db.dosomething();
    }
    class MysqlDB{
        public void dosomething();
    }
}

例如上面的这段代码,MyServer需要实例化一个MysqlDB 的对象来进行操作,MyServer的实现依赖于MysqlDB的实现。这种依赖关系导致了这个MyServer无法处理access、sqlserver等其他数据库,只能处理Mysql数据库。这个简单的示例程序里面可能只要更换MysqlDB类即可,但是在实际的项目中,使用这种方式组织代码会使得类与类之间形成成一张庞大的依赖关系网,一旦某个类出现了改动,那么依赖于它的类也需要进行修改,而更上层的各个类可能也要跟着修改,这是违反开放封闭原则的。

DIP

依赖倒置原则(Dependency Inversion Principle)就是其中的一种解决这种依赖问题的思想。需要注意的是DIP是一种原则或者说是一种思考方向,因此并未提供具体操作方法。

DIP:高层模块不应当依赖于低层模块,高层模块和低层模块应当都同时依赖于抽象。

我们拿计算机举个例子:
如果一台电脑,其主板设计出来只能使用因特尔公司的网卡,维护性会很差,这可以大致理解为高层模块依赖于低层模块。而如果某个厂家的内存条只能在指定的一些主板上使用,那就使得这个内存的应用范围太狭窄了,即低层模块太过依赖于高层模块。而依赖翻转就是说这两个不同层级的模块都不应当互相依赖,他们应当依赖于接口,即内存、网卡等等的接口协议。

放在代码里,可以理解为高层模块类和低层模块类在设计的时候都应该基于一个统一的接口来设计,即具有的属性、可调用的方法等等都是一致的,这样就在一定程度上既解决了高低层模块直接的依赖关系,又兼顾到了模块的移植扩展能力。

至于为什么这个叫依赖翻转,查到的资料是说这种抽象接口是和高层模块在同一等级的,因此之前的高层模块依赖于低层模块变成了低层模块依赖于高层接口。或者理解为接口协议高于各个模块,所以之前的依赖方向就由高层依赖低层变成了低层依赖高层,因此就翻转倒置(Inversion)了。

IOC

控制反转Inversion of Control是通过改变业务逻辑流程的控制方,由之前通过模块内部的代码确定逻辑流程,变更为由客户在使用时指定操作逻辑。在代码中的表现,是从一开始由代码书写者通过new class确定使用什么模块、通过方法调用确定使用什么逻辑流程,转变成在代码中只声明具有什么特点能实现什么功能,而只有在客户使用时通过交互事件、修改执行配置文件等方式才确定具体的执行模块和执行流程。即将控制权由代码书写者交给了客户、ioc容器或框架。

根据上面的描述可以知道,在书写代码的时候我们是不会确定模块具体new了哪一个类,调用了什么方法,因此也就避免了我们写出来的模块和其他模块直接产生依赖关系,即实现了解耦。

IOC:代码本职之外的工作都应该由某个第三方(IOC容器或框架)完成

试想一个场景,我们在网站登录账号时,在登录账号一栏里可以使用账号密码登录、手机验证码登录。按照传统的写法,我们会在用户管理类中创建一个登录类,在登录类中对登录方式进行判断,然后再根据不同方式进行登录认证并返回认证结果。如果在以后有了需求变更,需要增加二维码登录、qq等第三方登录,那我们就要回去修改这个登录类了,这就违背了开放封闭原则。根据ioc的建议,我们不应当在用户管理类中实例化具体的登录类,而应当在用户使用时(不管是手动临时写逻辑代码还是通过交互式点击或输入)才具体指定要实例化的是哪一个登录类。

这就是控制反转原则,将模块的创建、销毁、调度等等的控制权限由代码书写者交给使用者,由原本模块代码内部定义好关联依赖关系转换成用户通过交互式操作、自己手动书写新的控制逻辑甚至是交给容器框架托管。这样使得各个模块在使用时才建立起依赖关系,避免了依赖问题。

DI

DI是其中依据IOC原则产生的一种设计模式。依赖注入Dependency injection,根据其字面意思可知模块直接的关系是在用户使用时注入进去的,而不是原始代码逻辑中就已经存在的。

DI:依赖通过“注入”的方式提供给需要的类,是 DIP 和 IoC 的具体实现

我们以网络爬虫举个例子:

//RunSpider.java
public class RunSpider {
    String url,path;
    Spider163 spider163;
    Parse163 parse163;
    Download download;
    public RunSpider(String url,String path){
        spider163 = new Spider163();
        parse163 = new Parse163();
        download = new Download();
    }

    public static void main(String[] args) {
        //具体的循环爬取、异步协程管理、解析、下载流程
    }
}

上面的RunSpider 是依据基本的面向对象写出来的爬虫执行类。它在构造函数中创建爬虫、数据解析、数据下载这几个类的实例,并在main方法中执行具体的逻辑方法实现循环爬取、异步爬取、解析数据并保存至指定位置的功能。

很明显地可以看到这种写法有个很严重的问题,它严重依赖于这几个具体的处理类(假设这几个类都是针对163网站而写的),当我们需要去爬取其他网站的内容时我们又将需要重新修改代码或重新创建新的项目。而实际上爬取逻辑中有大量的重复操作,只是在不同网页的解析、反爬处理等上有变化,而主体的请求过程、异步处理等等是一致的。或者说,SpiderRun它只负责爬虫流程管理,他不应该关注爬取的页面是哪个,应该用哪个对应的解析模块来解析。

一个很自然的想法就是利用接口实现解析、爬取等等模块的多态问题:

public class RunSpider {
    String url,path;
    SpiderI spider;
    ParseI parse;
    DownloadI download;
    public RunSpider(SpiderI spiderImpl,
                    ParseI parseImpl,
                    DownloadI downloadImpl,
                    String url,String path){
        spider =  spiderImpl;
        parse = parseImpl;
        download = downloadImpl;
    }

    public static void main(String[] args) {
        //具体的循环爬取、异步协程管理、解析、下载流程
    }
}
interface SpiderI(){;}
interface ParseI(){;}
interface DownloadI(){;}

例如上面的代码,我们在定义RunSpider管理执行类的时候,只定义需要接受相应的接口类,表示我们将会用这一接口规格的类来进行处理。具体的接口实例化,我们可以通过构造函数,也可以通过抽象接口的set方法,总之我们把各个类的实例化交给了客户,而不是在代码中写死。

这种在使用时才赋予模块依赖关系的方式,就是依赖注入。一般常见的依赖注入通常包括构造函数注入、使用接口的set方法注入依赖关系、通过容器等方式管理注入等。

在上面的例子中,我们可以在main方法或其他方法中分别实现接口并实例化,然后将对应的实例化对象传入当做参数传入到RunSpider类的构造函数中,实现注入。也可以在构造方法中不对这些接口进行配置,而是单独地通过set方法将实例化的对象设置到各个接口上实现依赖注入。

虽说这样写避免了模块定义时产生依赖问题,但是我们还是需要在使用时手动的修改部分依赖注入的代码来指定依赖关系,这是非常不利于维护和管理的。当然也可以自己新建一个专门用于管理和配置依赖关系的类,不过还是应当优先选择使用ioc容器或框架来完成这样的工作,避免重复造轮子。

在Python中非常有名的Scrapy爬虫框架其实就广泛使用了类似依赖注入的机制,用户通过继承指定的抽象类生成自己的爬虫类(Spider)、解析类(Parse)、数据类(Items)、流程处理类(Pipeline)、中间件类(XXX-MiddleWare)等等,在配置文件(setting)中配置需要处理的类有哪些,并通过命令行或通过其框架提供的命令类执行命令来指定爬虫入口并启动爬虫。

同样比较典型的基于IOC容器的框架就是Spring,Spring中通过对Bean对象的处理,把对象的创建、初始化、销毁工作交给spring容器,统一管理各个对象的生命周期。

Reference


IoC vs. DI by Francisco Alvarez
Inversion of Control 对IOC、DIP、DI、IOC容器相对比较完整的讲解。
依赖注入那些事儿 通过一个简单的例子深入浅出,建议阅读
DIP vs IoC vs DI 一个相对不错的中文版的总结
Spring Framework Basics - What Is Inversion Of Control?
Spring框架如何加载和定义Spring Bean类?
spring02——IOC(控制反转)、依赖注入和依赖查找

你可能感兴趣的:(控制反转和依赖注入)