各位观众,晚上好!我是你们的老朋友,今天我们来聊聊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,MilkDecorator
和 SugarDecorator
是 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 什么时候使用代理模式?
当你需要控制对对象的访问,比如延迟加载、权限控制、远程访问等时,可以使用代理模式。
第四部分:总结
装饰器模式和代理模式都是非常有用的设计模式,可以帮助你编写更灵活、可扩展的代码。理解它们的区别和应用场景,可以让你在实际开发中更好地选择合适的模式。
希望今天的讲座对你有所帮助!记住,设计模式不是银弹,不要过度设计,选择最适合你的解决方案才是最重要的。
下次再见!