Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
Pines-Cheng opened this issue Jan 5, 2021 · 2 comments

Comments

@Pines-Cheng
Copy link
Owner

Pines-Cheng commented Jan 5, 2021

前言

SOLID

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

五个原则分别是:

  • Single-responsibility principle 单一责任原则
  • Open–closed principle 开闭原理
  • Liskov substitution principle Liskov 里氏代换原则
  • Interface segregation principle 接口隔离原则
  • Dependency inversion principle 依赖反转原则

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

IoC

  • IoC:Inversion of Control (IoC) 是软件工程(software engineering)中的一种编程原则(programming principle),在面向对象编程或其他编程范式中得到应用。IoC inverts the flow of control as compared to traditional control flow。

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:

  • service locator pattern 服务定位器模式
  • dependency injection 依赖注入(构造器注入、参数注入、Setter 注入、Interface 注入)
  • contextualized lookup 上下文化查找
  • template method design pattern 模板方法设计模式
  • strategy design pattern 策略设计模式

所以,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 个目标:

  • 允许 JavaScript 开发人员编写遵循 SOLID 原则的代码。
  • 促进和鼓励遵守最好的 OOP 和 IoC 实践。
  • 尽可能少地增加运行时开销。
  • 提供最先进的开发经验

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语法。

  • BindingToSyntax:指定class绑定到容器内的类型,从写法来说:container.bind(A).toXXX(),很好理解成绑定类A为(to)XXX。这里的XXX可以有以下几种:
  • to():必须传入一个构造器,定义的类型是:BindingTypeEnum.Instance,后续在使用的时候会new掉这个构造器
  • toSelf():to写法的一种简写方式,内部最后还是调用to
  • toConstantValue():绑定为常量,传入的是一个初始化后的实例,定义的类型是:BindingTypeEnum.ConstantValue
  • toDynamicValue():绑定为动态值,在获取的时候会去执行对应的函数,定义的类型是:BindingTypeEnum.DynamicValue
  • toConstructor():绑定为构造函数,在获取之后需要自己实例化,定义的类型是:BindingTypeEnum.Constructor
  • toFactory():绑定为工厂函数,与刚才的动态值不一样,动态值会执行完动态函数返回值,而工厂函数则会返回一个高阶函数,允许你进一步定制值,定义的类型是:BindingTypeEnum.Factory
  • toFunction():绑定为函数,其实就是toConstantValue的别名,定义的类型为:BindingTypeEnum.Function
  • toAutoFactory():绑定为自动工厂函数,此时的工厂函数不用开发者提供,内部自己实现掉了,定义的类型为:BindingTypeEnum.Factory
  • toProvider():绑定为一个异步的工厂函数,称之为Provider,对于需要一些异步操作的时候这种方式非常有用,定义的类型为:toProvider
  • toService():绑定为一个服务,让其解析为以前声明过的别的类型绑定,这个绑定很特殊,没有别的任何后续操作,因为它没有返回值。

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

image

使用

支持 Symbols

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

使用步骤

  • Step 1: 声明接口和类型
  • Step 2: @Injectable & @Inject decorators 声明依赖
  • Step 3: 创建并配置一个 Container
  • Step 4: 获取并使用

声明依赖的 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
Copy link
Owner Author

@Pines-Cheng
Copy link
Owner Author

Pines-Cheng commented Oct 21, 2021

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

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

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

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

参考:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant