概述
在前端的比较大型的框架构建中,依赖注入和控制反转已经是必不可少的设计原则。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
未经允许,请勿转载!