什么是设计模式?
设计模式是解决特定问题的可重用方案,是软件开发人员在长期实践中总结出的最佳实践。在JavaScript中,设计模式可以帮助我们编写更清晰、可维护和可扩展的代码。
设计模式的起源
设计模式的概念最早由建筑师克里斯托弗·亚历山大提出,用于描述建筑设计的通用解决方案。1994年,埃里希·伽玛等四位作者(被称为"四人帮")将这一概念引入软件工程领域,出版了《设计模式:可复用面向对象软件的基础》一书,奠定了软件设计模式的基础。
设计模式的核心原则
- 开闭原则 - 对扩展开放,对修改关闭
- 单一职责原则 - 一个类只负责一个功能领域
- 里氏替换原则 - 子类可以替换父类并保持程序正确性
- 接口隔离原则 - 使用多个专门的接口比使用单一的总接口要好
- 依赖倒置原则 - 依赖于抽象而不是具体实现
设计模式分类
设计模式通常分为三大类:创建型模式、结构型模式和行为型模式。
创建型模式
关注对象创建机制,以合适的方式创建对象
- 单例模式
- 工厂模式
- 抽象工厂模式
- 建造者模式
- 原型模式
结构型模式
关注类和对象的组合,形成更大的结构
- 适配器模式
- 装饰器模式
- 代理模式
- 外观模式
- 桥接模式
- 组合模式
- 享元模式
行为型模式
关注对象之间的职责分配和通信
- 观察者模式
- 策略模式
- 命令模式
- 状态模式
- 职责链模式
- 解释器模式
- 迭代器模式
- 中介者模式
- 备忘录模式
- 模板方法模式
- 访问者模式
JavaScript特有的设计模式
除了传统的面向对象设计模式,JavaScript还有一些特有的模式,充分利用了JavaScript的语言特性:
- 模块模式 - 利用闭包创建私有变量和方法
- 揭示模块模式 - 模块模式的变体,明确公开API
- 构造函数模式 - 使用构造函数创建对象
- 原型模式 - 利用原型链实现继承
- 混入模式 - 通过复制属性实现多重继承
创建型模式
创建型模式关注对象的创建机制,帮助我们以更灵活、更合适的方式创建对象,而不是直接使用new操作符。
1. 单例模式 (Singleton)
确保一个类只有一个实例,并提供全局访问点。单例模式常用于管理全局状态、配置信息、日志记录器等场景。
class Logger {
constructor() {
if (Logger.instance) {
return Logger.instance;
}
this.logs = [];
Logger.instance = this;
return this;
}
log(message) {
const timestamp = new Date().toISOString();
const logEntry = { message, timestamp };
this.logs.push(logEntry);
console.log(`[${timestamp}] ${message}`);
}
getLogs() {
return this.logs;
}
clearLogs() {
this.logs = [];
}
}
// 使用示例
const logger1 = new Logger();
const logger2 = new Logger();
console.log(logger1 === logger2); // true - 同一个实例
logger1.log('第一条日志');
logger2.log('第二条日志');
console.log(logger1.getLogs()); // 包含两条日志
// 现代ES6实现
const ConfigManager = (function() {
let instance;
let config = {};
function createInstance() {
return {
set(key, value) {
config[key] = value;
},
get(key) {
return config[key];
},
getAll() {
return { ...config };
}
};
}
return {
getInstance() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
const config1 = ConfigManager.getInstance();
const config2 = ConfigManager.getInstance();
console.log(config1 === config2); // true
单例模式应用场景
- 全局配置管理
- 日志记录器
- 数据库连接池
- 应用程序的状态管理
- 缓存系统
2. 工厂模式 (Factory)
创建对象而不暴露创建逻辑,通过工厂方法创建对象。工厂模式将对象的创建和使用分离,提高了代码的灵活性和可维护性。
// 简单工厂
class ButtonFactory {
static createButton(type) {
switch (type) {
case 'primary':
return new PrimaryButton();
case 'secondary':
return new SecondaryButton();
case 'danger':
return new DangerButton();
default:
throw new Error(`未知按钮类型: ${type}`);
}
}
}
class PrimaryButton {
render() {
return '<button class="btn btn-primary">主要按钮</button>';
}
}
class SecondaryButton {
render() {
return '<button class="btn btn-secondary">次要按钮</button>';
}
}
class DangerButton {
render() {
return '<button class="btn btn-danger">危险按钮</button>';
}
}
// 使用示例
const primaryBtn = ButtonFactory.createButton('primary');
console.log(primaryBtn.render());
// 抽象工厂
class UIFactory {
createButton() {
throw new Error('必须实现createButton方法');
}
createInput() {
throw new Error('必须实现createInput方法');
}
}
class LightThemeFactory extends UIFactory {
createButton() {
return new LightButton();
}
createInput() {
return new LightInput();
}
}
class DarkThemeFactory extends UIFactory {
createButton() {
return new DarkButton();
}
createInput() {
return new DarkInput();
}
}
class LightButton {
render() {
return '浅色主题按钮';
}
}
class DarkButton {
render() {
return '深色主题按钮';
}
}
工厂模式应用场景
- 创建复杂对象
- 需要根据不同条件创建不同对象
- 隐藏对象创建的复杂性
- 对象池管理
- 数据库连接创建
3. 建造者模式 (Builder)
将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。
class Pizza {
constructor() {
this.size = '';
this.crust = '';
this.toppings = [];
}
describe() {
return `一个${this.size}英寸的${this.crust}比萨,配料:${this.toppings.join(', ')}`;
}
}
class PizzaBuilder {
constructor() {
this.pizza = new Pizza();
}
setSize(size) {
this.pizza.size = size;
return this;
}
setCrust(crust) {
this.pizza.crust = crust;
return this;
}
addTopping(topping) {
this.pizza.toppings.push(topping);
return this;
}
build() {
return this.pizza;
}
}
// 使用示例
const myPizza = new PizzaBuilder()
.setSize(12)
.setCrust('薄脆')
.addTopping('芝士')
.addTopping('蘑菇')
.addTopping('火腿')
.build();
console.log(myPizza.describe());
结构型模式
结构型模式关注类和对象的组合,帮助我们构建更大的结构,同时保持结构的灵活性和效率。
4. 装饰器模式 (Decorator)
动态地为对象添加新功能,而不改变其结构。装饰器模式是继承的一个替代方案,提供了比继承更灵活的功能扩展方式。
// 基础组件
class Coffee {
cost() {
return 5;
}
description() {
return '普通咖啡';
}
}
// 装饰器基类
class CoffeeDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost();
}
description() {
return this.coffee.description();
}
}
// 具体装饰器
class MilkDecorator extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 2;
}
description() {
return this.coffee.description() + ', 加牛奶';
}
}
class SugarDecorator extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 1;
}
description() {
return this.coffee.description() + ', 加糖';
}
}
class WhippedCreamDecorator extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 3;
}
description() {
return this.coffee.description() + ', 加奶油';
}
}
// 使用示例
let myCoffee = new Coffee();
console.log(myCoffee.description(), '- 价格:', myCoffee.cost());
myCoffee = new MilkDecorator(myCoffee);
console.log(myCoffee.description(), '- 价格:', myCoffee.cost());
myCoffee = new SugarDecorator(myCoffee);
console.log(myCoffee.description(), '- 价格:', myCoffee.cost());
myCoffee = new WhippedCreamDecorator(myCoffee);
console.log(myCoffee.description(), '- 价格:', myCoffee.cost());
// ES7装饰器语法 (需要Babel转译)
function log(target, name, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
console.log(`调用 ${name} 方法, 参数:`, args);
const result = original.apply(this, args);
console.log(`方法 ${name} 返回:`, result);
return result;
};
return descriptor;
}
class Calculator {
@log
add(a, b) {
return a + b;
}
}
5. 适配器模式 (Adapter)
使不兼容的接口能够一起工作。适配器模式充当两个不兼容接口之间的桥梁,让它们能够协同工作。
// 旧系统接口
class OldPaymentSystem {
makePayment(amount, currency) {
console.log(`使用旧系统支付: ${amount} ${currency}`);
return `OLD_${Date.now()}`;
}
}
// 新系统期望的接口
class NewPaymentGateway {
processPayment(paymentDetails) {
// 期望接收 { amount: number, currency: string, method: string }
console.log(`使用新网关支付: ${paymentDetails.amount} ${paymentDetails.currency}`);
return `NEW_${Date.now()}`;
}
}
// 适配器
class PaymentAdapter {
constructor(oldSystem) {
this.oldSystem = oldSystem;
}
processPayment(paymentDetails) {
// 将新接口转换为旧接口
const { amount, currency } = paymentDetails;
return this.oldSystem.makePayment(amount, currency);
}
}
// 使用示例
const oldSystem = new OldPaymentSystem();
const adapter = new PaymentAdapter(oldSystem);
// 新系统可以使用适配器
const newGateway = new NewPaymentGateway();
// 模拟支付处理
function processPayment(gateway, payment) {
return gateway.processPayment(payment);
}
// 使用适配器让旧系统兼容新接口
const result1 = processPayment(adapter, {
amount: 100,
currency: 'USD',
method: 'credit'
});
const result2 = processPayment(newGateway, {
amount: 200,
currency: 'EUR',
method: 'paypal'
});
console.log('支付结果:', result1, result2);
6. 代理模式 (Proxy)
为其他对象提供一种代理以控制对这个对象的访问。代理模式可以在不改变原始对象的情况下,提供额外的功能。
// 真实对象
class RealImage {
constructor(filename) {
this.filename = filename;
this.loadFromDisk();
}
loadFromDisk() {
console.log(`加载图片: ${this.filename}`);
}
display() {
console.log(`显示图片: ${this.filename}`);
}
}
// 代理对象
class ProxyImage {
constructor(filename) {
this.filename = filename;
this.realImage = null;
}
display() {
if (this.realImage === null) {
this.realImage = new RealImage(this.filename);
}
this.realImage.display();
}
}
// 使用示例
const image = new ProxyImage('test.jpg');
// 图片此时还没有加载
image.display(); // 加载并显示图片
// 再次显示,不会重新加载
image.display();
行为型模式
行为型模式关注对象之间的职责分配和通信,帮助我们设计对象之间的交互和职责分配。
7. 观察者模式 (Observer)
定义对象间的一对多依赖关系,当一个对象状态改变时,所有依赖它的对象都会得到通知。
class Subject {
constructor() {
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer);
}
removeObserver(observer) {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
class Observer {
constructor(name) {
this.name = name;
}
update(data) {
console.log(`${this.name} 收到通知:`, data);
}
}
// 使用示例
const newsAgency = new Subject();
const subscriber1 = new Observer('订阅者1');
const subscriber2 = new Observer('订阅者2');
const subscriber3 = new Observer('订阅者3');
newsAgency.addObserver(subscriber1);
newsAgency.addObserver(subscriber2);
newsAgency.addObserver(subscriber3);
// 发布新闻
newsAgency.notify('今天天气晴朗');
newsAgency.removeObserver(subscriber2);
newsAgency.notify('明天有雨');
// 更现代的EventEmitter实现
class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(listener);
}
off(event, listener) {
if (!this.events[event]) return;
const index = this.events[event].indexOf(listener);
if (index > -1) {
this.events[event].splice(index, 1);
}
}
emit(event, data) {
if (!this.events[event]) return;
this.events[event].forEach(listener => {
listener(data);
});
}
once(event, listener) {
const onceWrapper = (data) => {
listener(data);
this.off(event, onceWrapper);
};
this.on(event, onceWrapper);
}
}
// 使用EventEmitter
const emitter = new EventEmitter();
const logData = (data) => console.log('收到数据:', data);
const logOnce = (data) => console.log('一次性监听:', data);
emitter.on('data', logData);
emitter.once('data', logOnce);
emitter.emit('data', '第一次发射'); // 两个监听器都会触发
emitter.emit('data', '第二次发射'); // 只有logData会触发
8. 发布-订阅模式 (Pub-Sub)
观察者模式的变体,使用主题/通道进行通信。发布-订阅模式中,发布者和订阅者不知道彼此的存在,通过消息代理进行通信。
class PubSub {
constructor() {
this.topics = {};
this.subId = -1;
}
subscribe(topic, callback) {
if (!this.topics[topic]) {
this.topics[topic] = [];
}
const token = (++this.subId).toString();
this.topics[topic].push({
token,
callback
});
return token;
}
publish(topic, data) {
if (!this.topics[topic]) {
return false;
}
this.topics[topic].forEach(subscriber => {
subscriber.callback(data);
});
return true;
}
unsubscribe(token) {
for (let topic in this.topics) {
if (this.topics[topic]) {
for (let i = 0; i < this.topics[topic].length; i++) {
if (this.topics[topic][i].token === token) {
this.topics[topic].splice(i, 1);
return token;
}
}
}
}
return false;
}
}
// 使用示例
const pubsub = new PubSub();
// 订阅主题
const subscription1 = pubsub.subscribe('user/login', (user) => {
console.log('发送登录通知邮件给:', user.email);
});
const subscription2 = pubsub.subscribe('user/login', (user) => {
console.log('记录用户登录日志:', user.username);
});
const subscription3 = pubsub.subscribe('user/logout', (user) => {
console.log('用户退出:', user.username);
});
// 发布事件
pubsub.publish('user/login', {
username: 'john_doe',
email: 'john@example.com',
loginTime: new Date()
});
// 取消订阅
pubsub.unsubscribe(subscription1);
pubsub.publish('user/login', {
username: 'jane_doe',
email: 'jane@example.com',
loginTime: new Date()
});
pubsub.publish('user/logout', {
username: 'john_doe'
});
9. 策略模式 (Strategy)
定义一系列算法,将它们封装起来,并且使它们可以相互替换。策略模式让算法的变化独立于使用算法的客户。
// 策略类
class NormalStrategy {
calculate(amount) {
return amount;
}
}
class DiscountStrategy {
constructor(discountRate) {
this.discountRate = discountRate;
}
calculate(amount) {
return amount * (1 - this.discountRate);
}
}
class PremiumStrategy {
calculate(amount) {
return amount * 0.8; // 20% 折扣
}
}
// 上下文类
class ShoppingCart {
constructor() {
this.items = [];
this.strategy = new NormalStrategy();
}
addItem(item) {
this.items.push(item);
}
setStrategy(strategy) {
this.strategy = strategy;
}
checkout() {
const total = this.items.reduce((sum, item) => sum + item.price, 0);
return this.strategy.calculate(total);
}
}
// 使用示例
const cart = new ShoppingCart();
cart.addItem({ name: '商品1', price: 100 });
cart.addItem({ name: '商品2', price: 200 });
console.log('普通价格:', cart.checkout()); // 300
cart.setStrategy(new DiscountStrategy(0.1)); // 10% 折扣
console.log('折扣价格:', cart.checkout()); // 270
cart.setStrategy(new PremiumStrategy()); // 20% 折扣
console.log('会员价格:', cart.checkout()); // 240
设计模式比较与选择指南
| 设计模式 | 类型 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 单例模式 | 创建型 | 需要全局唯一实例的场景 | 节省资源,严格控制访问 | 可能成为全局状态,难以测试 |
| 工厂模式 | 创建型 | 需要创建复杂对象的场景 | 封装创建逻辑,提高灵活性 | 增加了系统复杂度 |
| 装饰器模式 | 结构型 | 需要动态添加功能的场景 | 比继承更灵活,符合开闭原则 | 可能产生过多小对象 |
| 适配器模式 | 结构型 | 需要兼容不同接口的场景 | 提高代码复用性 | 可能增加系统复杂度 |
| 观察者模式 | 行为型 | 一对多的依赖关系场景 | 降低耦合度,支持广播通信 | 可能引起内存泄漏 |
| 策略模式 | 行为型 | 需要多种算法的场景 | 避免条件语句,易于扩展 | 客户端必须了解不同策略 |
- 需要控制对象创建 - 使用创建型模式(单例、工厂、建造者)
- 需要扩展对象功能 - 使用结构型模式(装饰器、适配器)
- 需要管理对象间通信 - 使用行为型模式(观察者、策略)
- 需要简化复杂接口 - 使用外观模式或适配器模式
- 需要实现算法族 - 使用策略模式
- 需要实现撤销操作 - 使用备忘录模式
实践练习
👆 请点击上方按钮进行演示操作
选择不同的设计模式演示来探索它们的实现原理和应用场景
设计模式最佳实践
- 不要过度设计 - 只在确实需要时使用设计模式,避免不必要的复杂性
- 了解JavaScript特性 - 充分利用JavaScript的动态性、闭包和原型链
- 优先使用组合而非继承 - JavaScript更适合使用组合和混入模式
- 考虑性能影响 - 某些设计模式可能带来性能开销,需要权衡
- 保持代码可读性 - 使用设计模式是为了提高代码质量,不是炫耀技巧
常见设计模式误用
- 单例模式滥用 - 可能导致全局状态污染和测试困难
- 过度使用继承 - 在JavaScript中,组合通常比继承更灵活
- 不必要的抽象 - 过早的抽象可能增加系统复杂度
- 模式堆砌 - 在一个地方使用太多设计模式,导致代码难以理解
下一步学习
掌握了设计模式后,可以继续学习:
- Web APIs - 探索浏览器提供的各种API
- JavaScript参考手册 - 查阅完整的JavaScript API参考
- 模块化 - 深入学习代码组织和架构
- ES6+新特性 - 学习现代JavaScript语法和特性