一、当JavaScript设计模式遇到TypeScript强类型

十年前我们用jQuery写业务逻辑时,模块化主要靠立即执行函数实现。而今大型前端项目中,TypeScript的类型系统和ES6+特性为我们打开新世界的大门。设计模式中的工厂模式不再需要复杂的new操作符判断,单例模式可以直接用模块系统实现,观察者模式中的事件类型也能获得类型提示。

// 单例模式示例(TypeScript技术栈)
class AppConfig {
  private static instance: AppConfig;
  public readonly apiEndpoint: string;

  private constructor() {
    this.apiEndpoint = process.env.API_URL || "http://default.api";
  }

  static getInstance(): AppConfig {
    if (!AppConfig.instance) {
      AppConfig.instance = new AppConfig();
    }
    return AppConfig.instance;
  }
}

const config = AppConfig.getInstance();
console.log(config.apiEndpoint); // 类型安全的访问

这个经过改造的单例模式,通过私有构造器和静态方法确保实例唯一性,readonly修饰符防止配置被意外修改。相比传统JavaScript实现,TypeScript在编译阶段就能拦截大部分潜在错误。

二、泛型:给设计模式装上类型安全盔甲

2.1 容器类通用化改造

想象我们要实现一个能存储任意类型的数据容器,传统的JavaScript方案通常使用any类型:

class AnyContainer {
  constructor(private data: any) {}
  
  getValue(): any {
    return this.data;
  }
}

const numContainer = new AnyContainer(42);
const strContainer = new AnyContainer("hello");

// 此处容易在运行时出错
const numResult: number = strContainer.getValue(); 

使用泛型重构后的版本:

class TypedContainer<T> {
  constructor(private data: T) {}
  
  getValue(): T {
    return this.data;
  }
}

const numberBox = new TypedContainer<number>(42);
const stringBox = new TypedContainer<string>("hello");

// 编译时报错:类型"string"的参数不能赋给类型"number"的参数
const result: number = stringBox.getValue(); 

泛型参数T如同一个类型占位符,编译器能自动推断类型关系。在工厂模式中,我们可以创建类型安全的对象工厂:

interface Animal {
  name: string;
  makeSound(): void;
}

class Dog implements Animal {
  constructor(public name: string) {}
  makeSound() { console.log("Woof!"); }
}

class AnimalFactory {
  static create<T extends Animal>(type: new (name: string) => T, name: string): T {
    return new type(name);
  }
}

const buddy = AnimalFactory.create(Dog, "Buddy");
buddy.makeSound(); // 输出"Woof!"且类型推断为Dog实例

2.2 泛型约束实战

在策略模式中,我们可以通过泛型约束确保策略的合规性:

interface SortingStrategy<T> {
  sort(items: T[]): T[];
}

class NumberSorter implements SortingStrategy<number> {
  sort(items: number[]): number[] {
    return items.slice().sort((a, b) => a - b);
  }
}

class StringSorter implements SortingStrategy<string> {
  sort(items: string[]): string[] {
    return items.slice().sort((a, b) => a.localeCompare(b));
  }
}

function executeSort<T>(strategy: SortingStrategy<T>, data: T[]): T[] {
  return strategy.sort(data);
}

const numbers = [3, 1, 4];
const sortedNumbers = executeSort(new NumberSorter(), numbers);
console.log(sortedNumbers); // [1, 3, 4] 且类型为number[]

三、装饰器:给传统模式注入超能力

3.1 方法装饰器实现日志追踪

假设我们需要给关键业务方法添加执行日志:

function logExecution(target: any, key: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  
  descriptor.value = function (...args: any[]) {
    console.log(`调用方法 ${key},参数:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`方法 ${key} 返回结果:`, result);
    return result;
  };

  return descriptor;
}

class PaymentService {
  @logExecution
  processPayment(amount: number, currency: string): boolean {
    // 模拟支付处理
    return Math.random() > 0.5;
  }
}

const payment = new PaymentService();
payment.processPayment(100, "USD");
// 控制台输出:
// 调用方法 processPayment,参数: [100, "USD"]
// 方法 processPayment 返回结果: true

3.2 属性装饰器实现响应式更新

实现一个简单的Vue式数据绑定:

const observersMap = new WeakMap<object, Function[]>();

function Observable(target: any, key: string) {
  let value = target[key];

  Object.defineProperty(target, key, {
    get: () => value,
    set: (newVal) => {
      if (newVal !== value) {
        value = newVal;
        const observers = observersMap.get(target) || [];
        observers.forEach(observer => observer());
      }
    }
  });
}

class Store {
  @Observable
  counter = 0;

  constructor() {
    observersMap.set(this, []);
  }

  subscribe(callback: Function) {
    const observers = observersMap.get(this)!;
    observers.push(callback);
  }
}

const store = new Store();
store.subscribe(() => console.log(`计数器更新: ${store.counter}`));
store.counter = 1; // 触发控制台输出

3.3 装饰器组合实现缓存策略

将多个装饰器组合使用:

function CacheResult(timeout: number) {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    let lastResult: any;
    let lastFetchTime = 0;

    descriptor.value = function (...args: any[]) {
      const now = Date.now();
      if (now - lastFetchTime < timeout) {
        console.log(`返回缓存结果:${lastResult}`);
        return lastResult;
      }
      
      lastResult = originalMethod.apply(this, args);
      lastFetchTime = Date.now();
      return lastResult;
    };

    return descriptor;
  }
}

class DataService {
  @CacheResult(2000)
  fetchData() {
    console.log("实际请求数据...");
    return Math.random();
  }
}

const service = new DataService();
service.fetchData(); // 输出"实际请求数据..."
service.fetchData(); // 输出"返回缓存结果..."
setTimeout(() => service.fetchData(), 3000); // 再次请求新数据

四、手写极简依赖注入框架

4.1 核心容器实现

让我们用装饰器构建一个DI容器:

type Constructor<T> = new (...args: any[]) => T;

const serviceMap = new Map<symbol, Constructor<any>>();
const instanceMap = new Map<symbol, any>();

function Injectable(token: symbol) {
  return function <T extends Constructor<any>>(target: T) {
    serviceMap.set(token, target);
  };
}

function Inject(token: symbol) {
  return function (target: any, key: string) {
    Object.defineProperty(target, key, {
      get: () => {
        if (!instanceMap.has(token)) {
          const Constructor = serviceMap.get(token)!;
          instanceMap.set(token, new Constructor());
        }
        return instanceMap.get(token);
      },
      enumerable: true,
      configurable: true
    });
  };
}

// 使用示例
const LoggerToken = Symbol("ILogger");
const DatabaseToken = Symbol("IDatabase");

@Injectable(LoggerToken)
class LoggerService {
  log(message: string) {
    console.log(`[LOG] ${new Date().toISOString()} ${message}`);
  }
}

@Injectable(DatabaseToken)
class DatabaseService {
  query(sql: string) {
    return `执行查询:${sql}`;
  }
}

class App {
  @Inject(LoggerToken)
  logger!: LoggerService;

  @Inject(DatabaseToken)
  db!: DatabaseService;

  run() {
    this.logger.log("应用启动");
    console.log(this.db.query("SELECT * FROM users"));
  }
}

const app = new App();
app.run();
// 输出:
// [LOG] 2023-09-01T00:00:00.000Z 应用启动
// 执行查询:SELECT * FROM users

4.2 依赖注入的嵌套场景

处理多层依赖注入:

const ApiToken = Symbol("IApi");

@Injectable(ApiToken)
class ApiClient {
  constructor(
    @Inject(LoggerToken) private logger: LoggerService
  ) {}

  fetchData() {
    this.logger.log("发起API请求");
    return { data: "API响应数据" };
  }
}

class BusinessLogic {
  @Inject(ApiToken)
  api!: ApiClient;

  @Inject(DatabaseToken)
  db!: DatabaseService;

  process() {
    const data = this.api.fetchData();
    this.db.query(`INSERT INTO logs VALUES ('${data.data}')`);
  }
}

const logic = new BusinessLogic();
logic.process();
// 输出:
// [LOG] 发起API请求
// 执行查询:INSERT INTO logs VALUES ('API响应数据')

五、模式应用全景分析

5.1 典型应用场景

  • 泛型:构建通用组件库、数据处理管道、类型安全的集合类
  • 装饰器:AOP编程、功能增强(日志/缓存/权限)、元数据管理
  • DI框架:大型应用架构、模块解耦、测试替身替换

5.2 技术优劣分析

优势:

  • 泛型:提升代码复用率,编译时类型检查,智能提示
  • 装饰器:非侵入式增强,声明式编程,关注点分离
  • DI:降低耦合度,提高可测试性,依赖关系显式声明

注意事项:

  1. 装饰器仍是实验性特性,需配置tsconfig.json启用
  2. 过度使用装饰器会导致代码可读性下降
  3. DI框架实现应考虑循环依赖检测
  4. 泛型复杂度过高会增加理解成本

5.3 最佳实践总结

  • 在DTO/API层优先使用泛型保持灵活性
  • 装饰器适合处理横切关注点,但不建议修改方法核心逻辑
  • 按模块划分DI容器,避免全局容器过度膨胀
  • 结合interfacesymbol提升DI可维护性