DI 原理分析,并实现一个简易版 DI 容器

发布于 2025-02-24 01:02:49 字数 6743 浏览 6 评论 0

本文基于自身理解进行输出,目的在于交流学习,如有不对,还望各位看官指出。

DI

DI—Dependency Injection,即“依赖注入”:对象之间依赖关系由容器在运行期决定,形象的说,即由 容器动态的将某个对象注入到对象属性之中 。依赖注入的目的并非为软件系统带来更多功能,而是为了提升对象重用的频率,并为系统搭建一个灵活、可扩展的框架。

使用方式

首先看一下常用依赖注入 (DI) 的方式:

function Inject(target: any, key: string){
    target[key] = new (Reflect.getMetadata('design:type',target,key))()
}

class A {
    sayHello(){
        console.log('hello')
    }
}

class B {
    @Inject   // 编译后等同于执行了 @Reflect.metadata("design:type", A)
    a: A

    say(){
       this.a.sayHello()  // 不需要再对 class A 进行实例化
    }
}

new B().say() // hello

原理分析

TS 在编译装饰器的时候,会通过执行 __metadata 函数 多返回一个属性装饰器 @Reflect.metadata ,它的目的是将需要实例化的 service 以元数据 'design:type' 存入 reflect.metadata ,以便我们在需要依赖注入时,通过 Reflect.getMetadata 获取到对应的 service , 并进行实例化赋值给需要的属性。

@Inject 编译后代码:

var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};

// 由于__decorate 是从右到左执行,因此, defineMetaData 会优先执行。
__decorate([
    Inject,
    __metadata("design:type", A)  //  作用等同于 Reflect.metadata("design:type", A)
], B.prototype, "a", void 0);

即默认执行了以下代码:

Reflect.defineMetadata("design:type", A, B.prototype, 'a');

Inject 函数需要做的就是从 metadata 中获取对应的构造函数并构造实例对象赋值给当前装饰的属性

function Inject(target: any, key: string){
    target[key] = new (Reflect.getMetadata('design:type',target,key))()
}

不过该依赖注入方式存在一个问题:

由于 Inject 函数 在代码编译阶段便会执行,将导致 B.prototype 在代码编译阶段被修改,这违反了 六大设计原则之开闭原则(避免直接修改类,而应该在类上进行扩展) 那么该如何解决这个问题呢,我们可以借鉴一下 TypeDI 的思想。

typedi - typedi 是一款支持 TypeScript 和 JavaScript 依赖注入工具

typedi 的依赖注入思想是类似的,不过多维护了一个 container

1. metadata

在了解其 container 前,我们需要先了解 typedi 中定义的 metadata ,这里重点讲述一下我所了解的比较重要的几个属性。

  • id: service 的唯一标识
  • type: 保存 service 构造函数
  • value: 缓存 service 对应的实例化对象
const newMetadata: ServiceMetadata<T> = {
      id: ((serviceOptions as any).id || (serviceOptions as any).type) as ServiceIdentifier,    // service 的唯一标识
      type: (serviceOptions as ServiceMetadata<T>).type || null,  // service 构造函数
      value: (serviceOptions as ServiceMetadata<T>).value || EMPTY_VALUE,  // 缓存 service 对应的实例化对象
};

2. container 作用

function ContainerInstance() {
        this.metadataMap = new Map();  //保存 metadata 映射关系,作用类似于 Refect.metadata
        this.handlers = []; // 事件待处理队列
        get(){};  // 获取依赖注入后的实例化对象
         ...
}
  • this. metadataMap - @service 会将 service 构造函数 以 metadata 形式保存到 this.metadataMap 中。
    • 缓存实例化对象,保证单例;
  • this.handlers - @inject 会将依赖注入操作的 对象目标行为 以 object 形式 push 进 handlers 待处理数组。
    • 保存 构造函数静态类型属性 间的映射关系。
      {
          object: target,  // 当前等待挂载的类的原型对象
          propertyName: propertyName,  // 目标属性值
          index: index, 
          value: function (containerInstance) {   // 行为
              var identifier = Reflect.getMetadata('design:type', target, propertyName)
              return containerInstance.get(identifier);
          }
      }
      
      @inject 将该对象 push 进一个等待执行的 handlers 待处理数组里,当需要用到对应 service 时执行 value 函数 并修改 propertyName。
      if (handler.propertyName) {
       instance[handler.propertyName] = handler.value(this);
      }
      
  • get - 对象实例化操作及依赖注入操作
    • 避免直接修改类,而是对其实例化对象的属性进行拓展;

相关结论

  • typedi 中的实例化操作不会立即执行,而是在一个 handlers 待处理数组,等待 Container.get(B) ,先对 B 进行实例化,然后从 handlers 待处理数组取出对应的 value 函数 并执行修改实例化对象的属性值,这样不会影响 Class B 自身
  • 实例的属性值被修改后,将被缓存到 metadata.value (typedi 的单例服务特性)。

相关问题案例可查看: https://stackoverflow.com/questions/55684776/typedi-inject-doesnt-work-but-container-get-does

new B().say()  // 将会输出 sayHello is undefined

Container.get(B).say()  // hello word

实现一个简易版 DI Container

此处代码依赖 TS ,不支持 JS 环境

interface Handles {
    target: any
    key: string,
    value: any
}

interface Con {
    handles: Handles []   // handlers 待处理数组
    services: any[]  // service 数组,保存已实例化的对象
    get<T>(service: new () => T) : T   // 依赖注入并返回实例化对象
    findService<T>(service: new () => T) : T  // 检查缓存
    has<T>(service: new () => T) : boolean  // 判断服务是否已经注册
}

var container: Con = {
    handles: [],  // handlers 待处理数组
    services: [], // service 数组,保存已实例化的对象
    get(service){
        let res: any = this.findService(service)
        if(res){
            return  res
        }

        res = new service()
        this.services.push(res)
        this.handles.forEach(handle=>{
            if(handle.target !== service.prototype){
                return
            }
            res[handle.key] = handle.value
        })
        return res
    },

    findService(service){
        return this.services.find(instance => instance instanceof service)
    },

   // service 是否已被注册
    has(service){
        return !!this.findService(service)
    }
}

function Inject(target: any, key: string){
    const service = Reflect.getMetadata('design:type',target,key)

    // 将实例化赋值操作缓存到 handles 数组
    container.handles.push({
        target,
        key,
        value: new service()
    })

    // target[key] = new (Reflect.getMetadata('design:type',target,key))()
}

class A {
    sayA(name: string){
        console.log('i am '+ name)
    }
}

class B {
    @Inject
    a: A

    sayB(name: string){
       this.a.sayA(name)
    }
}

class C{
    @Inject
    c: A

    sayC(name: string){
       this.c.sayA(name)
    }
}

// new B().sayB(). // Cannot read property 'sayA' of undefined
container.get(B).sayB('B')
container.get(C).sayC('C')

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

风柔一江水

暂无简介

文章
评论
837 人气
更多

推荐作者

闻呓

文章 0 评论 0

深府石板幽径

文章 0 评论 0

mabiao

文章 0 评论 0

枕花眠

文章 0 评论 0

qq_CrTt6n

文章 0 评论 0

红颜悴

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文