深入理解装饰器、reflect-metadata 与 IOC 控制反转

343 阅读4分钟

深入理解装饰器、reflect-metadata 与 IOC 控制反转

在现代的 JavaScript 和 TypeScript 开发中,装饰器、reflect-metadata 以及 IOC(控制反转)是构建强大、灵活且可维护软件系统的关键技术。让我们逐步深入探讨这些概念,并着重关注装饰器的输出及其在不同场景中的作用。

一、装饰器:提升代码功能的强大工具

装饰器的基础概念

装饰器是一种使用 @expression 语法的特殊声明,可应用于类声明、方法、属性或参数上。在运行时,expression 会被计算,且其结果必须是一个接收被装饰目标作为参数的函数。装饰器的强大之处在于,它允许我们在不修改原始代码结构的前提下,为代码增添额外的功能,这在诸如元编程、日志记录、性能监控和权限检查等方面表现出色。

装饰器的分类及其输出和行为

类装饰器

类装饰器主要用于修改类的构造函数或添加静态属性。以下是一个典型的类装饰器示例:

// 类装饰器函数,接收一个构造函数作为参数
function logClass(constructor: Function) {
    console.log(`Class ${constructor.name} has been instantiated.`);
    // 这里可以对构造函数进行修改或包装,这里仅添加日志
    return class extends constructor {
        constructor(...args: any[]) {
            console.log(`Before instantiation of ${constructor.name}`);
            super(...args);
            console.log(`After instantiation of ${constructor.name}`);
        }
    };
}

// 使用类装饰器
@logClass
class MyClass {
    constructor() {
        console.log("MyClass constructor");
    }
}

// 创建类的实例,触发装饰器
const myClassInstance = new MyClass();

输出解释

  • MyClasslogClass 装饰器装饰时,首先输出 Class MyClass has been instantiated.,表明装饰器已被应用。
  • 创建 MyClass 的实例时,会先输出 Before instantiation of MyClass,接着执行原始的 MyClass 构造函数,输出 MyClass constructor,最后输出 After instantiation of MyClass
方法装饰器

方法装饰器可以修改类方法的行为,常用于添加日志记录、性能监控等功能。以下是一个示例:

// 方法装饰器函数,接收三个参数:目标对象、方法名和属性描述符
function logMethod(target: any, methodName: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log(`Calling method ${methodName} with args: ${JSON.stringify(args)}`);
        const startTime = Date.now();
        const result = originalMethod.apply(this, args);
        const endTime = Date.now();
        console.log(`Method ${methodName} returned: ${JSON.stringify(result)} in ${endTime - startTime}ms`);
        return result;
    };
    return descriptor;
}

class MyClass {
    @logMethod
    myMethod(param: string) {
        console.log(`Executing myMethod with param: ${param}`);
        return `Hello, ${param}`;
    }
}

// 使用类和方法
const myClassInstance = new MyClass();
myClassInstance.myMethod("World");

输出解释

  • 调用 myMethod 时,首先输出 Calling method myMethod with args: ["World"],显示方法调用及传入的参数。
  • 随后执行 myMethod 的原始逻辑,输出 Executing myMethod with param: World
  • 最后输出 Method myMethod returned: "Hello, World" in [X]ms,其中 [X] 是方法执行的时间,展示了性能监控的效果。
属性装饰器

属性装饰器可修改属性的描述符或添加额外逻辑,常用于属性的访问和修改监控。以下是一个示例:

// 属性装饰器函数,接收两个参数:目标对象和属性名
function logProperty(target: any, propertyName: string) {
    let value: any;
    const getter = () => {
        console.log(`Getting value of ${propertyName}: ${value}`);
        return value;
    };
    const setter = (newVal: any) => {
        console.log(`Setting value of ${propertyName}: ${newVal}`);
        value = newVal;
    };
    Object.defineProperty(target, propertyName, {
        get: getter,
        set: setter,
        enumerable: true,
        configurable: true
    });
    return target;
}

class MyClass {
    @logProperty
    myProperty: string = "Initial Value";
}

// 使用类和属性
const myClassInstance = new MyClass();
console.log(myClassInstance.myProperty);
myClassInstance.myProperty = "New Value";

输出解释

  • 当访问 myProperty 时,输出 Getting value of myProperty: Initial Value
  • 当修改 myProperty 的值时,输出 Setting value of myProperty: New Value
参数装饰器

参数装饰器可修改参数的行为或添加额外逻辑,通常用于对方法的参数进行操作或验证。以下是一个示例:

// 参数装饰器函数,接收三个参数:目标对象、方法名和参数索引
function logParameter(target: any, methodName: string, parameterIndex: number) {
    console.log(`Parameter at index ${parameterIndex} of method ${methodName} has been logged.`);
    return target;
}

class MyClass {
    myMethod(@logParameter param: string) {
        console.log(`Executing myMethod with param: ${param}`);
    }
}

// 使用类和方法
const myClassInstance = new MyClass();
myClassInstance.myMethod("World");

输出解释

  • 当调用 myMethod 时,首先输出 Parameter at index 0 of method myMethod has been logged.,表明该参数已被装饰器记录。

在使用装饰器时,确保在 tsconfig.json 中启用 experimentalDecorators 选项,以确保 TypeScript 编译器的支持:

{
    "compilerOptions": {
        "experimentalDecorators": true
    }
}

二、reflect-metadata:元数据操作的核心库

概述

reflect-metadata 为 JavaScript 和 TypeScript 提供了元数据反射 API,允许我们为代码元素(类、方法、属性和参数)添加元数据,这些元数据可在运行时进行操作,极大地增强了代码的灵活性和可扩展性。

核心功能及元数据解释

元数据的基本操作:添加和获取

使用 Reflect.defineMetadata 方法添加元数据,使用 Reflect.getMetadata 方法获取元数据。例如:

import 'reflect-metadata';

class MyClass {
    @Reflect.metadata('version', '1.0')
    method() {}
}

const version = Reflect.getMetadata('version', MyClass.prototype, 'method');
console.log(version); // 输出 '1.0'

元数据解释

  • Reflect.metadata('version', '1.0')MyClassmethod 方法添加了元数据,元数据键是 version,值为 1.0
  • Reflect.getMetadata('version', MyClass.prototype, 'method')MyClass.prototypemethod 方法中获取 version 元数据,输出为 1.0
与自定义装饰器结合使用

reflect-metadata 与自定义装饰器结合,可以创建功能更强大的装饰器。例如,使用元数据进行属性长度验证:

import 'reflect-metadata';

const MinLength = (minLength: number) => (target: any, propertyKey: string) => {
    Reflect.defineMetadata('minLength', minLength, target, propertyKey);
};

class MyClass {
    @MinLength(5)
    name: string;
}

const minLength = Reflect.getMetadata('minLength', MyClass.prototype, 'name');
console.log(minLength); // 输出 5

元数据解释

  • MinLength 装饰器使用 Reflect.defineMetadataMyClassname 属性添加了 minLength 元数据,其值为 5,可用于后续的验证逻辑。
关于 design:paramtypesinject:paramtypes
  • design:paramtypes

    • 这是 reflect-metadata 中的一个特殊键,用于存储构造函数或方法的参数类型信息。当使用 Reflect.getMetadata('design:paramtypes', target, propertyKey) 时,可以获取目标(类的构造函数或方法)的参数类型数组。在 TypeScript 中,编译器会自动为构造函数和方法生成这些元数据,存储参数的类型信息。
    • 例如:
    import 'reflect-metadata';
    
    class MyClass {
        constructor(param1: string, param2: number) {}
    }
    
    const paramTypes = Reflect.getMetadata('design:paramtypes', MyClass, 'constructor');
    console.log(paramTypes); // 输出 [String, Number]
    

    这里,design:paramtypes 存储了 MyClass 构造函数的参数类型信息,便于在运行时进行依赖注入等操作。

  • inject:paramtypes

    • 这是我们自定义的元数据键,通常用于存储依赖注入的信息。在自定义的依赖注入系统中,我们可以使用这个键来存储哪些参数需要注入依赖以及它们的标识符。
    • 例如:
    import 'reflect-metadata';
    import { container } from './container';
    
    export function inject(serviceIdentifier: symbol) {
        return (target: any, _: string | undefined, parameterIndex: number) => {
            // 获取已存储的注入参数数组,如果不存在则创建新数组
            const existingParams = Reflect.getMetadata('inject:paramtypes', target) || [];
            const params = Array.isArray(existingParams)? existingParams : [];
            
            // 在正确的位置存储服务标识符
            params[parameterIndex] = { id: serviceIdentifier };
            
            // 存储整个参数数组
            Reflect.defineMetadata('inject:paramtypes', params, target);
        };
    }
    
    class MyClass {
        constructor(@inject(Symbol.for('MyService')) private myService: any) {}
    }
    
    const injectParamTypes = Reflect.getMetadata('inject:paramtypes', MyClass, 'constructor');
    console.log(injectParamTypes); // 输出 [{ id: Symbol(MyService) }]
    

    这里,inject:paramtypes 存储了 MyClass 构造函数的参数 myService 需要注入的服务标识符,方便在实例化 MyClass 时进行依赖注入。

应用于依赖注入

在依赖注入框架中,reflect-metadata 可存储和获取依赖关系信息,以下是一个更详细的示例:

import 'reflect-metadata';

const Inject = () => (target: any, propertyKey: string, parameterIndex: number) => {
    let metadataKey = `design:paramtypes`;
    let paramTypes: any[] = Reflect.getMetadata(metadataKey, target, propertyKey) || [];
    paramTypes[parameterIndex] = 'MyDependency';
    Reflect.defineMetadata(metadataKey, paramTypes, target, propertyKey);
};

class MyClass {
    constructor(@Inject() myDependency: any) {}
}

const paramTypes = Reflect.getMetadata('design:paramtypes', MyClass, 'constructor');
console.log(paramTypes); // 输出 ['MyDependency']

解释

  • Inject 装饰器利用 reflect-metadata 存储构造函数参数的依赖信息。
  • 通过 Reflect.getMetadata 获取 MyClass 构造函数的参数类型信息,这里我们将其修改为 ['MyDependency'],用于在运行时注入相应的依赖。
运行时类型信息

reflect-metadata 可存储和检索类型信息,这对于 TypeScript 尤其重要,因为其类型信息在编译后通常会被擦除:

import 'reflect-metadata';

class MyClass {
    myMethod(param: string) {}
}

const paramTypes = Reflect.getMetadata('design:paramtypes', MyClass.prototype, 'myMethod');
console.log(paramTypes); // 输出 [String]

解释

  • 这里使用 Reflect.getMetadata 获取 myMethod 的参数类型,输出为 [String],即使在编译后的 JavaScript 代码中,也能在运行时获取类型信息。

使用场景与注意事项

  • 使用场景

    • 验证和序列化:使用元数据为属性添加验证规则,在运行时验证属性的合法性。
    • 依赖注入:像 Angular 这样的框架使用 reflect-metadata 存储和管理依赖关系,根据存储的元数据在运行时注入依赖。
    • 文档生成:添加描述、参数说明等元数据,根据这些信息生成更完善的文档。
    • AOP(面向切面编程):使用元数据标记方法或属性,结合装饰器实现切面逻辑。
  • 注意事项

    • 在 TypeScript 中使用 reflect-metadata 时,需要启用 experimentalDecoratorsemitDecoratorMetadata 选项:
    {
      "compilerOptions": {
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
      }
    }
    
    • 确保在代码中导入 reflect-metadata 库,它通过全局的 Reflect 对象提供 API。

三、IOC(控制反转):实现松耦合的设计原则

概念与重要性

IOC(Inversion of Control)是一项重要的软件设计原则,其核心是将对象的创建以及对象间的依赖关系管理从应用程序代码中分离出来,交由外部容器负责。这有助于降低对象之间的耦合度,使代码更易于维护、扩展和测试。

代码结构
  • container.ts

    import 'reflect-metadata';
    
    export class Container {
      private static instance: Container;
      private services: Map<symbol, any>;
    
      private constructor() {
        this.services = new Map();
      }
    
      static getInstance(): Container {
        if (!Container.instance) {
          Container.instance = new Container();
        }
        return Container.instance;
      }
    
      bind(identifier: symbol, instance: any) {
        this.services.set(identifier, instance);
      }
    
      get(identifier: symbol): any {
        return this.services.get(identifier);
      }
    }
    
    export const container = Container.getInstance();
    

    输出解释

    • Container 类是一个单例类,用于存储服务。bind 方法将服务与唯一标识符绑定,get 方法根据标识符获取服务。
  • decorators.ts

    import 'reflect-metadata';
    import { container } from './container';
    
    export const TYPE = {
      userService: Symbol.for('UserService'),
      logService: Symbol.for('LogService'),
    };
    
    export function inject(serviceIdentifier: symbol) {
      return (target: any, _: string | undefined, parameterIndex: number) => {
        // 获取已存储的注入参数数组,如果不存在则创建新数组
        const existingParams = Reflect.getMetadata('inject:paramtypes', target) || [];
        const params = Array.isArray(existingParams)? existingParams : [];
        
        // 在正确的位置存储服务标识符
        params[parameterIndex] = { id: serviceIdentifier };
        
        // 存储整个参数数组
        Reflect.defineMetadata('inject:paramtypes', params, target);
      };
    }
    
    export function service(identifier: symbol) {
      return (target: any) => {
        // 获取注入的参数信息数组
        const params = Reflect.getMetadata('inject:paramtypes', target) || [];
        console.log('service-->params', params);
        
        // 创建实例并注入依赖
        const dependencies = params.map((param: any) => 
          param? container.get(param.id) : undefined
        );
        
        const instance = new target(...dependencies);
        
        // 注册到容器
        container.bind(identifier, instance);
      };
    }
    

    输出解释

    • inject 装饰器存储依赖注入的元数据,service 装饰器根据存储的元数据进行依赖注入并将实例绑定到容器。
  • services.ts

    import { service, inject, TYPE } from "./decorators";
    
    // 日志服务接口
    interface ILogService {
      log(message: string): void;
    }
    
    // 日志服务实现
    @service(TYPE.logService)
    class LogService implements ILogService {
      log(message: string) {
        console.log(`[Log]: ${message}`);
      }
    }
    
    // 用户服务接口
    interface IUserService {
      createUser(name: string): void;
    }
    
    // 用户服务实现
    @service(TYPE.userService)
    class UserService implements IUserService {
      constructor(@inject(TYPE.logService) private logService: ILogService) {}
    
      createUser(name: string) {
        this.logService.log(`Creating user: ${name}`);
      }
    }
    

    输出解释

    • LogServiceUserService 分别实现了日志和用户服务,使用装饰器进行服务注册和依赖注入。
  • index.ts

    import "reflect-metadata";
    import { container } from "./container";
    import "./services"; // 确保服务被装饰器处理
    import { TYPE } from "./decorators";
    
    // 获取服务实例
    const userService = container.get(TYPE.userService);
    userService.createUser("John"); // 输出: [Log]: Creating user: John
    

    输出解释

    • 从容器中获取 UserService 并调用 createUser 方法,输出 [Log]: Creating user: John,展示了服务的调用和依赖注入的效果。

解释

  • 在这个综合示例中,reflect-metadata 库和自定义装饰器共同实现了更复杂的 IOC 和 DI 机制。
  • container.ts 中的 Container 类是核心容器,管理服务的存储和获取。
  • decorators.tsinject 装饰器利用 reflect-metadata 存储依赖注入信息,service 装饰器在类实例化时处理依赖注入并绑定实例到容器。
  • services.ts 中的服务实现类通过装饰器实现依赖注入和服务注册。
  • 最后,index.ts 展示了如何使用容器获取服务并调用其方法,实现服务间的协作。

通过深入探讨装饰器、reflect-metadata 和 IOC,我们可以看到它们在软件开发中的强大功能和重要性。它们有助于构建更灵活、可维护和可测试的软件系统,在不同的开发场景中发挥着关键作用。希望本文能帮助你更好地理解这些技术,并在你的项目中灵活运用它们。