依赖注入(DI)

248 阅读6分钟

一、什么是依赖(Dependency)

有两个元素A、B,如果元素A的变化会引起元素B的变化,则称元素B依赖(Dependency)于元素A。

在类中,依赖关系有多种表现形式,如:一个类向另一个类发消息;一个类是另一个类的成员;一个类是另一个类的某个操作参数,等等。

二、为什么要依赖注入(DI)

我们先定义四个Class,车,车身,底盘,轮胎。然后初始化这辆车,最后跑这辆车。

我们要改动轮胎类(Tire)把尺寸变成动态的,不是每次都是30。

我们要让整个程序正常运行,我们需要做以下改动:

为了修改轮胎的构造函数,这种设计却需要修改整个上层所有类的构造函数!这样的设计几乎是不可维护的。

三、基本-模式

我们需要进行 控制反转(IoC) ,即上层控制下层,而不是下层控制着上层。我们用 依赖注入(Dependency Injection) 这种方式来实现控制反转。

所谓依赖注入,就是把底层类作为参数传入上层类,实现上层类对下层类的“控制”。

这里我们用构造方法传递的依赖注入方式重新写车类的定义:

这里我只需要修改轮胎类就行了,不用修改其他任何上层类。这显然是更容易维护的代码。不仅如此,在实际的工程中,这种设计模式还有利于不同组的协同合作和单元测试。

1、基本用法

// step1    首先写一个服务提供者,作为被依赖模块
class LogService {
    constructor() { }
    public info(...args: any[]): void {
        console.info('[INFO]', new Date(), ...args);
    }
}

// step2    再编写一个消费者
class CustomerController {
    private log!: LogService;
    private token = "token_传统写法";
    constructor(logInstrance: LogService) {
        this.log = logInstrance
    }
    public main(): void {
        this.log.info('Its running...', this.token);
    }
}

// step3    传统的调用方式
const logInstance = new LogService()
const customer = new CustomerController(logInstance);
customer.main();

四、托管(类或者类的实例) + 装饰器-模式

1.安装 typescript 环境以及重要的 reflect-metadata,在入口文件引入 reflect-metadata。

2.在 tsconfig.json 中配置 compilerOptions

{
    "experimentalDecorators": true, // 开启装饰器
    "emitDecoratorMetadata": true, // 开启元编程
}

4.1、Reflect

简介

Proxy 与 Reflect 是 ES6 为了操作对象引入的 API,Reflect 的 API 和 Proxy 的 API 一一对应,

并且可以函数式的实现一些对象操作。另外,使用 reflect-metadata 可以让 Reflect 支持元编程

重要:

  • Reflect.getMetadata('design:type', target, propertyKey); // 获取被装饰属性的类型

  • Reflect.getMetadata("design:paramtypes",target,propertyKey);// 获取被装饰的参数类型

  • Reflect.getMetadata("design:returntype", target, propertyKey); // 获取被装饰函数的返回值类型

基本使用

Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey)

Reflect.getMetadata(metadataKey, target, propertyKey)

/**
 * metadataKey:meta 数据的 key
 * metadataValue:meta 数据的 值
 * target:meta 数据附加的目标
 * propertyKey(可选):对应的 property key
 */

Reflect.getMetadata('design:type', target, propertyKey); // 获取被装饰属性的类型
Reflect.getMetadata("design:paramtypes", target, propertyKey); // 获取被装饰的参数类型
Reflect.getMetadata("design:returntype", target, propertyKey); // 获取被装饰函数的返回值类型

装饰器简化操作

  • 通过 Reflect.defineMetadata 方法调用来添加 元数据
  • 通过 @Reflect.metadata 装饰器来添加 元数据
import "reflect-metadata"

@Reflect.metadata("n", 1)
class A {
    @Reflect.metadata("n", 2)
    public static method1() {
    }


    @Reflect.metadata("n", 4)
    public method2() {
    }
}

let obj = new A;

console.log(Reflect.getMetadata("n", A));  // 1
console.log(Reflect.getMetadata("n", A, "method1")) // 2
console.log(Reflect.getMetadata("n", obj)) // undefined
console.log(Reflect.getMetadata("n", obj, "method2")) // 4

自定义类元数据装饰器

import 'reflect-metadata';

function Role(name: string): ClassDecorator {
  return target => {
    Reflect.defineMetadata('role', name, target);
  };
}

@Role('admin')
class Post {}

const metadata = Reflect.getMetadata('role', Post);

console.log(metadata); // 'admin'

4.2、设计思路

  • 变更架构设计:
  • 1、需要用一个 Map 来存储 注册的依赖,并且它的key必须唯一。所以我们首先设计一个容器
  • 2、注册依赖的时候尽可能简单,甚至不需要用户自己定义key,所以这里使用 Symbol 和唯一字符串来确定一个依赖
  • 3、我们注册的依赖不一定是类,也可能是一个函数、字符串、单例。暂时不考虑使用装饰器的情况

4.3、代码

step4、先设计一个Container类 用来存储注册的模块,set和get用来注册和读取模块,has用来判断模块是否已经注册

type UnionType = string | symbol;

class Container {
    private ContainerMap = new Map<UnionType, any>();
    public set = (id: UnionType, value: any): void => {
        this.ContainerMap.set(id, value);
    }
    public get = <T>(id: UnionType): T => {
        return this.ContainerMap.get(id) as T;
    }
    public has = (id: UnionType): Boolean => {
        return this.ContainerMap.has(id);
    }
}
export const ContainerInstance = new Container();

step5、现在实现Service 装饰器来注册类依赖

  • id 是可选的一个标记模块 变量
  • singleton是一个可选的标记是否是单例的 变量,
  • target表示当前要注册的类,拿到这个类后,给它添加metadata,方便日后使用
type Constructor<T = any> = new (...args: any[]) => T;

export function Service(id: string): Function;
export function Service(singleton: boolean): Function;
export function Service(id: string, singleton: boolean): Function;
export function Service(idOrSingleton?: string | boolean, singleton?: boolean): Function {
    return (target: Constructor) => { // 这里的 target 是一个Constructor
        let _id;
        let _singleton;
        let _singleInstance;

        if (typeof idOrSingleton === 'boolean') {
            console.log(target.name, '🐈--->target.name');
            _singleton = true;
            _id = Symbol(target.name);
        } else {
            console.log(target.name, '🍉--->target.name');
            // 判断如果设置id, id是否唯一
            if (idOrSingleton && ContainerInstance.has(idOrSingleton)) {
                throw new Error(`Service: 此标识(${idOrSingleton})已被注册`)
            }
            _id = idOrSingleton || Symbol(target.name);
            _singleton = singleton;
        }

        Reflect.defineMetadata('cus:id', _id, target); // 目标类上注册 meta数据 
        if (_singleton) { // 实例 or 不实例化
            _singleInstance = new target();
        }

        ContainerInstance.set(_id, _singleInstance || target); // Map中:meta数据value值为 key;val 为 类实例、或者类
    }
}

step6 实现Inject 装饰器用来注入依赖

/**
 * 使用id定义模块后,要使用id来注入模块
 */
export function Inject(id?: string): PropertyDecorator {
// 这里的 target 是一个Object, 实例化后的 obj
    return (target: Object, propertyKey: UnionType) => {
        const Dependency = Reflect.getMetadata("design:type", target, propertyKey); // 获取被装饰属性的类型(目标类)
        const _id = id || Reflect.getMetadata("cus:id", Dependency); // 获取目标类的 meta数据value值。
        const _dependency = ContainerInstance.get(_id); // 获取实例

        // 给属性注入依赖
        Reflect.defineProperty(target, propertyKey, { // 给实例挂到属性上。
            value: _dependency,
        })
    }
}

step7、服务提供者 (给类,注册原数据)

ContainerInstance.set('size', 30); // 首次写入size.

@Service('Tire') // 不是单例
class Tire {
    // 使用Container.get 注入
    static size: number = ContainerInstance.get('size');
    static price: number = 100;
    public salePrice: number;
    constructor() {
        Tire.size = ContainerInstance.get('size');
        this.salePrice = Tire.price * Tire.size;
    }
}

@Service('Bottom', true) // 单例
class Bottom {
    constructor() { }
    @Inject()
    public tire: Tire; // 插入的是一个Tire类,而不是实例
}

@Service('Framework', true) // 单例
class Framework {
    constructor() { }
    @Inject()
    public bottom: Bottom;
}

@Service(true) // 单例
class Car {
    constructor() { }
    @Inject()
    public framework: Framework;
    public info(...args: any[]): void {
        console.info('[INFO]', new Date(), ...args);
    }

}

// 可以在入口文件调用处理
ContainerInstance.set('token', 'token_依赖注入');
// 假设一个消费者
class CarCustomer {
    constructor() { }

    // 使用Inject注入
    @Inject()
    private car!: Car

    // 使用Container.get 注入
    private token = ContainerInstance.get('token');

    public main(): void {
        this.car.info('Its running...', this.token);
    }

    public getCarSizeClass(size?: number): void {
        const tireConstructor: any = this.car?.framework?.bottom?.tire; // 通过编辑器提示依次拿到下层类

        // 使用实例化:多个实例
        // if (size) ContainerInstance.set('size', size); // 重新写入size
        // return new tireConstructor();

        // 使用静态属性:一个类
        if (size) tireConstructor.size = size;
        return tireConstructor
    }
}

const customer2 = new CarCustomer();
customer2.main();

const tireSize30 = customer2.getCarSizeClass();
console.log(tireSize30);

const tireSize50 = customer2.getCarSizeClass(50);
console.log(tireSize50);

4.4、代码执行

使用实例化:多个实例

使用静态属性一个类

五、思考

为什么要依赖注入 + 托管? 上层类可以直接拿到下层类的实例,或拿到下层类,直接做操作。

从而实现:上层对下层类的控制,控制反转。

六、参考链接

用TypeScript装饰器实现一个简单的依赖注入

TypeScript08:装饰器、元数据