PGYou-bbt | 理解前端开发中的控制反转和依赖注入

理解前端开发中的控制反转和依赖注入

2021-04-07

概念明确

控制反转(英语:Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)

控制反转是一种(面向对象中的)思想、设计原则,依赖注入则是这一思想的一种「实现模式」。同时,依赖注入在不用的语言和场景也有不同的具体实现。

控制反转

控制反转有多种语义

  1. 控制反转, 也就是把自己程序一部分的执行控制交给某个第三方。即程序的调用方式,时间,次数等,不由定义方“控制”。

    在《你不知道的 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 控制。这就是一种 「有明确表达的契约」。

  2. 第二种语义是讨论最广的,也是各个百科词条中的释义。

    “哪些方面的控制被反转了?”。

    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)是指一种特定的解耦(传统的依赖关系创建在高层次上,而具体的策略设置则应用在低层次的模块上)形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。

该原则规定:

  1. 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口
  2. 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口的需求(这一点应该从设计实现的角度理解,即需求决定了实现)。

个人理解:依赖反转的含义比控制反转更广。控制反转是面向对象编程中的思想,注入对象的实例。控制反转也是源于依赖反转。

参考

  1. 前端中的 IoC 理念
  2. 前端理解依赖注入(控制反转) ,文章不完全准确,参考评论区置顶评论。
  3. 维基百科-依赖反转
  4. 维基百科-控制反转
  5. 维基百科-依赖注入
  6. 《你不知道的 JavaScript 中卷》
Copyright ©PGYou 2022