Inversion of Control
什么是控制反转?
控制反转是指程序的流程控制权相对于传统的面向过程编程而言发生了反转。下面是维基百科的描述
In software engineering, inversion of control (IoC) is a programming principle. IoC inverts the flow of control as compared to traditional control flow.
看到这里大家可能会觉得云里雾里的…控制反转(Inversion of Control)实际是控制(Control)和反转(Inversion)两个词的组合,所以拨开云雾的关键在于理解控制和反转。
怎么理解“控制”?
控制是指程序的流程控制权,用来决定程序的运行方式和流程。举个例子,想必大家都知道,要把大象放进冰箱需要三步:1)打开冰箱门;2)把大象放进冰箱;3)关上冰箱门。用一段代码用表示的话,代码可能长这样
void putElephantToRefrigerator() {
打开冰箱门;
放进大象;
关闭冰箱门;
}
int main() {
putElephantToRefrigerator();
return 0;
}
不过,冰箱可能已经被塞满放不下大象了,我们需要把冰箱里没用的东西清理掉,再把大象团成团放进去,代码会变成这样
void putElephantToRefrigerator() {
打开冰箱门;
清理冰箱;
把大象团成团;
放进大象;
关闭冰箱门;
}
...
当然,我们完全可以先把大象团成团,像这样
void putElephantToRefrigerator() {
把大象团成团;
打开冰箱门;
清理冰箱;
放进大象;
关闭冰箱门;
}
...
所以,我们要把大象放进冰箱里都会经过一个流程,目前这个流程的具体内容是由我们自己控制的。
怎么理解“反转”?
反转指的是状态变化,既然是变化那就需要一个参照,这个参照就是传统的面向过程编程实践。那么到底是哪里和面向过程编程不一样呢?具体是什么发生了反转呢?答案是程序的流程控制权。继续看上面的例子,假如我们有一款极其智能的冰箱,不需要手动开门、关门,只要关心放什么东西进去、怎么放进去(nice,事情变得更简单了),代码会变成这样(考虑设计模式-模板方法)
typedef struct AutoRefrigerator;
struct MyRefrigerator : AutoRefrigerator {
private void putSomething() {
...
}
}
int main() {
MyRefrigerator refrigerator;
refrigerator.put(elephant);
return 0;
}
上面的代码中语句“refrigerator.put(elephant);”会包含多条子语句(open、close、putSomething…),但AutoRefrigerator只有putSomething的定义但没具体实现,这个操作需要我们自己定制的。
到目前为止,控制程序流程的不是我们的代码而是框架提供的AutoRefrigerator,程序流程控制权发生了反转,控制权由我们自己的代码转移到了框架中。下面是关于控制反转前后的一些变化
- 反转前:我们自己的代码决定应用程序的工作流程,决定什么时候调用哪些函数(我们自己的代码是甲方,有时兼任乙方,第三方代码是乙方)
- 反转后:第三方库的代码(框架)决定程序的工作流程,开发人员只管按照框架给定的接口提供函数(我们自己的代码是乙方,第三方代码是甲方)
可见,在控制反转前我们自己的代码会调用第三方库的代码,而控制反转后则是由第三方库(框架)的代码调用我们的代码,这种调用关系的变化也可以理解成控制反转的反转内容。
有什么好处?
好处?怎么可以这么物质?这个问题超纲了,这里不谈…笔者是个物质的人,所以只谈谈有什么好处,好处可以用两个字概括:复用。
复用代码有三种方式:类库、框架、设计模式。
- 类库:定义一组可复用的代码,供其他程序调用。拿来主义,别人的东西拿来用,用别人的锤子砸核桃。
- 框架:定义一组工作结构,开发人员通过预留的接口插入代码(做填空题),完成应用程序。把自己的锤子装在流水线上,让它砸核桃。
- 设计模式:???
控制反转为框架提供了可能性。框架的主要作用是在一类场景下复用流程,由于某些步骤的具体操作需要定制,因此框架需要调用我们写的代码,控制反转提供了这种可能性。控制反转和编码原则之一的**好莱坞原则(It calls me rather me calling the framework)**也有着莫名的联系。
另外,按照Martin Fowler的描述,控制反转还有Seperating Configuration from Use的作用。实际上这是一些容器的主要作用(如Spring),而这些容器又不可避免地使用了控制反转。
怎么实践?
说了这么多,又有这么诱人的好处,怎么才能得到它呢?最简单的实践就是设计模式-模板方法,不过最常见的实践方法还是依赖注入和依赖查询。
依赖注入(DI:Dependency Inject):被动接收依赖对象,由容器将被依赖对象注入到对象内部; 依赖查询(DL:Dependency Lookup):主动查询依赖对象,由对象自身通过 服务定位器 查询被依赖对象;依赖查询也经常以服务定位器模式Service Locator的形式出现。
DI | DL |
---|
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8CRScLNl-1646914553258)(/Users/nirui/myworkspace/note/DI.png)] | [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kekeEGNP-1646914553261)(/Users/nirui/myworkspace/note/DL.png)] | Context依赖各事例 | 各实例依赖context | 通过构造函数、setter函数等容易查看依赖关系 | 依赖关系不容易查看,需要分析调用locator的源码分析依赖关系 | 装配关系在业务类外的配置文件中 | 装配关系在业务类中 | 被动接收依赖对象 | 主动查询依赖对象 |
这两种方式的主要区别在于配置方式不同、获得依赖对象的方式不同。在代码中该怎么选择DI还是DL呢?Martin Fowler有话说
So the decision between locator and injector depends on whether that dependency is a problem.
参考连接
https://www.martinfowler.com/bliki/InversionOfControl.html https://www.martinfowler.com/articles/injection.html https://en.wikipedia.org/wiki/Inversion_of_control
|