IoC

Posted by huangqing on March 1, 2019

Ioc

什么是 IoC

IoC 的全称叫做 Inversion of Control,可翻译为为「控制反转」「依赖倒置」,它主要包含了三个准则:

  1. 高层次的模块不应该依赖于低层次的模块,它们都应该依赖于抽象
  2. 抽象不应该依赖于具体实现,具体实现应该依赖于抽象
  3. 面向接口编程 而不要面向实现编程

Ioc 演进

演进 1-简单的 App

假设需要构建一款应用叫 App,它包含一个路由模块Router 和一个页面监控模块 Track

app.js

import Router from "./modules/Router";
import Track from "./modules/Track";

class App {
  constructor(options) {
    this.options = options;
    /*
          问题:
          
          这里,在构造函数中对模块 Router 和 Track 生成了实例。

          实际应用中需求是非常多变的,可能需要给路由新增功能(比如实现 history 模式)或者更新配置(启用 history, new Router({ mode: 'history' }))。
          这就不得不在 App 内部去修改这两个模块,这是一个 INNER BREAKING 的操作,而对于之前测试通过了的 App 来说,也必须重新测试。
          
          很明显,这不是一个好的应用结构,高层次的模块 App 依赖了两个低层次的模块 Router 和 Track,
          对低层次模块的修改都会影响高层次的模块 App。

          那么如何解决这个问题呢,解决方案就是接下来要讲述的 依赖注入(Dependency Injection)。

          主要的方式就是不要在构造函数中使用 new Router() 这样的方式去生成实例,
          而是将生成好的实例作为参数传进来。
        */
    this.router = new Router();
    this.track = new Track();

    this.init();
  }

  init() {
    window.addEventListener("DOMContentLoaded", () => {
      this.router.to("home");
      this.track.tracking();
      this.options.onReady();
    });
  }
}

index.js

// index.js
import App from "path/to/App";
new App({
  onReady() {
    // do something here...
  }
});

演进 2-依赖注入(Dependency Injection)

所谓的依赖注入,简单来说就是把高层模块所依赖的模块通过传参的方式把依赖「注入」到模块内部,上面的代码可以通过依赖注入的方式改造成如下方式:

app.js

class App {
  constructor(options) {
    this.options = options;
    //演进2-可以看到 router 和 track 的实例是通过参数 options 传入的。
    //通过依赖注入解决了上面所说的 INNER BREAKING 的问题,可以直接在 App 外部对各个模块进行修改而不影响内部。
    this.router = options.router;
    this.track = options.track;

    this.init();
  }

  init() {
    window.addEventListener("DOMContentLoaded", () => {
      /*
            问题:演进2

            虽然 App 通过依赖注入的方式在一定程度上解耦了与其他几个模块的依赖关系,但是还不够彻底,
            其中的 this.router 和 this.track 等属性其实都还是对「具体实现」的依赖,
            明显违背了 IoC 思想的准则。
            
            那如何进一步抽象 App 模块呢,将下面具体的初始化工作使用接口实现,如 init。
            将具体的初始化工作放到 Router 、Track 模块中,实现接口 init
            */

      this.router.to("home");
      this.track.tracking();
      this.options.onReady();
    });
  }
}

index.js

import App from "path/to/App";
import Router from "./modules/Router";
import Track from "./modules/Track";

new App({
  //演进2-在实例化App时生成了相关的实例
  router: new Router(),
  track: new Track(),
  onReady() {
    // do something here...
  }
});

演进 3-依赖接口

经过改造后 App 内已经没有「具体实现」了,看不到任何业务代码了。

app.js

class App {
  static modules = [];
  constructor(options) {
    this.options = options;
    this.init();
  }
  init() {
    window.addEventListener("DOMContentLoaded", () => {
      //演进3: 通过initModules统一进行实例化和初始化工作
      this.initModules();
      this.options.onReady(this);
    });
  }

  /*
    演进3: 通过 App.use() 方法来「注入」依赖,
    在 ./modules/some-module.js 中按照一定的「约定」去初始化相关配置,
    比如此时需要新增一个 Share 模块的话,无需到 App 内部去修改内容。

    App.use 做了一件非常简单的事情,就是把依赖保存在了 App.modules 属性中,等待后续初始化模块的时候被调用。
    */
  static use(module) {
    Array.isArray(module)
      ? module.map(item => App.use(item))
      : App.modules.push(module);
  }
  /*
    可以发现该方法同样做了一件非常简单的事情,就是遍历 App.modules 中所有的模块,
    判断模块是否包含 init 属性且该属性必须是一个函数,
    如果判断通过的话,该方法就会去执行模块的 init 方法并把 App 的实例 this 传入其中,以便在模块中引用它。

    这其实就是 IoC 思想中对「面向接口编程 而不要面向实现编程」这一准则的很好的体现。App 不关心模块具体实现了什么,只要满足对 接口 init 的「约定」就可以了。

    从这个方法中可以看出,要实现一个可以被 App.use() 的模块,就必须满足两个「约定」:
    1. 模块必须包含 init 属性
    2. init 必须是一个函数
    */
  initModules() {
    App.modules.map(
      module =>
        module.init && typeof module.init == "function" && module.init(this)
    );
  }
}

modules/Share.js

import Share from "path/to/Share";
export default {
  //App 中 module 接口init的具体实现
  init(app) {
    app.share = new Share();
    app.setShare = data => app.share.setShare(data);
  }
};

index.js

//加载一个新的模块
App.use(Share);
new App({
  // ...
  onReady(app) {
    app.setShare({
      title: "Hello IoC.",
      description: "description here..."
      // some other data here...
    });
  }
});

总结

App 模块此时应该称之为「容器」比较合适了,跟业务已经没有任何关系了,它仅仅只是提供了一些方法来辅助管理注入的依赖和控制模块如何执行。

控制反转(Inversion of Control)是一种「思想」,依赖注入(Dependency Injection)则是这一思想的一种具体「实现方式」,而这里的 App 则是辅助依赖管理的一个「容器」。