IoC和DI的基本概念及InversifyJS入门

5,732 阅读4分钟

概述

在前端的比较大型的框架构建中,依赖注入和控制反转已经是必不可少的设计原则。InversifyJS是当前相对最成熟的一个前端IoC(Inversion of Control)管理库。

IoC基本概念

Inversion of Control字面意思是控制反转,具体定义是高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。

举个例子🌰

类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。 解决方案:将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。

DI基本概念

依赖注入(Dependency Injection)其实和IoC是同根生,这两个原本就是一个东西,只不过由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以2004年大师级人物Martin Fowler又给出了一个新的名字:“依赖注入”。 类A依赖类B的常规表现是在A中使用B的instance。

举个例子🌰

注:官网使用了忍者做inversify的例子,所以这里我也用忍者,方便做对比

未使用依赖注入
interface Weapon {
    name: string;
}
interface Warrior {
    name: string;
    weapon: Weapon;
}
// 武士刀
class Katana implements Weapon {
    public name: string;
    public constructor() {
      this.name = "Katana";
    }
}
// 忍者
class Ninja implements Warrior {
    public name: string;
    public weapon: Weapon;
    public constructor() {
      this.name = "Ninja";
      const katana = new Katana();
      this.weapon = katana;
    }
}
const ninja = new Ninja();
console.log(ninja.weapon.name); // Katana

Ninja手里有一把Katana,所以现在Ninja依赖于Katana,我们在Ninja的构造函数中使用了Katana的实例。这是没有什么问题的,但是如果Katana改变了呢?比如Katana变成了name可以配置:

class Katana implements Weapon {
    public name: string;
    public constructor(name: string) {
      this.name = name;
    }
}

那么这个时候Ninja组件就会报错了:

我们就得修改Ninja里的代码。这显然是不合理的,我们希望Ninja只要用好Katana就行了,不要关注Katana到底做了什么。所以我们可以把Katana的实例通过Ninja的constructor传进去。

使用依赖注入

仅仅看改动的Ninja和调用部分:

class Ninja implements Warrior {
    public name: string;
    public weapon: Weapon;
    public constructor(katana: Katana) {
      this.name = "Ninja";
      this.weapon = katana;
    }
}
const ninja = new Ninja(new Katana("katana"));

这样无论Katana内部如何变,都不影响Ninja的使用了。🎉🎉🎉

问题

依赖注入的好处从上面的例子中我们已经清楚了,那么上面的案例有啥问题呢?假如Ninja除了有Katana,还有Shuriken(手里剑),那么又要把Shuriken的实例注入进来。Shuriken继续依赖别的组件的实例呢?这样的组件依赖一旦多了起来,他们之间的关系将变得难以维护,出现问题也难以排查。

InversifyJS

我们来解读一下官方的Basic示例 既然要遵循依赖抽象而不依赖具体实现,那首先定义一些接口(也就是抽象)

// file interfaces.ts
export interface Warrior {
    fight(): string;
    sneak(): string;
}
export interface Weapon {
    hit(): string;
}
export interface ThrowableWeapon {
    throw(): string;
}

inversify需要用type作为标识符,推荐使用Symbol来确保全局的唯一性,关于Symbol的用法可以看阮一峰的ES6相关文章:阮一峰Symbol教程

// file types.ts
const TYPES = {
    Warrior: Symbol.for("Warrior"),
    Weapon: Symbol.for("Weapon"),
    ThrowableWeapon: Symbol.for("ThrowableWeapon")
};
export { TYPES };

接下来定义一些类来实现上面的接口,注意所有实现上都要有@injectable装饰器。 当Ninja需要依赖Katana和Shuriken的时候,我们需要在注入的时候使用@inject装饰器

// file entities.ts
import { injectable, inject } from "inversify";
import "reflect-metadata";
import { Weapon, ThrowableWeapon, Warrior } from "./interfaces";
import { TYPES } from "./types";
@injectable()
class Katana implements Weapon {
    public hit() {
        return "cut!";
    }
}
@injectable()
class Shuriken implements ThrowableWeapon {
    public throw() {
        return "hit!";
    }
}

//  这里使用构造函数注入
@injectable()
class Ninja implements Warrior {
    private _katana: Weapon;
    private _shuriken: ThrowableWeapon;
    public constructor(
	    @inject(TYPES.Weapon) katana: Weapon,
	    @inject(TYPES.ThrowableWeapon) shuriken: ThrowableWeapon
    ) {
        this._katana = katana;
        this._shuriken = shuriken;
    }
    public fight() { return this._katana.hit(); }
    public sneak() { return this._shuriken.throw(); }
}
export { Ninja, Katana, Shuriken };

当然,我们也可以不在构造函数中注入,而在Ninja的属性中注入:

@injectable()
class Ninja implements Warrior {
    @inject(TYPES.Weapon) private _katana: Weapon;
    @inject(TYPES.ThrowableWeapon) private _shuriken: ThrowableWeapon;
    public fight() { return this._katana.hit(); }
    public sneak() { return this._shuriken.throw(); }
}

接下来我们要配置一个Container容器,推荐取名为inversify.config.ts,这个文件是整个项目中唯一存在耦合的地方,在项目其他地方不应该还有依赖关系

// file inversify.config.ts
import { Container } from "inversify";
import { TYPES } from "./types";
import { Warrior, Weapon, ThrowableWeapon } from "./interfaces";
import { Ninja, Katana, Shuriken } from "./entities";
const myContainer = new Container();
// Ninja是Warrior的实现,所以我们给他们绑定,Katana和Shuriken也一样
myContainer.bind<Warrior>(TYPES.Warrior).to(Ninja);
myContainer.bind<Weapon>(TYPES.Weapon).to(Katana);
myContainer.bind<ThrowableWeapon>(TYPES.ThrowableWeapon).to(Shuriken);
export { myContainer };

最后,我们来看一下运行的结果:

import { myContainer } from "./inversify.config";
import { TYPES } from "./types";
import { Warrior } from "./interfaces";

// 通过container的get<T>方法来创建实例
const ninja = myContainer.get<Warrior>(TYPES.Warrior);
console.log(ninja.fight()); // cut!
console.log(ninja.sneak()); // hit!

可以看到,我们的Ninja在没有注入Shuriken和Katana的实例的情况下,也能调用他们两个的实例方法了!

美滋滋🌺

参考文章: 依赖倒置原则 InversifyJS

未经允许,请勿转载!