zoukankan      html  css  js  c++  java
  • DI 原理解析 并实现一个简易版 DI 容器

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

    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')
    

    · 往期精彩 ·

    【不懂物理的前端不是好的游戏开发者(一)—— 物理引擎基础】

    【3D性能优化 | 说一说glTF文件压缩】

    【京东购物小程序 | Taro3 项目分包实践】

    欢迎关注凹凸实验室博客:aotu.io

    或者关注凹凸实验室公众号(AOTULabs),不定时推送文章:

    欢迎关注凹凸实验室公众号

  • 相关阅读:
    C. New Year Book Reading
    B. New Year Permutation
    A. New Year Transportation
    D. The Child and Zoo
    python 3.6 'utf-8' codec can't decode byte 0x8b in position 1: invalid start byte错误
    VB ListView控件各种操作详解
    开发者工具下载
    phpstorm10激活加汉化
    Windows Server 2016 配置指南 之 FTP环境搭建篇
    VB.NET Jobject 解析 JSON
  • 原文地址:https://www.cnblogs.com/o2team/p/15190818.html
Copyright © 2011-2022 走看看