以下是关于 装饰器模式(Decorator Pattern) 的系统梳理,涵盖核心概念、实现方式、应用场景及注意事项,帮助我们掌握这种灵活扩展对象能力的经典设计模式:


一、装饰器模式基础

1. 核心概念
  • 定义:动态地为对象添加额外职责,相比继承更灵活,遵循 开放-封闭原则(对扩展开放,对修改封闭)。
  • 核心思想:通过包装对象(装饰器)增强功能,而非修改原始对象。
  • 参与者:
    • Component(组件接口):定义被装饰对象的接口。
    • ConcreteComponent(具体组件):实现基础功能的对象。
    • Decorator(装饰器基类):继承/实现组件接口,持有组件引用。
    • ConcreteDecorator(具体装饰器):添加具体增强功能。
2. 基础实现(ES5 示例)
// 组件接口
class Coffee {
  cost() { return 5; }
}

// 装饰器基类
class CoffeeDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }
  cost() {
    return this.coffee.cost();
  }
}

// 具体装饰器:加牛奶
class MilkDecorator extends CoffeeDecorator {
  cost() {
    return super.cost() + 2;
  }
}

// 具体装饰器:加糖
class SugarDecorator extends CoffeeDecorator {
  cost() {
    return super.cost() + 1;
  }
}

// 使用
let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
console.log(coffee.cost()); // 8

二、JavaScript 装饰器语法(ES7+)

1. 类装饰器
function logClass(target) {
  console.log(`装饰类: ${target.name}`);
  return class extends target {
    constructor(...args) {
      super(...args);
      console.log('实例化增强');
    }
  };
}

@logClass
class MyClass {
  // ...
}

new MyClass(); // 输出:装饰类: MyClass → 实例化增强
2. 方法装饰器
function readonly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

class User {
  @readonly
  name = 'Alice';
}

const user = new User();
user.name = 'Bob'; // TypeError: Cannot assign to read only property
3. 属性装饰器
function formatDate(format) {
  return function (target, key) {
    let value = target[key];
    Object.defineProperty(target, key, {
      get: () => value.toLocaleDateString(format),
      set: (v) => { value = new Date(v); },
    });
  };
}

class Event {
  @formatDate('zh-CN')
  date = new Date();
}

const event = new Event();
console.log(event.date); // "2023/10/20"

三、进阶知识

1. 装饰器组合
  • 链式装饰:多个装饰器按声明顺序从外到内执行。
    @decoratorA
    @decoratorB
    class MyClass { /* ... */ }
    // 等效于 decoratorA(decoratorB(MyClass))
    
2. 高阶装饰器(工厂模式)
  • 参数化装饰器:通过函数返回装饰器实现动态配置。
    function log(message) {
      return function (target, name, descriptor) {
        const original = descriptor.value;
        descriptor.value = function (...args) {
          console.log(`${message}: ${name}`);
          return original.apply(this, args);
        };
        return descriptor;
      };
    }
    
    class Calculator {
      @log('执行计算')
      add(a, b) { return a + b; }
    }
    
3. 装饰器与 Mixin
  • 混合模式:通过装饰器实现多重继承。
    function mixin(...mixins) {
      return target => Object.assign(target.prototype, ...mixins);
    }
    
    const CanSwim = { swim() { console.log('游泳'); } };
    const CanFly = { fly() { console.log('飞行'); } };
    
    @mixin(CanSwim, CanFly)
    class Duck {}
    
    const duck = new Duck();
    duck.swim(); // 游泳
    

四、实际应用场景

1. 日志与性能监控
function benchmark(target, name, descriptor) {
  const original = descriptor.value;
  descriptor.value = function (...args) {
    const start = performance.now();
    const result = original.apply(this, args);
    console.log(`${name}耗时: ${performance.now() - start}ms`);
    return result;
  };
  return descriptor;
}

class DataProcessor {
  @benchmark
  process(data) { /* 耗时操作 */ }
}
2. 权限校验
function requireAuth(target, name, descriptor) {
  const original = descriptor.value;
  descriptor.value = function (...args) {
    if (!this.isAuthenticated) {
      throw new Error('需要登录');
    }
    return original.apply(this, args);
  };
  return descriptor;
}

class UserService {
  isAuthenticated = true;

  @requireAuth
  deleteAccount() { /* ... */ }
}
3. 数据校验
function validate(schema) {
  return function (target, name, descriptor) {
    const original = descriptor.value;
    descriptor.value = function (data) {
      if (!schema.validate(data)) {
        throw new Error('数据校验失败');
      }
      return original.call(this, data);
    };
    return descriptor;
  };
}

class ApiService {
  @validate({ name: 'string' })
  createUser(data) { /* ... */ }
}

五、注意事项

1. 执行顺序陷阱
  • 装饰器顺序影响结果:装饰器从外到内执行,但可能因嵌套导致逻辑混乱。
  • 最佳实践:保持装饰器职责单一,避免相互依赖。
2. 原型链问题
  • 方法装饰器修改原型:影响所有实例,需谨慎处理。
    function decorator(target, name, descriptor) {
      // 直接修改原型方法
    }
    
3. 浏览器兼容性
  • ES7 装饰器语法:需通过 Babel(@babel/plugin-proposal-decorators)转译。
  • TypeScript 支持:需启用 experimentalDecorators 编译器选项。
4. 过度使用风险
  • 代码可读性下降:多层装饰器嵌套增加理解难度。
  • 调试困难:装饰器可能隐藏原始逻辑,需结合 Source Map 调试。

六、装饰器模式 vs 代理模式

维度装饰器模式代理模式
核心目标增强对象功能控制对象访问(延迟初始化、权限)
关系装饰器与被装饰对象实现相同接口代理与目标对象实现相同接口
关注点动态添加职责间接访问,功能隔离

七、总结与最佳实践

  • 适用场景:动态扩展对象功能、AOP(日志/校验/监控)、兼容性封装。
  • 优势:灵活扩展、符合单一职责原则、避免类爆炸。
  • 替代方案:组合模式、策略模式(根据场景选择)。
  • 现代应用:React 高阶组件(HOC)、Angular 装饰器语法、NestJS 依赖注入。

通过合理应用装饰器模式,可以优雅地扩展代码能力,但需注意保持简洁性和可维护性,避免陷入过度设计的陷阱。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐