控制反转,依赖注入,依赖倒置傻傻分不清楚?

通过这篇文章,你将了解到

1. 控制反转(IoC)

1.1 一个典型案例

介绍「控制反转」之前,我们先看一段代码

public class UserServiceTest {
    public static boolean doTest() {
        //此处编写自己的判断逻辑
        return false;
    }

    public static void main(String[] args) {

        if (doTest()) {
            System.out.println("Test succeed.");
        } else {
            System.out.println("Test failed.");
        }
    }
}

如上,我们为一个方法写了一个测试用例,包括 main方法的创建,所有的流程都是我们自己来控制的。

现在有这么一个框架,代码如下:

public abstract class TestCase {
    public void run() {
        if (doTest()) {
            System.out.println("Test succeed.");
        } else {
            System.out.println("Test failed.");
        }
    }

    public abstract boolean doTest();
}

public class JunitApplication {
    private static final List cases = new ArrayList();

    public static void register(TestCase testCase){
        cases.add(testCase);
    }

    public static void main(String[] args) {
        for(TestCase testCase : cases){
            testCase.run();
        }
    }
}

利用这么框架,我们如果再为 UserServiceTest写一个测试用例,只需要继承 TestCase,并重写其中的 doTest方法即可。

public class UserServiceTestCase extends TestCase{
    @Override
    public boolean doTest() {
        //此处编写自己的判断逻辑
        return false;
    }

}

//注册测试用例
JunitApplication.register();

看完这里例子,相信读者朋友已经明白了这个框架给我们带来了怎样的便利。一开始我们需要为每个测试方法添加一个 main方法,一旦待测试的方法多起来会非常的不方便。现在框架给我们制定了程序运行的基本骨架,并为我们预设了埋点,我们只需要设置好框架的埋点,剩下的执行流程就交给框架来完成就可以了。

这就是「框架实现控制反转」的典型例子。这里的「控制」指的是对执行流程的控制,「反转」指的是在框架产生之前我们需要手动控制全部流程的执行,而框架产生之后,有框架来执行整个大流程的执行,流程控制由我们「反转」给了框架。

1.2 IoC概念的提出

早在1988年,Ralph E. Johnson与Brian Foote在文章Designing Reusable Classes中提出了 inversion of control的概念,他们怎么也没想到,这几个单词会在未来给中国的编程者造成多大的麻烦!

控制反转,依赖注入,依赖倒置傻傻分不清楚?

虽然Spring框架把IoC的概念发扬光大,但IoC的诞生远远早于Spring,并且IoC的概念正是在讨论框架设计的时候被提出来的。至于框架和IoC是先有鸡还是先有蛋,这个问题对我们并没有什么意义。​

当IoC概念模糊不清的时候,追本溯源或许是让我们彻底理解这个概念的好想法。至于概念之外的延伸不过是细枝末节罢了。接下来我们体会一下文章中比较重要两段话,我进行了意译。

One important characteristic of a framework is that the methods defined by the user to tailor the framework will often be called from within the framework itself, rather than from the user’s application code.

「框架」的一个重要特征是,框架本身定义的方法常常由框架自己调用,而非用户的应用程序代码调用。

This inversion of control gives frameworks the power to serve as extensible skeletons. The methods supplied by the user tailor the generic algorithms defined in the framework for a particular application.

​这种「控制反转」使框架作为一个程序运行的骨架,具有了可扩展的能力。用户可以自定义框架中预设好的埋点。

IoC就是一种思想,而不是某种具体编程技术的落地。应用了「控制反转」思想的框架允许用户在一定程度上「填空」即可,其余的运行都交给框架。

1.3 为什么提出IoC

几乎所有编程思想的提出都是基于一个目的——解耦。Ioc是怎么解决耦合问题的呢?

假设我们有四个对象,彼此之间的依赖关系如图

控制反转,依赖注入,依赖倒置傻傻分不清楚?
翻译成代码大致如下:
class A{
    Object b = new B();
    ...

}

class B{
    Object c = new C();
    Object d = new D();
    ...

}

class C{
    Object d = new D();
}

但是 A对象就是实实在在地需要 B对象啊,这种依赖关系无法被抹除,就意味着耦合关系不可能完全解除,但是可以减弱!IoC的思想是引入一个IoC容器来处理对象之间的依赖关系,由主动依赖转为被动依赖,减轻耦合关系,从强耦合变为弱耦合。

控制反转,依赖注入,依赖倒置傻傻分不清楚?
关于IoC容器的作用,给大家举个生活中的例子。

假设有3个顾客分别从4个店铺购买了商品,好巧不巧,所有人都碰到了质量问题,在第三方购物平台诞生之前,每个顾客都只能分别与每家店铺协商理赔问题,此时顾客和店铺之间是强耦合关系。

控制反转,依赖注入,依赖倒置傻傻分不清楚?
有了第三方购物平台之后,顾客可以直接和平台投诉,让平台和各个店铺进行协商,平台对每位顾客进行统一理赔,此时顾客和店铺之间就是松耦合的关系,因为最累的工作被平台承担了,此时平台的作用就类似IoC容器。
控制反转,依赖注入,依赖倒置傻傻分不清楚?
最后拿Spring再举个例子。

从大粒度上看,使用Spring之后我们不需要再写 Servlet,其中调用 Servlet的流程全部交给Spring处理,这是IoC。

从小粒度上看,在Spring中我们可以用以下两种方式创建对象

// 方式1
private MySQLDao dao = new MySQLDaoImpl();

// 方式2
private MySQLDao dao = (MySQLDao) BeanFactory.getBean("mySQLDao");

使用方式1, dao对象的调用者和 dao对象之间就是强耦合关系,一旦 MySQLDaoImpl源码丢失,整个项目就会在编译时期报错。

使用方式2,如果我们在xml文件中配置了 mySQLDao这个bean,如果源码丢失,最多报一个运行时异常(ClassNotFound错误),不至于影响项目的启动。

Spring提供了方式2这样的方式,自动给你查找对象,这也是IoC,而且这是IoC的常用实现方法之一, 依赖查找。另一种是 依赖注入,我们一会儿再介绍。

1.4 Spring和IoC的关系

Spring是将IoC思想落地的框架之一,并将之发扬光大的最著名的框架(没有之一)。

1.5 面试中被问到IoC怎么回答

「控制反转」是应用于软件工程领域的,在运行时被装配器对象用来绑定耦合对象的一种编程思想,对象之间的耦合关系在编译时通常是未知的。

在传统的编程方式中,业务逻辑的流程是由应用程序中早已设定好关联关系的对象来决定的。在使用「控制反转」的情况下,业务逻辑的流程是由对象关系图来决定的,该对象关系图由IoC容器来负责实例化,这种实现方式还可以将对象之间关联关系的定义抽象化。绑定的过程是由”依赖注入”实现的。

控制反转是一种以给予应用程序中目标组件更多控制为目的的设计范式,并在实际工作中起到了有效作用。

2. 依赖注入(DI)

依赖注入的英文翻译是 Dependency Injection,缩写为 DI。

依赖注入不等于控制反转!依赖注入只是实现控制反转的一种方式!
依赖注入不等于控制反转!依赖注入只是实现控制反转的一种方式!
依赖注入不等于控制反转!依赖注入只是实现控制反转的一种方式!

这个概念披着”高大上”的外衣,但是实质却非常单纯。用人话解释就是:不通过 new() 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。

举一个平时编码常用的一个例子,我们在 Controller中调用 Service服务的时候一般会这么写

@Api(tags = {"报警联系人接口"})
@RestController
@RequestMapping("/iot/contact")
public class AlarmContactController extends BaseController {

    // 这就是大名鼎鼎的DI啊,是不是非常简单!
    @Autowired
    private IAlarmContactService alarmContactService;

    ...

}

这就是大名鼎鼎的DI啊,是不是非常简单!

2.1 面试中被问到「依赖注入」怎么回答

依赖注入是在编译阶段尚不知道所需功能是来自哪个类的情况下,将其他对象所依赖的功能对象实例化的手段。有三种实现方式:构造器注入、setter方法注入、接口注入。

3. 依赖倒置原则(DIP)

3.1 定义

「依赖倒置」原则的英文翻译是 Dependency Inversion Principle,缩写为 DIP。中文翻译有时候也叫「依赖反转」原则。

「依赖倒置」是本文要讲述的主要内容,是七大设计原则之二,在生产实际中应用的非常广泛,主要内容为

  1. 高层模块(high-level modules) 不要直接依赖低层模块(low-level);
  2. 高层模块和低层模块应该 通过抽象(abstractions)来互相依赖
  3. 抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)依赖抽象(abstractions)。

暂时看不懂没关系,我们先看个代码案例。

3.2 代码示例

陀螺研发了一套自动驾驶系统,在积极谈判之下和本田以及福特达成了合作协议,两个厂商各自提供汽车启动、转弯和停止的api供自动驾驶调用,系统就能实现自动驾驶,代码如下

/**
 * @author 公众号【蝉沐风】
 * @desc 福特汽车厂商提供的接口
 */
public class FordCar{
    public void run(){
        System.out.println("福特开始启动了");
    }

    public void turn(){
        System.out.println("福特开始转弯了");
    }

    public void stop(){
        System.out.println("福特开始停车了");
    }
}

/**
 * @author 公众号【蝉沐风】
 * @desc 本田汽车厂商提供的接口
 */
public class HondaCar {
    public void run() {
        System.out.println("本田开始启动了");
    }

    public void turn() {
        System.out.println("本田开始转弯了");
    }

    public void stop() {
        System.out.println("本田开始停车了");
    }
}

/**
 * @author 公众号【蝉沐风】
 * @desc 自动驾驶系统
 */
public class AutoDriver {
    public enum CarType {
        Ford, Honda
    }

    private CarType type;

    private HondaCar hcar = new HondaCar();
    private FordCar fcar = new FordCar();

    public AutoDriver(CarType type) {
        this.type = type;
    }

    public void runCar() {
        if (type == CarType.Ford) {
            fcar.run();
        } else {
            hcar.run();
        }
    }

    public void turnCar() {
        if (type == CarType.Ford) {
            fcar.turn();
        } else {
            hcar.turn();
        }
    }

    public void stopCar() {
        if (type == CarType.Ford) {
            fcar.stop();
        } else {
            hcar.stop();
        }
    }

}

自动驾驶系统运转良好,很快,奥迪和奔驰以及宝马纷纷找到陀螺寻求合作,陀螺不得不把代码改成这个样子。

/**
 * @author 公众号【蝉沐风】
 * @desc 自动驾驶系统
 */
public class AutoDriver {
    public enum CarType {
        Ford, Honda, Audi, Benz, Bmw
    }

    private CarType type;

    private HondaCar hcar = new HondaCar();
    private FordCar fcar = new FordCar();
    private AudiCar audicar = new AudiCar();
    private BenzCar benzcar = new BenzCar();
    private BmwCar bmwcar = new BmwCar();

    public AutoDriver(CarType type) {
        this.type = type;
    }

    public void runCar() {
        if (type == CarType.Ford) {
            fcar.run();
        } else if (type == CarType.Honda) {
            hcar.run();
        } else if (type == CarType.Audi) {
            audicar.run();
        } else if (type == CarType.Benz) {
            benzcar.run();
        } else {
            bmwcar.run();
        }
    }

    public void turnCar() {
        if (type == CarType.Ford) {
            fcar.turn();
        } else if (type == CarType.Honda) {
            hcar.turn();
        } else if (type == CarType.Audi) {
            audicar.turn();
        } else if (type == CarType.Benz) {
            benzcar.turn();
        } else {
            bmwcar.turn();
        }
    }

    public void stopCar() {
        if (type == CarType.Ford) {
            fcar.stop();
        } else if (type == CarType.Honda) {
            hcar.stop();
        } else if (type == CarType.Audi) {
            audicar.stop();
        } else if (type == CarType.Benz) {
            benzcar.stop();
        } else {
            bmwcar.stop();
        }
    }

}

如果看过我上一篇开闭原则的文章,你会马上意识到这段代码不符合开闭原则。没错,一段代码可能同时不符合多种设计原则,那针对今天的「依赖倒置」原则,这段代码问题出现在哪里呢?

我们再来看一下「依赖倒置」原则的要求:

  1. 高层模块(high-level modules) 不要直接依赖低层模块(low-level);
  2. 高层模块和低层模块应该 通过抽象(abstractions)来互相依赖
  3. 抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)依赖抽象(abstractions)。

针对第1点,高层模块 AutoDriver直接依赖了底层模块 XXCar,体现就是在 AutoDriver中直接 new了具体的汽车对象。因此也就没有做到第2点和第3点。UML类图如下:

控制反转,依赖注入,依赖倒置傻傻分不清楚?
那我们就在上层模块和低层模块之间加一层抽象吧,定义一个接口 ICar,表示抽象的汽车,这样 AutoDriver直接依赖的就是抽象 ICar,看代码:
/**
 * @author 公众号【蝉沐风】
 * @desc 汽车的抽象接口
 */
public interface ICar {
    void run();
    void turn();
    void stop();
}

public class FordCar implements ICar{
    @Override
    public void run(){
        System.out.println("福特开始启动了");
    }

    @Override
    public void turn(){
        System.out.println("福特开始转弯了");
    }

    @Override
    public void stop(){
        System.out.println("福特开始停车了");
    }
}

public class HondaCar implements ICar{
    @Override
    public void run() {
        System.out.println("本田开始启动了");
    }

    @Override
    public void turn() {
        System.out.println("本田开始转弯了");
    }

    @Override
    public void stop() {
        System.out.println("本田开始停车了");
    }
}

public class AudiCar implements ICar{
    @Override
    public void run() {
        System.out.println("奥迪开始启动了");
    }

    @Override
    public void turn() {
        System.out.println("奥迪开始转弯了");
    }

    @Override
    public void stop() {
        System.out.println("奥迪开始停车了");
    }
}

public class BenzCar implements ICar{
    @Override
    public void run() {
        System.out.println("奔驰开始启动了");
    }

    @Override
    public void turn() {
        System.out.println("奔驰开始转弯了");
    }

    @Override
    public void stop() {
        System.out.println("奔驰开始停车了");
    }
}

public class BmwCar implements ICar {
    @Override
    public void run() {
        System.out.println("宝马开始启动了");
    }

    @Override
    public void turn() {
        System.out.println("宝马开始转弯了");
    }

    @Override
    public void stop() {
        System.out.println("宝马开始停车了");
    }
}

/**
 * @author 公众号【蝉沐风】
 * @desc 自动驾驶系统
 */
public class AutoDriver {

    private ICar car;

    public AutoDriver(ICar car) {
        this.car = car;
    }

    public void runCar() {
        car.run();
    }

    public void turnCar() {
        car.turn();
    }

    public void stopCar() {
        car.stop();
    }

}

重构之后我们发现高层模块 AutoDriver直接依赖于抽象 ICar,而不是直接依赖 XXXCar,这样即使有更多的汽车厂家加入合作也不需要修改 AutoDriver。这就是高层模块和低层模块之间通过抽象进行依赖。

此外, ICar也不依赖于 XXXCar,因为 ICar是高层模块定义的抽象,汽车厂家如果想达成合作,就必须遵循 AutoDriver定义的标准,即需要实现 ICar的接口,这就是第3条所说的具体细节依赖于抽象!

我们看一下重构之后的UML图

控制反转,依赖注入,依赖倒置傻傻分不清楚?

可以看到,原本是 AutoDriver直接指向 XXXCar,现在是 AutoDriver直接指向抽象 ICar,而各种 XXXCar对象反过来指向 ICar,这就是所谓的「依赖倒置(反转)」。

看到这里,不知道你是不是对「依赖倒置」原则有了深刻的理解。其实这种中间添加抽象层的思想应用非常广泛,再举两个例子。

3.3 无所不在的抽象

3.3.1 JVM的抽象

JVM虽然被称为Java虚拟机,但是其底层代码的运行并不直接依赖于Java语言,而是定义了一个字节码抽象(行业标准),只要实现字节码的标准,任何语言都可以运行在JVM之上。

3.3.2 货币的诞生

回到物物交换的时代,王二想用自己多余的鸡换一双草鞋,李四想用自己多余的草鞋换一条裤子,赵五想用自己多余的裤子换个帽子。。。如果用物物交换的方式进行下去,这个圈子可就绕到姥姥家了。然后人们就抽象出了中间层——货币,货币作为购买力的标准使得物物交换变得更加方便。

4. 推荐阅读

5. 参考资料

Original: https://www.cnblogs.com/chanmufeng/p/15881992.html
Author: 蝉沐风
Title: 控制反转,依赖注入,依赖倒置傻傻分不清楚?

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/711239/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 数据库的备份和恢复命令,使用视图,索引,事务

    备份库 直接在cmd窗口中直接输入,结束不需要输入; mysqldump -h端口号 -u用户名 -p密码 数据库名>备份地址 恢复库 在cmd窗口中进行 1、连接数据库 m…

    技术杂谈 2023年6月21日
    074
  • 将知识变成你的技能点

    学习的东西,只有经过多次使用和尝试后,才能成为你的技能点。一个事情,从你学懂它,到它成为你的技能点,这个过程是很漫长的。学习之后,还需要大量的训练和练习。 本文谈谈俺点亮过或正在点…

    技术杂谈 2023年6月1日
    079
  • 【赵渝强老师】Kubernetes的探针

    Kubernetes提供了探针(Probe)对容器的健康性进行检测。实际上我们不仅仅要对容器进行健康检测,还要对容器内布置的应用进行健康性检测。 Probe有以下两种类型: liv…

    技术杂谈 2023年7月24日
    064
  • 【软考】信息系统开发方法

    1.结构化方法 结构是指系统内各个组成要素之间的相互联系、相互作用的框架。架构化方法也称为生命周期法,是一种传统的信息系统开发方法,由结构化分析(Structured Analys…

    技术杂谈 2023年5月31日
    076
  • Linux软件包常见的几种下载、安装方法

    如果服务器是处于在线状态,在使用默认下载源是外国的情况下,安装更新软件包往往会比较痛苦的存在,下载了许久来一个超时就gg了。国内有许多镜像源,完美的解决了这个问题。 对于rpm系列…

    技术杂谈 2023年6月21日
    0123
  • 今年美国什么工作最吃香?程序猿薪酬超医生

    [导读]美国程序猿平均年薪90060美元,成美国今年最佳职业。被人们普遍看好的医生职业仅名列第八。 USNews最新发布了2014年全美最佳职业TOP100排行榜(The 100 …

    技术杂谈 2023年5月31日
    082
  • Microsoft.Bcl.Build 1.0.10 稳定版发布

    Microsoft.Bcl.Build 1.0.10 稳定版发布 解决了之前 1.0.8 在未下载相应的Nuget Package 的情况下项目无法加载的情况 但由于 Micros…

    技术杂谈 2023年5月31日
    0119
  • 数据报表开发技巧:自动为数据报表添加【小计】、【总计】行

    在开发ERP系统的数据报表时,几乎都是需要看到【小计】、【总计】这样的汇总数据的,在数据报表的显示列表中,最下面的一行通常就是【小计】或者【总计】的汇总行。如果手动为每个报表都增加…

    技术杂谈 2023年6月1日
    089
  • 内核内存错误检测工具KFENCE【转】

    转自:https://www.cnblogs.com/linhaostudy/p/15629244.html 正文 Linux 5.12引入一个新的内存错误检测工具:KFENCE(…

    技术杂谈 2023年5月31日
    0108
  • 一个理科直男如何看《鱿鱼游戏》

    前言 我一向不怎么喜欢看棒子片。 但是十一期间却疯狂的追着一部剧:《鱿鱼游戏》。 这片子在全网实在是太火了,火到全球播放量1.11亿次,成为奈飞收视率最高的全球非英语原创剧。 鱿鱼…

    技术杂谈 2023年7月11日
    079
  • gauss杀进程

    1)查询当前所有连接的状态 select datname,pid,application_name,state from pg_stat_activity; 2)关闭当前state…

    技术杂谈 2023年7月24日
    074
  • Takeown–夺取文件or文件夹所有权

    强制将当前目录下的所有文件及文件夹、子文件夹下的所有者更改为管理员组(administrators)命令:takeown /f * /a /r /d y 将所有d:\documen…

    技术杂谈 2023年6月1日
    081
  • diary 开始

    恐惧一直是人类最大的敌人 恐惧来源于对敌人和自身力量的不确定 明天要体测了,跑1000米,随后体育课测2400米,中间隔了四天。我再次想起被体测支配的恐惧,那种不可名状的,压倒性的…

    技术杂谈 2023年7月24日
    078
  • 假如,程序员面试的时候说真话

    做程序员这么长时间了,经常能够听到一句话:面试造火箭,入职拧螺丝。而且,随着就业环境越来越卷,现在只会造火箭恐怕都不行了,得能造个空间站才行。 回想自己刚毕业那会儿,哪有什么八股文…

    技术杂谈 2023年6月21日
    094
  • checking for tgetent()… configure: error: NOT FOUND!

    今天centos出现了下面的异常: checking for tgetent()… configure: error: NOT FOUND! You need to insta…

    技术杂谈 2023年6月1日
    087
  • maven常见问题汇总

    主要记录一些学习及工作时遇到过的一些问题。 1 版本问题 由于版本兼容问题配置maven折腾了一点时间。例:IDEA 2019以上版本与maven3.6.3以上版本不兼容我的笔记本…

    技术杂谈 2023年7月24日
    066
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球