Published on

装饰器模式详解

Authors
  • avatar
    Name
    青雲
    Twitter

在软件设计中,装饰器模式(Decorator Pattern)是一种结构型设计模式。它允许向一个现有对象添加新的功能,同时又不改变其结构。装饰器模式通过创建一个装饰类来包装原始类,从而使得原始类和装饰类可以独立变化。

为什么需要装饰器模式?

想象一下,你走进了一家咖啡店,决定买一杯简单的黑咖啡。这杯黑咖啡就像是我们软件开发中的一个基本组件,它具有自己的功能(提供咖啡)和价格。但许多人喜欢根据自己的口味调整咖啡,比如加糖、加奶或是加香草糖浆等。每加一种配料,咖啡的味道(功能)和价格都会相应变化。

在软件开发中,如果我们要为基本组件增加附加的功能而不改变其结构,那么装饰器模式就派上用场了。使用装饰器模式,你可以在运行时动态地为对象添加额外的功能,而不必改变对象的类。 将这个比喻转换为代码概念:

  • 基础组件(Component):基础咖啡(例如,黑咖啡)。
  • 装饰器(Decorator):咖啡的各种调料(例如,糖、奶、香草糖浆)。
  • 装饰过程:顾客根据个人口味选择添加的调料,每添加一个调料,都相当于“装饰”了原来的咖啡。

在前端开发中,装饰器模式解决了在不修改原始类的基础上向其添加行为的需求。例如,当我们需要扩展某个类的功能,但又不希望创建大量子类时,装饰器模式是一个理想的选择。它允许我们在运行时动态组合对象和行为,使得系统更具灵活性和可扩展性。

基本概念

装饰器模式包括以下几个部分:

  1. 组件(Component):定义了要操作的对象接口。
  2. 具体组件(Concrete Component):实现了组件接口的具体类,是被装饰的对象。
  3. 装饰基类(Decorator):实现了组件接口,并持有一个组件对象的引用。
  4. 具体装饰类(Concrete Decorator):继承装饰基类,实现具体的装饰功能。

实现示例

假设我们在开发一个文本编辑器,基本文本(PlainText)需要支持各种装饰,如加粗、斜体和下划线。我们可以使用装饰器模式来实现这些功能,使得可以自由组合各种装饰并独立扩展。

定义组件接口

interface TextComponent {
  getText(): string;
}

实现具体组件(PlainText)

class PlainText implements TextComponent {
  private text: string;

  constructor(text: string) {
    this.text = text;
  }

  getText(): string {
    return this.text;
  }
}

实现装饰基类

abstract class TextDecorator implements TextComponent {
  protected component: TextComponent;

  constructor(component: TextComponent) {
    this.component = component;
  }

  getText(): string {
    return this.component.getText();
  }
}

实现具体装饰类(加粗、斜体、下划线)

class BoldDecorator extends TextDecorator {
  getText(): string {
    return `<b>${super.getText()}</b>`;
  }
}

class ItalicDecorator extends TextDecorator {
  getText(): string {
    return `<i>${super.getText()}</i>`;
  }
}

class UnderlineDecorator extends TextDecorator {
  getText(): string {
    return `<u>${super.getText()}</u>`;
  }
}

使用装饰器模式

const plainText = new PlainText("Hello, World!");

const boldText = new BoldDecorator(plainText);
const italicBoldText = new ItalicDecorator(boldText);
const underlinedItalicBoldText = new UnderlineDecorator(italicBoldText);

console.log(plainText.getText()); // 输出: Hello, World!
console.log(boldText.getText()); // 输出: <b>Hello, World!</b>
console.log(italicBoldText.getText()); // 输出: <i><b>Hello, World!</b></i>
console.log(underlinedItalicBoldText.getText()); // 输出: <u><i><b>Hello, World!</b></i></u>
  1. 创建 PlainText 实例:传入的文本是 "Hello, World!"。
  2. 创建 BoldDecorator 实例:传入 PlainText 实例,并通过 super.getText() 方法调用 PlainTextgetText 方法获取文本,然后在文本外加上 <b> 标记。
  3. 创建 ItalicDecorator 实例:传入加粗后的文本实例 BoldDecorator,再次通过 super.getText() 调用其 getText 方法获取已加粗的文本,然后在文本外加上 <i> 标记。
  4. 创建 UnderlineDecorator 实例:传入斜体加粗后的文本实例 ItalicDecorator,同样通过 super.getText() 调用其 getText 方法获取已斜体加粗的文本,然后在文本外加上 <u> 标记。

应用场景

增强组件功能(React/Vue组件)

在React或Vue等前端框架中,装饰器模式常被用于增强组件的功能。例如,你可能想为一个组件添加日志记录、性能监控或错误处理等功能,而不修改其本身的代码。通过装饰器函数或高阶组件(HOC),可以轻松实现这一点。

性能监控高阶组件

import React, { ComponentType, useEffect, useState } from 'react';

// 高阶组件:性能监控
function withPerformanceMonitoring<T>(WrappedComponent: ComponentType<T>) {
  return (props: T) => {
    const [startTime] = useState(Date.now());

    useEffect(() => {
      const renderTime = Date.now() - startTime;
      console.log(`Component ${WrappedComponent.name} rendered in ${renderTime}ms`);

      return () => {
        const unmountTime = Date.now() - startTime;
        console.log(`Component ${WrappedComponent.name} existed for ${unmountTime}ms`);
      };
    }, [props, startTime]);

    return <WrappedComponent {...props} />;
  };
}

// 示例组件
const SimpleComponent: React.FC<{ message: string }> = ({ message }) => {
  return <div>{message}</div>;
};

// 使用高阶组件增强功能
const MonitoredComponent = withPerformanceMonitoring(SimpleComponent);

// 使用增强组件
const App: React.FC = () => {
  return <MonitoredComponent message="Hello, World!" />;
};

export default App;

与桥接模式的区别

《桥接模式详解》一文中提到,利用高阶组件实现桥接模式,可以实现业务逻辑与 UI 逻辑的分离。其中,抽象的 HOC 负责处理业务逻辑,而具体的展示组件则负责呈现数据。 高阶组件(Higher-Order Components,HOC)是一种增强 React 组件功能的模式,它通过将组件作为参数并返回一个新的组件,从而允许我们复用组件逻辑。

高阶组件可以实现多种设计模式,其中包括装饰器模式和桥接模式。尽管这两种模式都使用了高阶组件(HOC),但它们的设计目的和应用场景却有所不同。

  • 装饰器模式:主要用于动态地给对象添加责任,以增强对象的功能,如日志记录、性能监控等。装饰器模式侧重于增强组件的行为,而不改变其核心逻辑。
  • 桥接模式:主要用于分离抽象部分与实现部分,使它们可以独立变化。在 React 中,通过将业务逻辑和展示逻辑分离,可以实现两者的独立更新,而不会相互影响。

日志记录器

在日志记录器中,装饰器模式可以用于添加各种日志记录功能,如格式化、时间戳和写入文件。通过装饰器模式,可以将这些功能独立实现,并在需要时动态组合,使得代码更具灵活性和可扩展性

// 定义组件接口
interface Logger {
  log(message: string): void;
}

// 实现具体组件
class SimpleLogger implements Logger {
  log(message: string): void {
    console.log(message);
  }
}

// 实现装饰基类
abstract class LoggerDecorator implements Logger {
  protected logger: Logger;

  constructor(logger: Logger) {
    this.logger = logger;
  }

  log(message: string): void {
    this.logger.log(message);
  }
}

// 实现具体装饰类
// 添加时间戳
class TimestampLogger extends LoggerDecorator {
  log(message: string): void {
    const timestamp = new Date().toISOString();
    super.log(`[${timestamp}] ${message}`);
  }
}
// 添加格式化
class FormatLogger extends LoggerDecorator {
  log(message: string): void {
    const formattedMessage = `*** ${message} ***`;
    super.log(formattedMessage);
  }
}
// 写入文件
const fs = require("fs");

class FileLogger extends LoggerDecorator {
  private filePath: string;

  constructor(logger: Logger, filePath: string) {
    super(logger);
    this.filePath = filePath;
  }

  log(message: string): void {
    fs.appendFileSync(this.filePath, message + "\n");
    super.log(message);
  }
}

// 使用装饰器模式
const simpleLogger = new SimpleLogger();
const timestampLogger = new TimestampLogger(simpleLogger);
const formatTimestampLogger = new FormatLogger(timestampLogger);
const fileFormatTimestampLogger = new FileLogger(
  formatTimestampLogger,
  "log.txt",
);

fileFormatTimestampLogger.log("This is a log message.");
  • 组件接口(Logger):定义了日志记录的基本操作方法。
  • 具体组件(SimpleLogger):实现了日志记录接口,提供最基本的日志记录功能。
  • 装饰器基类(LoggerDecorator):实现了日志记录接口,并保存了一个日志记录器对象的引用,定义了装饰的基础操作。
  • 具体装饰类(TimestampLogger、FormatLogger、FileLogger):继承装饰器基类,分别实现了添加时间戳、格式化和写入文件的功能。

功能标记或注解

在函数或方法上添加标记,以指示某些特殊行为或处理逻辑是非常有用的。这些标记可以随后被解释器或其他部分的代码识别和处理。 接下来,我们将实现@deprecated,即在方法上添加自定义标记,以警告开发者该方法已被弃用。

什么是装饰器

装饰器(Decorator)是一种特殊类型的声明,它能被附加到类声明、方法、访问符、属性或参数上,可以修改类的行为。装饰器在编译时被处理,而不是在运行时。TypeScript 在 ES2016 Decorator 提案的基础上实现了装饰器。

配置 TypeScript 装饰器

在 TypeScript 中使用装饰器,首先需要在 tsconfig.json 中启用 experimentalDecorators 选项:

{
  "compilerOptions": {
    "target": "es6",
    "experimentalDecorators": true
  }
}

实现自定义装饰器

function deprecated(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor,
) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.warn(`Warning: Method ${propertyKey} is deprecated.`);
    return originalMethod.apply(this, args);
  };

  return descriptor;
}

class ExampleClass {
  @deprecated
  oldMethod() {
    console.log("This is the old method.");
  }

  newMethod() {
    console.log("This is the new method.");
  }
}

const example = new ExampleClass();
example.oldMethod();
example.newMethod();

在上面的示例中,@deprecated 装饰器被添加到 oldMethod 上。这种装饰器会在调用 oldMethod 时发出警告,提醒开发者此方法已弃用。

扩展使用装饰器

可以结合其他装饰器进行组合,增加更多功能标记。

function logExecutionTime(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor,
) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    const start = performance.now();
    const result = originalMethod.apply(this, args);
    const finish = performance.now();
    console.log(`${propertyKey} executed in ${finish - start} milliseconds`);
    return result;
  };

  return descriptor;
}

class AdvancedExampleClass {
  @deprecated
  @logExecutionTime
  oldMethod() {
    console.log("This is the old method.");
  }

  @logExecutionTime
  newMethod() {
    console.log("This is the new method.");
  }
}

const advancedExample = new AdvancedExampleClass();
advancedExample.oldMethod();
advancedExample.newMethod();

在这个示例中,oldMethod 即被标记为已弃用,又记录了执行时间,而 newMethod 仅记录了执行时间。

装饰器的使用场景

方法装饰器

标记方法已弃用、日志记录、权限验证等

访问器装饰器

监控或更改属性访问行为。

function readOnly(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor,
) {
  descriptor.writable = false;
}

class ReadOnlyExample {
  @readOnly
  get getter(): string {
    return "This is a read-only property.";
  }
}

const readOnlyExample = new ReadOnlyExample();
console.log(readOnlyExample.getter);

属性装饰器

为属性添加元数据,如序列化、验证逻辑等。

function required(target: any, propertyKey: string) {
  // 使用Reflect.defineMetadata可定义元数据
  Reflect.defineMetadata("required", true, target, propertyKey);
}

class User {
  @required
  public name: string;

  constructor(name: string) {
    this.name = name;
  }
}

const user = new User("Alice");
const isNameRequired = Reflect.getMetadata("required", user, "name");
console.log(isNameRequired); // 输出:true

参数装饰器

为方法的参数添加元数据,如依赖注入、验证等。

function logParameter(
  target: any,
  propertyKey: string | symbol,
  parameterIndex: number,
) {
  const existingMetadata =
    Reflect.getOwnMetadata("log_parameters", target, propertyKey) || [];
  existingMetadata.push(parameterIndex);
  Reflect.defineMetadata(
    "log_parameters",
    existingMetadata,
    target,
    propertyKey,
  );
}

class LoggerExample {
  method(@logParameter message: string): void {
    console.log(message);
  }
}

const loggerExample = new LoggerExample();
loggerExample.method("Hello, World!");
const loggedParameters = Reflect.getMetadata(
  "log_parameters",
  loggerExample,
  "method",
);
console.log(loggedParameters);

装饰器模式的优缺点

优点

  1. 灵活性:装饰器模式允许我们动态添加或删除对象的功能,而不会影响其他对象。
  2. 可扩展性:通过创建新的装饰类,可以方便地扩展对象的功能,而不需要修改现有代码。
  3. 符合单一职责原则:每个装饰类只负责一个特定的功能,使得代码更具模块化。

缺点

  1. 复杂性增加:由于引入了多个装饰类,增加了系统的复杂性。
  2. 高度依赖:装饰器模式强调对象之间的组合,但过多的组合可能导致调试和维护困难。

总结

装饰器模式提供了一种灵活的解决方案来扩展对象的功能,而无需修改原始对象的代码。通过将对象包装在执行额外行为的装饰器中,这种模式不仅增强了代码的可复用性和灵活性,而且还促进了关注点的分离,从而更加简洁、高效地实现功能的动态扩展。