一、什么是 IoC
IoC
的全称叫做 Inversion of Control
,可翻译为为「控制反转」或「依赖倒置」,它主要包含了三个准则:
- 高层次的模块不应该依赖于低层次的模块,它们都应该依赖于抽象
- 抽象不应该依赖于具体实现,具体实现应该依赖于抽象
- 面向接口编程 而不要面向实现编程
概念总是抽象的,所以下面将以一个例子来解释上述的概念。假设需要构建一款应用叫 App
,它包含一个路由模块 Router
和一个页面监控模块 Track
,一开始可能会这么实现:
1 | // app.js |
嗯,看起来没什么问题,但是实际应用中需求是非常多变的,可能需要给路由新增功能(比如实现 history
模式)或者更新配置(启用 history
, new Router({ mode: 'history' })
)。这就不得不在 App
内部去修改这两个模块,这是一个 INNER BREAKING
的操作,而对于之前测试通过了的 App
来说,也必须重新测试。
很明显,这不是一个好的应用结构,高层次的模块 App
依赖了两个低层次的模块 Router
和 Track
,对低层次模块的修改都会影响高层次的模块 App
。那么如何解决这个问题呢,解决方案就是接下来要讲述的 依赖注入(Dependency Injection)。
二、依赖注入
所谓的依赖注入,简单来说就是把高层模块所依赖的模块通过传参的方式把依赖「注入」到模块内部,上面的代码可以通过依赖注入的方式改造成如下方式:
1 | // app.js |
可以看到,通过依赖注入解决了上面所说的 INNER BREAKING
的问题,可以直接在 App
外部对各个模块进行修改而不影响内部。
是不是就万事大吉了?理想很丰满,但现实却是很骨感的,没过两天产品就给你提了一个新需求,给 App
添加一个分享模块 Share
。这样的话又回到了上面所提到的 INNER BREAKING
的问题上:你不得不对 App
模块进行修改加上一行 this.share = options.share
,这明显不是我们所期望的。
虽然 App
通过依赖注入的方式在一定程度上解耦了与其他几个模块的依赖关系,但是还不够彻底,其中的 this.router
和 this.track
等属性其实都还是对「具体实现」的依赖,明显违背了 IoC
思想的准则,那如何进一步抽象 App
模块呢。
三、面向接口编程
1 | class App { |
经过改造后 App
内已经没有「具体实现」了,看不到任何业务代码了,那么如何使用 App
来管理我们的依赖呢:
1 | // modules/Router.js |
可以发现 App
模块在使用上也非常的方便,通过 App.use()
方法来「注入」依赖,在 ./modules/some-module.js
中按照一定的「约定」去初始化相关配置,比如此时需要新增一个 Share
模块的话,无需到 App
内部去修改内容:
1 | // modules/Share.js |
直接在 App
外部去 use
这个 Share
模块即可,对模块的注入和配置极为方便。
这其实就是 IoC
思想中对「面向接口编程 而不要面向实现编程」这一准则的很好的体现。App
不关心模块具体实现了什么,只要满足对 接口 init
的「约定」就可以了。
四、总结
App
模块此时应该称之为「容器」比较合适了,跟业务已经没有任何关系了,它仅仅只是提供了一些方法来辅助管理注入的依赖和控制模块如何执行。
控制反转(Inversion of Control
)是一种「思想」,依赖注入(Dependency Injection
)则是这一思想的一种具体「实现方式」,而这里的 App
则是辅助依赖管理的一个「容器」。
注:直接依赖 =》 传参注入依赖 =》 接口注入依赖