概念明确
控制反转(英语:Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)
控制反转是一种(面向对象中的)思想、设计原则,依赖注入则是这一思想的一种「实现模式」。同时,依赖注入在不用的语言和场景也有不同的具体实现。
控制反转
控制反转有多种语义
控制反转, 也就是把自己程序一部分的执行控制交给某个第三方。即程序的调用方式,时间,次数等,不由定义方“控制”。
在《你不知道的 JavaScript(中卷)》2.3 中提到回调函数中“信任问题”的例子
analytics.trackPurchase(purchaseData, function () { chargeCreditCard(); displayThankyouPage(); });
《你不知道的 JavaScript》作者认为回调函数的方式,导致了“控制反转”(回调函数的执行方式的控制权在第三方程序手上)。并且认为此时的“控制反转”是”有害的“。例如上面例子中,支付的回调代码被重复执行了五次,或者压根没有执行(比如第三方程序运行出错,终止了运行)。
我们把这称为控制反转(inversion of control), 也就是把自己程序一部分的执行控制交给某个第三方。在你的代码和第三方工具(一组你希望有人维护的东西)之间有一份并「没有明确表达的契约」 。
……
回调最大的问题是控制反转,它会导致信任链的完全断裂。
这个问题本质是异步编程的问题,书中列了两种方式优化这种情况。一种是 Nodejs 中常见的
error first
模式。就是回调函数的一个参数是 error。dosomethingSync(function(error,...args) { if(error){ ... } ... });
这样能对第三方程序中不受控的错误情况进行处理。
第二种就是使用 promise,其实也是对一种的优化。其实同样存在”控制反转“。只不过第三方程序不控制调用了,而是维护 promise 的状态,有且仅有一种决议状态(要么完成,要么拒绝)。但是也不由使用者控制回调函数的调用,而是由 promise 控制。这就是一种 「有明确表达的契约」。
第二种语义是讨论最广的,也是各个百科词条中的释义。
“哪些方面的控制被反转了?”。
Martin Fowler总结出是 依赖对象的获得被反转了 。即依赖的哪个具体的实例对象被调用,不由调用方“控制”了。
因为大多数应用程序都是由两个或是更多的类,通过彼此的合作来实现业务逻辑。这使得每个对象都需要获取与其合作的对象(也就是它所依赖的对象)的引用。如果这个获取过程要靠自身实现,那么这将导致代码高度耦合,并且难以维护和调试。
Class A 中用到了 Class B 的对象 b,一般情况下,需要在 A 的代码中显式的 new 一个 B 的对象。
采用依赖注入技术之后,A 的代码只需要定义一个私有的 B 对象,不需要直接 new 来获得这个对象,而是通过相关的容器控制程序来将 B 对象在外部 new 出来并注入到 A 类里的引用中。而具体获取的方法、对象被获取时的状态由配置文件(如 XML)来指定。
所谓“反转”依赖对象的获得,是指:依赖的实际对象,不是在写作代码时,由开发者指定;而是在运行过程中,由运行时框架决定。用伪代码表示,大概是这样的:
// framework.js const fruitMap = new Map(); export function inject(instance) { // do inject instance.afterInjected(); } export function map(key, value) { } // tropical.js import framework from './framework'; export default class Tropical() { construct() { this.inject = ['fruit']; framework.inject(this); } afterInjected() { } } // index.js import framework from './framework'; // 简陋的依赖注入 if ( // 某种逻辑) { framework.map('fruit', new Fruit('香蕉')); } else ( // 另外一种逻辑) { framework.map('fruit', new Fruit('西瓜')); } const tropical = new Tropical();
下面提到的控制反转,都指语义 2。
依赖注入
所谓的依赖注入,简单来说就是把高层模块所依赖的模块通过传参的方式把依赖「注入」到模块内部。引用自前端中的 IoC 理念。
给予调用方它所需要的事物。 “依赖”是指可被方法调用的事物。依赖注入形式下,调用方不再直接指使用“依赖”,取而代之是“注入” 。“注入”是指将“依赖”传递给调用方的过程。在“注入”之后,调用方才会调用该“依赖”。传递依赖给调用方,而不是让让调用方直接获得依赖,这个是该设计的根本需求。
依赖注入是控制反转的最为常见的一种技术。
// app.js
class App {
static modules = []
constructor(options) {
this.options = options;
this.init();
}
init() {
window.addEventListener('DOMContentLoaded', () => {
this.initModules();
this.options.onReady(this);
});
}
static use(module) {
Array.isArray(module) ? module.map(item => App.use(item)) : App.modules.push(module);
}
initModules() {
App.modules.map(module => module.init && typeof module.init == 'function' && module.init(this));
}
}
// modules/Router.js
import Router from 'path/to/Router';
export default {
init(app) {
app.router = new Router(app.options.router);
app.router.to('home');
}
};
// modules/Track.js
import Track from 'path/to/Track';
export default {
init(app) {
app.track = new Track(app.options.track);
app.track.tracking();
}
};
// index.js
import App from './app.js';
import Router from './modules/Router';
import Track from './modules/Track';
App.use([Router, Track]);
new App({
router: {
mode: 'history',
},
track: {
// ...
},
onReady(app) {
// app.options ...
},
});
通过参数传递依赖,是一种依赖注入,回调函数当然也是一种依赖注入。通过装饰器,完成对接口方法的替换,也是一种依赖注入。只不过有的方式更复杂更高级,拓展性更好。具体实现贴合需求就好。
依赖注入有如下实现方式:
- 基于接口。实现特定接口以供外部容器注入所依赖类型的对象。
- 基于 set 方法。实现特定属性的 public set 方法,来让外部容器调用传入所依赖类型的对象。
- 基于构造函数。实现特定参数的构造函数,在新建对象时传入所依赖类型的对象。
- 基于注解。基于Java 的注解功能,在私有变量前加“@Autowired”等注解。
依赖注入,并不一定就为了实现控制反转。例如 vue 中的Provide/Inject
,react 中Provider/Consumer
,angularjs。依赖注入遵循了 依赖反转原则 和单一职责原则。
这里又出现了一个词,依赖反转。
依赖反转原则(依赖倒置原则)
依赖反转和控制反转,乍一看太像了,在含义上也和上面提到的第二种语义很像。维基百科-依赖反转
在面向对象编程领域中,依赖反转原则(Dependency inversion principle,DIP)是指一种特定的解耦(传统的依赖关系创建在高层次上,而具体的策略设置则应用在低层次的模块上)形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。
该原则规定:
- 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
- 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口的需求(这一点应该从设计实现的角度理解,即需求决定了实现)。
个人理解:依赖反转的含义比控制反转更广。控制反转是面向对象编程中的思想,注入对象的实例。控制反转也是源于依赖反转。
参考
- 前端中的 IoC 理念
- 前端理解依赖注入(控制反转) ,文章不完全准确,参考评论区置顶评论。
- 维基百科-依赖反转
- 维基百科-控制反转
- 维基百科-依赖注入
- 《你不知道的 JavaScript 中卷》