大话设计模式——多应用实例下的IOC隔离

前言

本文主要介绍IOC(控制反转)容器与DI(依赖注入)的Typescript实现,如何解决多应用实例下的隔离问题。

如果一个应用极度复杂(多模块/多实例/多事件/多交互/多流程),那么使用DDD设计模式+事件驱动,可能是最优解,而该设计底层必须要实现IOC容器。而web端业务常常会出现多应用实例场景,比如页面上有两个Tab页签或弹窗,其内部分别是两个独立的完整应用。这就必须要求实现IOC容器的隔离。

IOC容器隔离的两种实现思路

容器隔离的核心问题是,如何在构造实例时,将该实例放入对应的IOC容器。对应的思路有两种:

  1. 先在全局创建不同容器,再通过该容器注册应用内的类
  1. 创建不同应用时,对该应用创建不同容器,逐层构造注入的依赖实例的同时,也对该实例注入容器。

方案1的实现可以参考社区优秀开源方案:inversify。不过方案2更符合直觉,本文也主要介绍方案2的实现。

单例/多例与IOC容器隔离

IOC容器隔离,也就是两个应用之间各自创建自己的实例对象,这些对象共处同一IOC容器中,但仍然要保持全局对象的单例性。同时应用内也需要支持某些class是多例的。

比如下面例子中,DB对象全局唯一,而Store对象在应用内多例,EventBus与TaskService/UserService应用内单例,

typescript 复制代码
@Scoped(Scope.Global)
@Injectable()
class DB {
  constructor() {
    console.log("db");
  }
}

@Scoped(Scope.Transient)
@Injectable()
class Store {
  constructor() {
    console.log("store");
  }
}
@Scoped(Scope.Singleton)
@Injectable()
class EventBus {
  constructor() {
    console.log("event bus");
  }
  emit() {
    console.log("emit");
  }
}
@Injectable()
class TaskService {
  @Inject() store!: Store;
  @Inject() eventBus!: EventBus;
  constructor() {
    console.log("task service");
  }
  save() {
    console.log("save Task");
  }
}
@Injectable()
class UserService {
  @Inject() store!: Store;
  @Inject() eventBus!: EventBus;
  constructor() {
    console.log("user service");
  }
  save() {
    console.log("save user ");
  }
}

最终要实现的依赖注入方式和预期效果如下:

typescript 复制代码
@App()
class Application {
  @Inject() userService!: UserService;
  @Inject() taskService!: TaskService;
  @Context() context!: IoCContainer;
  @Inject() db!: DB;
  constructor() {}
}
const app1 = new Application();
const app2 = new Application();

console.log(
  app1.db === app2.db, //true,全局单例
  app1.context instanceof IoCContainer, //true 注入context
  app1 === app2, //false 应用间实例隔离
  app1.context === app2.context, //false 应用上下文隔离
  app1.userService === app2.userService, // false应用隔离
  app1.taskService === app2.taskService, // false应用隔离
  Reflect.get(app1.taskService, CONTEXT_KEY) &&
    Reflect.get(app1.taskService, CONTEXT_KEY) ===
      Reflect.get(app1.userService, CONTEXT_KEY), //true 同应用上下文
  app1.userService.eventBus === app2.userService.eventBus, //false 应用单例
  app1.userService.eventBus === app1.taskService.eventBus, //true 应用单例
  app1.userService.store === app1.taskService.store //false 多例
);

App注解实现

App注解最重要的任务是,创建IOC容器并挂到实例上。

typescript 复制代码
const CONTEXT_KEY = Symbol("CONTEXT");
export function App() {
  return function <T extends { new (...args: any[]): {} }>(constructor: T) {
    // 创建一个继承自原类的新类
    return class extends constructor {
      constructor(...args: any[]) {
        super(...args);
        const context = new IoCContainer();
        (this as any)[CONTEXT_KEY] = context;
        Reflect.defineProperty(this, CONTEXT_KEY, {
          value: context,
          writable: false,
          enumerable: false,
        });
      }
    };
  };
}

该属性默认隐藏,不过可以使用@Context注解显示声明需要使用该属性

typescript 复制代码
function Context(): PropertyDecorator {
  return (target: any, propertyKey: string | symbol) => {
    Object.defineProperty(target, propertyKey, {
      get() {
        return this[CONTEXT_KEY];
      },
      enumerable: true,
      configurable: false,
    });
  };
}

IOCContainer实现

createInstance方法会解析inject注解,创建对应实例。

该实现关键点在89行,创建实例的时候将容器自身注入到该实例内,以实现容器的逐层传递。

typescript 复制代码
export class IoCContainer {
  static classRegistry = new Map<string, Constructor>();
  static registClass(key: string, value: Constructor) {
    this.classRegistry.set(key, value);
  }
  static instances = new Map<string, any>();
  static resolve<T extends Object>(
    ctor: Constructor<T>,
    ...args: ConstructorParameters<Constructor<T>>
  ): T {
   //此处省略
  }
  static createInstance<T extends Object>(
    ctor: Constructor<T>,
    ...args: ConstructorParameters<Constructor<T>>
  ): T {
  
    const scope: Scope = Reflect.getMetadata(SCOPE_METADATA_KEY, ctor);
    if (scope !== Scope.Global) {
      console.warn("IOC 全局对象声明异常");
    }
    //参考实例上的createInstance,此处实现省略
  }
  private instances = new Map<string, any>(); // 每个容器有自己的实例存储

  resolve<T extends Object>(
    ctor: Constructor<T>,
    ...args: ConstructorParameters<Constructor<T>>
  ): T {
    const scope: Scope =
      Reflect.getMetadata(SCOPE_METADATA_KEY, ctor) || Scope.Singleton;

    switch (scope) {
      case Scope.Global: {
        if (!IoCContainer.instances.has(ctor.name)) {
          IoCContainer.instances.set(
            ctor.name,
            IoCContainer.createInstance(ctor, ...args)
          );
        }
        return IoCContainer.instances.get(ctor.name);
      }
      case Scope.Singleton: {
        if (!this.instances.has(ctor.name)) {
          this.instances.set(ctor.name, this.createInstance(ctor, ...args));
        }
        return this.instances.get(ctor.name);
      }
      case Scope.Transient:
      default: {
        return this.createInstance(ctor, ...args);
      }
    }
  }

  private createInstance<T extends Object>(
    ctor: Constructor<T>,
    ...args: ConstructorParameters<Constructor<T>>
  ): T {
   
    const instance = new ctor(...args);
    const propMetadata =
      Reflect.getMetadata(INJECT_METADATA_KEY, ctor.prototype) || [];
    for (const { propertyKey, token } of propMetadata) {
    //处理inject注解,此处需要先校验该依赖是否标记了injectable,这里省略
      const dependency = this.resolve(token);
      Reflect.defineProperty(instance, propertyKey, {
        value: dependency,
        writable: false,
      });
    }
    //这里注入ioc容器
    Reflect.defineProperty(instance, CONTEXT_KEY, {
      value: this,
      writable: false,
    });
    return instance;
  }
}

Injectable和Inject实现

Injectable仅作标记,Inject在所属实例运行时,会通过getter按需引用或创建依赖对象。由于类装饰器顺序在属性装饰器之前,此处无法在创建实例的同时,创建依赖实例,只能通过getter按需创建。

typescript 复制代码
// @Injectable 装饰器(可选,与 @Scope 结合使用)
function Injectable() {
  return function <T>(target: Constructor<T>) {
    Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target);
    IoCContainer.registClass(target.name, target);
  };
}

// Inject 装饰器:通过属性的类型自动注入实例
function Inject(): PropertyDecorator {
  return (target: any, propertyKey: string | symbol) => {
    // 获取属性的类型 (通过 reflect-metadata 提供的元数据)
    const type = Reflect.getMetadata("design:type", target, propertyKey);

    if (!type) {
      throw new Error(`IOC 无法解析 ${String(propertyKey)}`);
    }

    const getter = function (this: { [CONTEXT_KEY]: IoCContainer }) {
      if (!this[CONTEXT_KEY]) {
        console.error("未注入app context", this);
      }
      return (this[CONTEXT_KEY] || IoCContainer)?.resolve(type);
    }; // 使用类名作为标识符
    Reflect.defineProperty(target, propertyKey, {
      get: getter,
      enumerable: true,
      configurable: true,
    });
  };
}
相关推荐
布列瑟农的星空几秒前
从webpack到vite——配置与特性全面对比
前端
程序员鱼皮3 分钟前
我代表编程导航,向大家道歉!
前端·后端·程序员
车前端7 分钟前
极致灵活:如何用一个输入框,满足后台千变万化的需求
前端
用户11481867894847 分钟前
Rollup构建JavaScript核验库,并发布到NPM
前端
肥晨10 分钟前
前端私有化变量还只会加前缀嘛?保姆级教程教你4种私有化变量方法
前端·javascript
小高00710 分钟前
前端 Class 不是花架子!3 个大厂常用场景,告诉你它有多实用
前端·javascript·面试
zjjuejin14 分钟前
Maven 生命周期与插件机制
后端·maven
掘金安东尼14 分钟前
AI 应用落地谈起 ,免费试用 Amazon Bedrock 的最佳时机
java·架构
不喝奶茶哦喝奶茶长胖15 分钟前
CSS 文本换行控制:text-wrap、white-space 和 word-break 详解
前端
阿杆29 分钟前
为什么我建议你把自建 Redis 迁移到云上进行托管
redis·后端