在这个标题中,除了 JS 是乱入以外,其它的几个词汇都是存在一个共同点的,那就是依赖。ios
那么,依赖是什么呢?git
好比,如今我正在写这篇博客文,可是我得在电脑上编辑,电脑即是我完成这件事的依赖。而在代码中,最直观的体现是模块之间的依赖。如某个模块依赖另一个模块,那么另外的那个模块就是该模块的依赖。其实在上篇博客文章《JaVaScript中的模块》中,咱们也手写了一个模块依赖管理器。github
依赖这个理解起来很简单,但这不表明能够随意的依赖。在写模块的时候,讲究个高内聚低耦合,以提升模块的可拓展性和可维护性。模块依赖了谁,怎么去依赖,都关乎了最终模块的好与坏。编程
还好在编程界有着提升代码质量的金科玉律,咱们能够用理论来指导实践,写出更好的代码。axios
依赖反转原则(Dependency inversion principle,DIP),是一种特定的解耦形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。———— 维基百科设计模式
该原则规定:fetch
如今用一个例子来解释一波。ui
// Ajax.js
class Ajax {
get() {
return this.constructor.name;
}
}
export default Ajax;
// main.js
import Ajax from './Ajax';
class Main {
constructor() {
this.render()
}
render() {
let content = (new Ajax()).get();
console.log('content from', content);
}
}
new Main();
复制代码
刚开始的时候,咱们基于 XMLHttpRequest 对象,封装了 Ajax 用于请求数据。后来 fetch 出来了,咱们打算跟上时代的脚步,封装 fetch 以取代 Ajax。this
// Fetch.js
class Fetch {
fetch() {
return this.constructor.name;
}
}
export default Fetch;
// main.js
import Fetch from './Fetch';
class Main {
constructor() {
this.render();
}
render() {
let content = (new Fetch()).fetch();
console.log('content from', content);
}
}
new Main();
复制代码
从以上能够看出来,整个替代过程很麻烦,咱们须要找出封装请求模块(Ajax、Fetch)的全部引用,而后替换掉。又因为 Ajax、Fetch 的方法命名也是不一样,因此也须要对应地作更改。spa
这就是传统的处理依赖关系的方式。在这里 Main 是高层次模块,Ajax、Fetch 是低层次模块。依赖关系建立于高层次模块,且高层次模块直接依赖低层次模块,这种依赖关系限制了高层次模块的复用性。
依赖反转原则则颠倒这种依赖关系,并以上面提到的两个规定做为指导思想。
// Service.js
class Service {
request(){
throw `${this.constructor.name} 没有实现 request 方法!`
}
}
class Ajax extends Service {
request(){
return this.constructor.name;
}
}
export default Ajax;
// Main.js
import Service from './Service.js';
class Main {
constructor() {
this.render();
}
render() {
let content = (new Service).request();
console.log('content from', content);
}
}
new Main();
复制代码
在这里咱们把共同依赖的 Service 做为抽象接口,它就是高层次模块与低层次模块须要共同遵照的契约。在高层次模块中,它会默认 Service 会有 request 方法用来请求数据。在低层次模块中,它会听从 Service 复写应该存在的方法。这在《在JavaScript中尝试组合模式》中,不管分支对象仍是叶对象都实现 expense()
方法的道理差很少。
即便后来须要封装 axios 取代 fetch,咱们也只须要在 Service.js 中修改便可。
再次回顾下传统的依赖关系。
依赖关系建立于高层次模块,且高层次模块直接依赖低层次模块。
通过以上的折腾,咱们充其量只是解决了高层次模块直接依赖低层次模块的问题。那么依赖关系建立于高层次模块的问题呢?
若是说依赖反转原则告诉咱们该依赖谁,那么控制反转则告诉们谁应该来控制依赖。
像上面的 Main 模块,它依赖 Service 模块。为了得到 Service 实例的引用,Main 在内部靠自身 new
出了一个 Service 实例。这样明显地引用其它模块,无异加大了模块间的耦合。
控制反转(Inversion of Control,IoC),经过控制反转,对象在被建立的时候,有一个控制系统内全部对象的外界实体,将其所依赖的对象的引用传递给它。能够说,依赖被注入到对象中。———— 维基百科
这些话的意思就是将依赖对象的建立和绑定转移到被依赖对象类的外部来实现。实现控制反转最多见的方式是依赖注入,还有一种方式依赖查找。
依赖注入(Dependency Injection,DI),在软件工程中,依赖注入是种实现控制反转用于解决依赖性设计模式。一个依赖关系指的是可被利用的一种对象(即服务提供端)。依赖注入是将所依赖的传递给将使用的从属对象(即客户端)。该服务将会变成客户端的状态的一部分。传递服务给客户端,而非容许客户端来创建或寻找服务,是本设计模式的基本要求。
没看懂?不要紧。这句话讲的是,把过程放在外面,将结果带入内部。在《JaVaScript中的模块》中,咱们已经用到过依赖注入,就是对于依赖模块的模块,则把依赖做为参数使用
。
因此咱们再次改造下,
// Service.js
class Service {
request() {
throw `${this.constructor.name} 没有实现 request 方法!`
}
}
class Ajax extends Service {
request() {
return this.constructor.name;
}
}
export default Ajax;
// Main.js
class Main {
constructor(options) {
this.Service = options.Service;
this.render();
}
render() {
let content = this.Service.request();
console.log('content from', content);
}
}
export default Main;
// index.js
import Service from './Service.js';
import Main from './Main.js';
new Main({
Service: new Service()
})
复制代码
在 Main 模块中, Service 的实例化是在外部完成,并在 index.js
中注入。相比上一次,改动后的代码并无看出带来多大的好处。若是咱们再增长一个模块呢?
class Router {
constructor() {
this.init();
}
init() {
console.log('Router::init')
}
}
export default Router;
复制代码
# Main.js
+ this.Service = options.Router;
# index.js
+ import Router from './Router.js'
new Main({
+ Router: new Service()
})
复制代码
如果内部实例化就很差处理了。可换成依赖注入后,这个问题就很好解决了。
// utils.js
export const toOptions = params =>
Object.entries(params).reduce((accumulator, currentValue) => {
accumulator[currentValue[0]] = new currentValue[1]()
return accumulator;
}, {});
// Main.js
class Main {
constructor(options) {
Object.assign(this, options);
this.render();
}
render() {
let content = this.Service.request();
console.log('content from', content);
}
}
export default Main;
// index.js
import Service from './Service.js';
import Router from './Router.js';
import Main from './Main.js';
import { toOptions } from './utils.js'
/** * toOptions 转换成参数形式 * @params {Object} 类 * @return {Object} {Service: Service实例, Router: Router实例} */
const options = toOptions({Service, Router});
new Main(options);
复制代码
由于依赖注入把依赖的引用从外部引入,因此这里使用 Object.assign(this, options)
方式,把依赖所有加到了 this 上。即便再增长模块,也只须要在 index.js
中引入便可。
到了这里,DIP、IoC、DI 的概念应该有个清晰的认识了。而后咱们再结合实际,加个功能再次巩固一下。
做为一个功能独立的模块,通常都有个初始化的过程。
如今咱们要作的是遵照一个初始化的约定,定义一个抽象接口,
// Interface.js
export class Service {
request() {
throw `${this.constructor.name} 没有实现 request 方法!`
}
}
export class Init {
init() {
throw `${this.constructor.name} 没有实现 init 方法!`
}
}
// Service.js
import { Init, Service } from './Interface.js';
import { mix } from './utils.js'
class Ajax extends mix(Init, Service) {
constructor() {
super();
}
init() {
console.log('Service::init')
}
request() {
return this.constructor.name;
}
}
export default Ajax;
复制代码
Main、Service、Router 都依赖 Init 接口(在这里就是一种协定),Service 模块比较特殊,因此作了 Mixin 处理。要作到统一初始化,Main 还须要作些事。
// Main.js
import { Init } from './Interface.js'
class Main extends Init {
constructor(options) {
super();
Object.assign(this, options);
this.options = options;
this.render();
}
init() {
(Object.values(this.options)).map(item => item.init());
console.log('Main::init');
}
render() {
let content = this.Service.request();
console.log('content from', content);
}
}
export default Main;
复制代码
至此,结束
// index.js
import Service from './Service.js';
import Router from './Router.js';
import Main from './Main.js';
import { toOptions } from './utils.js'
/** * toOptions * 转换成参数形式 * @params {Object} 类 * @return {Object} * { * Service: Service实例, * Router: Router实例 * } */
const options = toOptions({ Service, Router });
(new Main(options)).init();
// content from Ajax
// Service::init
// Router::init
// Main::init
复制代码
(以上全部示例可见 GitHub)