Pines-Cheng / blog

技术博客
https://pines-cheng.github.io/blog/
546 stars 42 forks source link

从 InversifyJS 学习 IoC 的概念、实现以及在 JS 中的应用 #86

Open Pines-Cheng opened 3 years ago

Pines-Cheng commented 3 years ago

前言

SOLID

面向对象编程(object-oriented computer programming)中,SOLID 是五个设计原则(design principles)的缩写。

五个原则分别是:

因此,DIP:Dependency inversion principle,是面向对象编程中的设计原则之一,通过共享抽象解耦高层和低层的关系。

IoC

Software frameworks, callbacks, schedulers, event loops, dependency injection, and the template method are examples of design patterns that follow the inversion of control principle, although the term is most commonly used in the context of object-oriented programming.

Inversion of control is sometimes facetiously referred to as the "Hollywood Principle: Don't call us, we'll call you".

控制反转有时被戏称为好莱坞原则: 不要打电话给我们,我们会打给你。

反转:有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;

在面向对象编程中,有几个基本的方式可以实现 IoC:

所以,DI(dependency injection)是面向对象编程中 IoC 的一种实现的方式。

IoC 容器:依赖注入的框架,用来映射依赖,管理对象创建和生存周期(DI框架)。

InversifyJS 介绍

InversifyJS 是一个强大的、轻量级的 IoC 容器,使用 TS 编写,可用于构建 JS/Node 应用。InversifyJS 使用 class constructor 来定义和注入依赖,API 设计简单友好,方便你使用 OOP 和 IoC 的最佳编程实践。

JavaScript 现在支持基于类的继承的面向对象(OO)编程。这些特征是伟大的,但事实是,他们也是危险的。

我们需要一个好的 OO 设计(SOLID、复合重用(Composite Reuse)等)来保护我们自己免受这些威胁。问题是面向对象的设计是困难的,这正是我们创建 inversion.js 的原因。

InversifyJS 的开发为了实现 4 个目标:

InversifyJS 需要现代的 JavaScript engine 支持:

基础概念

Container

容器本身就是一个类实例,而 inversify 要做的就是利用这么一个类实例来管理诸多别的类实例,而且依靠一套有序的方法实现。

容器本身还有父容器和子容器的概念,所以 Container 对象有一个字段 parent 来表示,这样可以做到继承。这个概念在使用Container.resolve 的时候有用到。

Scope

在 inversify.js 中,或者说是在 IoC 的概念中存在一个叫做 scope 的单词,它是和 class 的注入关联在一起的。一个类的注入 scope可以支持以下三种模式:

Transient:每次从容器中获取的时候(也就是每次请求)都是一个新的实例 Singleton:每次从容器中获取的时候(也就是每次请求)都是同一个实例 Request:社区里也成为Scoped模式,每次请求的时候都会获取新的实例,如果在这次请求中该类被require多次,那么依然还是用同一个实例返回。

Scope可以全局配置,通过defaultScope参数传参进去,也可以针对每个类进行区别配置,使用方法是:

container.bind<Shuriken>("Shuriken").to(Shuriken).inTransientScope(); // Default
container.bind<Shuriken>("Shuriken").to(Shuriken).inSingletonScope();
container.bind<Shuriken>("Shuriken").to(Shuriken).inRequestScope();

TS 的装饰器

参考:TS - 装饰器

Reflect Metadata

实现架构

Inversify在真正解析一个依赖之前会执行三个必须的操作(另外包含两个可选操作):

  1. 注解阶段(Annotation)
  2. 计划阶段(Planning)
  3. 中间件(这个是可选的步骤)
  4. 解析阶段(Resolution)
  5. 激活(这个也是可选的步骤)

inversify的绑定过程

除了to语法,其余的语法其实都是在往Binding这个类实例的属性赋值。

就是下面这些属性:

interface Binding<T> extends Clonable<Binding<T>> {
        id: number;
        moduleId: string;
        activated: boolean;
        serviceIdentifier: ServiceIdentifier<T>;
        constraint: ConstraintFunction;
        dynamicValue: ((context: interfaces.Context) => T) | null;
        scope: BindingScope;
        type: BindingType;
        implementationType: Newable<T> | null;
        factory: FactoryCreator<any> | null;
        provider: ProviderCreator<any> | null;
        onActivation: ((context: interfaces.Context, injectable: T) => T) | null;
        cache: T | null;
    }

所有的入口都是指向BindingToSyntax这个类,再往外衍生出各种when语法。

上面的所有绑定除了最后一个,都会返回一个when/on/in语法供开发者往绑定里面加入更多的元素,比如一些限制条件、指定生效scope等等,接下来的演变如下图,只有to和toDynamicValue才支持in操作,所有其走的路线是inWhenOn,其余的都是WhenOn路线:

image

使用

支持 Symbols

在非常大的应用程序中,使用字符串作为将由 inversionjs 注入的类型的标识符,可能会导致命名冲突。支持并推荐使用 Symbols 而不是字符串文字。

使用步骤

声明依赖的 Demo 如下,具体可以参考 Readme。

@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(); }

}

也可以使用 property injection 代替 constructor injection ,这样就不用声明构造函数。

@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(); }
}

创建和配置容器:

const myContainer = new Container();
myContainer.bind<Warrior>(TYPES.Warrior).to(Ninja);
myContainer.bind<Weapon>(TYPES.Weapon).to(Katana);
myContainer.bind<ThrowableWeapon>(TYPES.ThrowableWeapon).to(Shuriken);

使用

const ninja = myContainer.get<Warrior>(TYPES.Warrior);

expect(ninja.fight()).eql("cut!"); // true

示例

Angular

在 Angular 2+ 的版本中,控制反转与依赖注入便是基于此实现,现在,我们来实现一个简单版:

type Constructor<T = any> = new (...args: any[]) => T;

const Injectable = (): ClassDecorator => target => {};

class OtherService {
  a = 1;
}

@Injectable()
class TestService {
  // 使用参数属性,把声明和赋值合并至一处
  constructor(public readonly otherService: OtherService) {}

  testMethod() {
    console.log(this.otherService.a);
  }
}

const Factory = <T>(target: Constructor<T>): T => {
  // 获取所有注入的服务
  const providers = Reflect.getMetadata('design:paramtypes', target); // [OtherService]
  const args = providers.map((provider: Constructor) => new provider());
  return new target(...args);
};

Factory(TestService).testMethod(); // 1

参考

Pines-Cheng commented 3 years ago

Introduction to “reflect-metadata” package and its ECMAScript proposal

Pines-Cheng commented 3 years ago

InversifyJS 是一个 JavaScript 依赖注入库,功能强大,轻量级,使用简单。但是,将它与 React 一起作为组件特性使用仍然具有挑战性。

这是因为 inversion.js 使用构造函数注入,而 React 不允许用户扩展其组件的构造函数。因此,在 React Component 里面是获取不到 @inject 的实例的。

然而,让我们来看看几个可以用来扩展其行为的 inversion.js 扩展库。

  1. 使用 inversify-inject-decorator
  2. 使用 inversify-react
  3. 使用 react-inversify

参考: