大话设计模式——多应用实例下的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,
    });
  };
}
相关推荐
小只笨笨狗~9 分钟前
el-dialog宽度根据内容撑开
前端·vue.js·elementui
weixin_4903543413 分钟前
Vue设计与实现
前端·javascript·vue.js
David爱编程16 分钟前
多核 CPU 下的缓存一致性问题:隐藏的性能陷阱与解决方案
java·后端
追逐时光者37 分钟前
一款基于 .NET 开源、功能全面的微信小程序商城系统
后端·.net
烛阴1 小时前
带你用TS彻底搞懂ECS架构模式
前端·javascript·typescript
绝无仅有2 小时前
Go 并发同步原语:sync.Mutex、sync.RWMutex 和 sync.Once
后端·面试·github
绝无仅有2 小时前
Go Vendor 和 Go Modules:管理和扩展依赖的最佳实践
后端·面试·github
卓码软件测评2 小时前
【第三方网站运行环境测试:服务器配置(如Nginx/Apache)的WEB安全测试重点】
运维·服务器·前端·网络协议·nginx·web安全·apache
龙在天2 小时前
前端不求人系列 之 一条命令自动部署项目
前端
自由的疯2 小时前
Java 实现TXT文件导入功能
java·后端·架构