什么是 JavaScript 中的装饰器模式 (Decorator Pattern) 和代理模式 (Proxy Pattern)?

各位观众,晚上好!我是你们的老朋友,今天我们来聊聊JavaScript中的两种非常有趣的设计模式:装饰器模式和代理模式。别紧张,虽然名字听起来像高深的魔法,但其实它们都是解决实际问题的实用工具。

让我们开始吧!

第一部分:装饰器模式 (Decorator Pattern)

想象一下,你是一位咖啡师,你的任务是制作各种各样的咖啡。最基础的咖啡可能只是黑咖啡,但顾客们的需求千奇百怪:有人要加糖,有人要加奶,有人要加巧克力酱,还有人要加各种奇奇怪怪的配料。如果每次来一个新需求,你就修改黑咖啡的制作方法,那你会崩溃的。

装饰器模式就像是给咖啡加配料,它允许你动态地给对象添加新的功能,而不需要修改对象的原始代码。这就像是在黑咖啡的基础上,通过添加糖、奶等“装饰器”,来制作出不同口味的咖啡。

1.1 装饰器模式的核心概念

  • Component(组件): 这是被装饰的对象,也就是我们的黑咖啡。它定义了可以动态添加职责的接口。
  • ConcreteComponent(具体组件): 这是Component接口的具体实现,也就是具体的黑咖啡。
  • Decorator(装饰器): 这是一个抽象类或接口,它持有Component的引用,并定义了与Component相同的接口。它是所有具体装饰器的基类。
  • ConcreteDecorator(具体装饰器): 这是具体的装饰器类,它继承自Decorator,并实现了具体的装饰逻辑,比如加糖、加奶等。

1.2 代码示例:咖啡的装饰器

// Component 接口:咖啡
class Coffee {
  getDescription() {
    return "Unknown Coffee";
  }

  getCost() {
    return 0;
  }
}

// ConcreteComponent:黑咖啡
class SimpleCoffee extends Coffee {
  getDescription() {
    return "Simple Coffee";
  }

  getCost() {
    return 1;
  }
}

// Decorator 抽象类
class CoffeeDecorator extends Coffee {
  constructor(coffee) {
    super();
    this.coffee = coffee;
  }

  getDescription() {
    return this.coffee.getDescription();
  }

  getCost() {
    return this.coffee.getCost();
  }
}

// ConcreteDecorator:加奶
class MilkDecorator extends CoffeeDecorator {
  constructor(coffee) {
    super(coffee);
  }

  getDescription() {
    return this.coffee.getDescription() + ", Milk";
  }

  getCost() {
    return this.coffee.getCost() + 0.5;
  }
}

// ConcreteDecorator:加糖
class SugarDecorator extends CoffeeDecorator {
  constructor(coffee) {
    super(coffee);
  }

  getDescription() {
    return this.coffee.getDescription() + ", Sugar";
  }

  getCost() {
    return this.coffee.getCost() + 0.2;
  }
}

// 使用装饰器
let coffee = new SimpleCoffee();
console.log(coffee.getDescription() + " Cost: $" + coffee.getCost()); // Simple Coffee Cost: $1

coffee = new MilkDecorator(coffee);
console.log(coffee.getDescription() + " Cost: $" + coffee.getCost()); // Simple Coffee, Milk Cost: $1.5

coffee = new SugarDecorator(coffee);
console.log(coffee.getDescription() + " Cost: $" + coffee.getCost()); // Simple Coffee, Milk, Sugar Cost: $1.7

在这个例子中,Coffee 是 Component,SimpleCoffee 是 ConcreteComponent,CoffeeDecorator 是 Decorator,MilkDecoratorSugarDecorator 是 ConcreteDecorator。

通过这种方式,我们可以灵活地组合不同的装饰器,来创建各种各样的咖啡,而不需要修改 SimpleCoffee 的代码。

1.3 装饰器模式的优点

  • 灵活性: 可以动态地添加和删除对象的职责。
  • 可扩展性: 可以很容易地添加新的装饰器,而不需要修改现有的代码。
  • 避免类爆炸: 如果使用继承来实现不同的功能组合,可能会导致类的数量爆炸式增长。装饰器模式可以避免这种情况。
  • 符合开闭原则: 对扩展开放,对修改关闭。

1.4 装饰器模式的缺点

  • 增加复杂性: 可能会导致类的数量增加,增加代码的复杂性。
  • 调试困难: 多个装饰器嵌套在一起时,调试可能会比较困难。

1.5 JavaScript 中的装饰器语法 (ESNext)

ESNext 引入了装饰器语法,可以更方便地使用装饰器模式。需要注意的是,目前这个语法仍然是实验性的,需要使用 Babel 等工具进行转译。

function log(target, name, descriptor) {
  const original = descriptor.value;

  if (typeof original === 'function') {
    descriptor.value = function (...args) {
      console.log(`Calling ${name} with arguments: ${args}`);
      const result = original.apply(this, args);
      console.log(`Method ${name} returned: ${result}`);
      return result;
    };
  }
  return descriptor;
}

class Calculator {
  @log
  add(a, b) {
    return a + b;
  }
}

const calculator = new Calculator();
calculator.add(2, 3);

在这个例子中,@log 就是一个装饰器,它会记录 add 方法的调用信息。

第二部分:代理模式 (Proxy Pattern)

想象一下,你是一位明星的经纪人。明星很忙,不可能事事亲力亲为。所以,你需要代理明星处理一些事务,比如安排行程、处理合同、与媒体沟通等。

代理模式就像是明星的经纪人,它提供一个对象来控制对另一个对象的访问。代理对象通常会执行一些额外的操作,比如权限控制、缓存、延迟加载等。

2.1 代理模式的核心概念

  • Subject(主题): 定义了 RealSubject 和 Proxy 的共同接口,也就是明星需要做的事情。
  • RealSubject(真实主题): 定义了真正的对象,也就是明星本人。
  • Proxy(代理): 持有 RealSubject 的引用,并实现了与 Subject 相同的接口。代理对象可以在调用 RealSubject 之前或之后执行一些额外的操作。

2.2 代理模式的类型

  • 虚拟代理 (Virtual Proxy): 用于延迟加载 RealSubject,直到真正需要时才创建。
  • 远程代理 (Remote Proxy): 用于代表位于不同地址空间的对象,比如远程服务器上的对象。
  • 保护代理 (Protection Proxy): 用于控制对 RealSubject 的访问,比如权限控制。
  • 缓存代理 (Cache Proxy): 用于缓存 RealSubject 的结果,提高性能。

2.3 代码示例:图片的虚拟代理

// Subject 接口:图片
class Image {
  constructor(url) {
    this.url = url;
  }

  display() {
    console.log("Displaying image: " + this.url);
  }
}

// RealSubject:真实图片
class RealImage extends Image {
  constructor(url) {
    super(url);
    this.loadFromDisk();
  }

  loadFromDisk() {
    console.log("Loading image from disk: " + this.url);
  }

  display() {
    console.log("Displaying image: " + this.url);
  }
}

// Proxy:图片代理
class ProxyImage extends Image {
  constructor(url) {
    super(url);
    this.realImage = null;
  }

  display() {
    if (this.realImage === null) {
      this.realImage = new RealImage(this.url);
    }
    this.realImage.display();
  }
}

// 使用代理
const image1 = new ProxyImage("image1.jpg");
const image2 = new ProxyImage("image2.jpg");

// 第一次显示图片,会加载图片
image1.display(); // Loading image from disk: image1.jpg  Displaying image: image1.jpg

// 第二次显示图片,直接显示,不需要加载
image1.display(); // Displaying image: image1.jpg

image2.display(); // Loading image from disk: image2.jpg  Displaying image: image2.jpg

在这个例子中,Image 是 Subject,RealImage 是 RealSubject,ProxyImage 是 Proxy。

通过使用 ProxyImage,我们可以延迟加载图片,直到真正需要显示时才创建 RealImage 对象。这可以提高页面的加载速度。

2.4 代码示例:权限控制的保护代理

// Subject 接口:银行账户
class BankAccount {
  constructor(accountNumber, balance) {
    this.accountNumber = accountNumber;
    this.balance = balance;
  }

  deposit(amount) {
    this.balance += amount;
    console.log(`Deposited ${amount}. New balance: ${this.balance}`);
  }

  withdraw(amount) {
    if (amount > this.balance) {
      console.log("Insufficient funds.");
      return;
    }
    this.balance -= amount;
    console.log(`Withdrawn ${amount}. New balance: ${this.balance}`);
  }

  getBalance() {
    return this.balance;
  }
}

// Proxy:银行账户代理
class BankAccountProxy {
  constructor(accountNumber, balance, user) {
    this.account = new BankAccount(accountNumber, balance);
    this.user = user;
  }

  deposit(amount) {
    if (this.user.role !== "admin") {
      console.log("Insufficient permissions to deposit.");
      return;
    }
    this.account.deposit(amount);
  }

  withdraw(amount) {
    this.account.withdraw(amount);
  }

  getBalance() {
    return this.account.getBalance();
  }
}

// 用户
const user1 = { name: "Alice", role: "user" };
const user2 = { name: "Bob", role: "admin" };

// 创建代理
const account1 = new BankAccountProxy("1234567890", 100, user1);
const account2 = new BankAccountProxy("9876543210", 500, user2);

// Alice 尝试存款,没有权限
account1.deposit(50); // Insufficient permissions to deposit.

// Bob 存款,有权限
account2.deposit(100); // Deposited 100. New balance: 600

// Alice 取款,可以取款
account1.withdraw(20); // Withdrawn 20. New balance: 80

在这个例子中,只有拥有 admin 角色的用户才能存款,其他用户只能取款。

2.5 代理模式的优点

  • 控制访问: 可以控制对 RealSubject 的访问,比如权限控制、延迟加载等。
  • 灵活性: 可以在不修改 RealSubject 的情况下,添加额外的功能。
  • 提高性能: 可以通过缓存等方式来提高性能。

2.6 代理模式的缺点

  • 增加复杂性: 可能会导致类的数量增加,增加代码的复杂性。
  • 性能损失: 代理对象可能会增加一些额外的开销,导致性能损失。

第三部分:装饰器模式 vs 代理模式

虽然装饰器模式和代理模式都涉及到对象之间的包装,但它们的目的和使用场景是不同的。

特性 装饰器模式 代理模式
目的 动态地添加对象的职责 控制对对象的访问
关系 装饰器和组件通常实现相同的接口 代理和真实主题通常实现相同的接口
行为 装饰器添加新的行为,通常会修改原始对象的行为 代理控制访问,可能会延迟加载、权限控制等,不一定修改原始对象行为
应用场景 动态地添加功能,比如日志、缓存、权限控制等 延迟加载、远程访问、权限控制、缓存等

3.1 什么时候使用装饰器模式?

当你需要在运行时动态地给对象添加新的功能,而不想修改对象的原始代码时,可以使用装饰器模式。

3.2 什么时候使用代理模式?

当你需要控制对对象的访问,比如延迟加载、权限控制、远程访问等时,可以使用代理模式。

第四部分:总结

装饰器模式和代理模式都是非常有用的设计模式,可以帮助你编写更灵活、可扩展的代码。理解它们的区别和应用场景,可以让你在实际开发中更好地选择合适的模式。

希望今天的讲座对你有所帮助!记住,设计模式不是银弹,不要过度设计,选择最适合你的解决方案才是最重要的。

下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注