3136 字
16 分钟
一文搞定常用设计模式
前 言本文不会对设计模式理论和实现进行系统的介绍,篇幅重点在于应用场景的总结。
如果有时间,推荐大家系统的对设计模式进行学习(无论是为了晋升、求职还是单纯想要提高自己的代码质量),在文章末尾有个人推荐一些相关的文章和书籍。
本文适合人群?
- 设计模式入门小白
- 不知道如何使用设计模式(应用场景)
本文你能收获什么?
- 一些常用的设计模式在日常开发中的应用场景和简单实现
什么是设计模式?
官方一点的解释:设计模式是经过验证的、可复用的解决方案,用于解决软件设计中的常见问题。
通俗点讲,设计模式就像游戏玩家的”攻略”或厨师的”菜谱”。在程序世界,编程的”套路”就是设计模式,即针对特定问题、场景的较优解决方案。
为什么需要设计模式?
引用鲁迅在《故乡》中的一句话:“其实地上本没有路,走的人多了,也便成了路”,设计模式便是前人总结出来的”路”。当遇到相似的问题或场景的时候,就能利用这些经验和套路更快给出更优的解决方案,也算是“踩在巨人的肩膀上前进”了
一些常用的设计模式:
策略模式(Strategy Pattern)
概 述
- 定义:策略模式属于行为型设计模式,其通常定义了一系列算法(策略),并且每个算法封装成独立的类,使它们可以想换替换。这样做,可以将算法(策略)的变化独立于客户端,从而可以便捷的动态切换算法,避免冗余的条件判断。
- 核心思想:将做什么(目标)和怎样做(具体实现)分开。
- 核心组成:
- 策略接口(Strategy Interface)
- 定义算法的公共策略
- 具体策略类(Concrete Strategies)
- 策略的具体算法实现
- 上下文类(Context)
- 持有一个策略对象的引用,通过策略接口调用具体算法(客户端代码与具体策略的桥梁角色)
- 例子:在使用导航的时候,你只需要提供你的出发地和目的地,而具体走哪条线路(高速优先、避开拥堵、最短距离等)由导航策略决定,变更线路的时候,只需要切换策略即可。
场景:电商促销折扣策略
假设一个电商平台需要支持多种促销折扣(无折扣、满减、打折),通过策略模式动态切换折扣策略。
// 策略接口,定义折扣计算方法的规范
interface DiscountStrategy {
calculate(price: number): number;
}
// 具体策略类: 实现不同的折扣算法
// 原价
class NoDiscount implements DiscountStrategy {
calculate(price: number): number {
return price;
}
}
// 满减
class FullReduction implements DiscountStrategy {
constructor(private threshold: number, private reduction: number) {}
calculate(price: number): number {
if (price >= this.threshold) {
return price - this.reduction;
}
return price;
}
}
// 打折
class PercentageDiscount implements DiscountStrategy {
constructor(private percentage: number) {}
calculate(price: number): number {
return price * (1 - this.percentage / 100);
}
}
// 上下文类
class ShoppingCart {
private strategy: DiscountStrategy;
constructor(strategy: DiscountStrategy = new NoDiscount()) {
this.strategy = strategy;
}
setStrategy(strategy: DiscountStrategy) {
this.strategy = strategy;
}
checkout(price: number): number {
return this.strategy.calculate(price);
}
}
// 使用示例:
const cart = new ShoppingCart();
// 默认无折扣
console.log(cart.checkout(100)); // 100
// 切换为满减策略(满200减50)
cart.setStrategy(new FullReduction(200, 50));
console.log(cart.checkout(300)); // 250
// 切换为打折策略(8折)
cart.setStrategy(new PercentageDiscount(20));
console.log(cart.checkout(300)); // 240
适用场景可以看到上面的方式节省了大量的
if...else...
,还有一些其他适用场景比如:表单验证规则、权限校验、支付方式切换等等。
当满足以下情况时,都可以考虑使用:
- 各判断条件下的策略相互独立并且可复用
- 策略内部逻辑相对复杂
- 策略需要灵活组合
发布-订阅模式
概 述
- 定义:发布-订阅属于行为型设计模式,它定义了一种一对多的依赖关系:当一个对象(发布者)的状态发生改变时,所有依赖它的对象(订阅者)都会自动收到通知并执行相应操作。
- 核心特点:
- 发布者和订阅者完全解耦,彼此不直接依赖
- 通过事件通道传递消息
- 支持动态添加和移除订阅者
- 核心组成:
- 事件通道(Event Channel)
- 维护订阅关系(事件类型与订阅者的映射)
- 提供订阅(subscribe)、取消订阅(unsubscribe)和发布(publish)方法
- 发布者(Publisher)
- 触发事件,通过事件通道发布消息
- 订阅者(Subscriber)
- 事件通道注册回调函数,收到通知后执行操作
- 例子:
- 微信公众号:
- 发布者(公众号):负责发布新文章
- 订阅者(用户):关注公众号后,新文章会自动推送。
- 事件通道(微信平台):负责管理订阅关系,将文章分发给所有订阅者。
场景:实现一个事件总线
type EventHandler = (...args: any[]) => void;
class EventBus {
private events: Map<string, EventHandler[]> = new Map();
// 订阅事件
subscribe(eventName: string, handler: EventHandler): void {
const handlers = this.events.get(eventName) || [];
handlers.push(handler);
this.events.set(eventName, handlers);
}
// 取消订阅
unsubscribe(eventName: string, handler: EventHandler): void {
const handlers = this.events.get(eventName);
if (!handlers) return;
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
}
// 发布事件
publish(eventName: string, ...args: any[]): void {
const handlers = this.events.get(eventName);
if (handlers) {
handlers.forEach(handler => handler(...args));
}
}
}
// 使用示例
const eventBus = new EventBus();
// 订阅者A:监听登录事件
eventBus.subscribe('login', (user: string) => {
console.log(`订阅者A:用户 ${user} 已登录`);
});
// 订阅者B:监听登录事件
const handlerB = (user: string) => {
console.log(`订阅者B:欢迎 ${user}!`);
};
eventBus.subscribe('login', handlerB);
// 发布登录事件
eventBus.publish('login', '张三');
// 输出:
// 订阅者A:用户 张三 已登录
// 订阅者B:欢迎 张三!
// 取消订阅者B
eventBus.unsubscribe('login', handlerB);
// 再次发布事件
eventBus.publish('login', '李四');
// 输出:
// 订阅者A:用户 李四 已登录
适用场景
- 跨模块/组件通信:多个独立模块或组件需通信,但直接依赖会导致耦合
- 实时事件驱动系统(比如实时聊天系统):需要实时响应异步事件并广播给多个消费者
- 日志和监控系统:多个子系统需要监听核心事件以记录日志或监控状态
- 插件化架构:允许第三方插件通过订阅事件扩展核心功能
- 批量任务处理:将任务分解为多个步骤,通过事件驱动异步处理
- … …
判断是否使用发布订阅模式的一些标准:
- 解耦需求:系统需要减少模块间的直接依赖
- 多对多通信:存在多个发布者和多个订阅者(如聊天室中所有用户既是发布者又是订阅者)
- 动态订阅关系:订阅关系需在运行时动态调整(如用户自定义通知偏好)
- 异步处理:事件处理可延迟或并行执行(如日志记录、非关键任务)
- 可扩展性需求:未来可能新增事件类型或订阅者(如插件系统)
观察者模式
概述
- 定义:观察者模式是一种行为型设计模式,定义了一种一对多的依赖关系,当一个对象(被观察者/Subject)的状态发生改变时,所有依赖它的对象(观察者/Observers)都会自动收到通知并更新。
- 核心思想:松耦合、同步、静态注册(需显式添加/移除观察者)
- 核心组成:
- 观察者
- 定义更新接口(如 update()),用于接收被观察者的状态变更通知
- 被观察者
- 维护观察者列表
- 提供注册(attach)、移除(detach)和通知(notify)方法
场景:天气预报通知系统(当天气数据更新时,多个显示设备(手机、电视)自动刷新)
气象站需要实时、同步地将温度变化通知所有显示设备(如手机、电视),确保数据更新的即时性。
interface Observe {
update(temperature: number): void
}
// 被观察者
class WeatherStation {
private observers: Observe[] = []
private temperature: number = 0
// 注册观察者
attach(observer: Observe) {
this.observers.push(observer)
}
// 移除观察者
detach(observer: Observe) {
const index = this.observers.indexOf(observer)
if(index > -1) {
this.observers.splice(index, 1)
}
}
// 通知观察者
notify() {
this.observers.forEach(observer => observer.update(this.temperature))
}
// 更新温度并通知观察者
setTemperature(temperature: number) {
this.temperature = temperature
this.notify()
}
}
// 具体观察者
class PhoneDisplay implements Observe{
update(temperature: number): void {
console.log(`手机显示温度:${temperature}℃`);
}
}
class TvDisplay implements Observe {
update(temperature: number): void {
console.log(`电视显示温度:${temperature}℃`);
}
}
// 使用示例
const weatherStation = new WeatherStation()
const phone = new PhoneDisplay()
const tv = new TvDisplay()
// 显示注册观察者
weatherStation.attach(phone)
weatherStation.attach(tv)
weatherStation.setTemperature(20)
// 输出:
// 手机显示温度:25°C
// 电视显示温度:25°C
weatherStation.detach(tv);
weatherStation.setTemperature(30);
// 输出:手机显示温度:30°C
适用场景
- UI组件联动: 例如表单输入框内容变化时,实时更新预览区域
- 状态管理工具: 例如 Redux 中 Store 状态变化触发 React 组件更新
- 实时数据监控:例如股票价格变动时通知多个显示终端
如何选择?
观察者模式:需要直接控制通知逻辑、保证实时性(如UI更新、游戏引擎)。 用发布订阅模式:需要解耦跨模块通信、支持动态扩展(如微服务、事件总线)。
装饰器模式
概 述
- 定义:装饰器模式是一种结构型设计模式,允许通过将对象放入包含行为的特殊封装对象中来动态地为对象添加新的行为。该模式通过组合替代继承,提供了比继承更灵活的扩展功能方式
- 核心思想:
- 动态扩展功能:在不修改原有对象结构的情况下,通过包装对象(装饰器)来增强其功能。
- 组合优于继承:避免因多层级继承导致的类爆炸问题。
- 核心组成:
- 组件接口(Component):定义被装饰对象和装饰器的公共接口,确保装饰器与被装饰对象类型兼容。
- 具体组件(Concrete Component):实现组件接口的基础对象,即需要被动态扩展功能的对象。
- 装饰器基类(Decorator):持有一个组件接口的实例,并实现组件接口,通常通过委托调用被装饰对象的方法。
- 具体装饰器(Concrete Decorators):继承装饰器基类,添加额外的功能或修改原有行为。
场景:React高阶组件(HOC)
动态为React组件添加日志、权限校验等能力
// 高阶组件(HOC)作为装饰器
import React from 'react';
// 组件接口:原始组件
interface ButtonProps {
onClick: () => void;
label: string;
}
class Button extends React.Component<ButtonProps> {
render() {
return <button onClick={this.props.onClick}>{this.props.label}</button>;
}
}
// 装饰器:日志记录
function withLogging<P extends object>(WrappedComponent: React.ComponentType<P>) {
return class extends React.Component<P> {
componentDidMount() {
console.log("组件已挂载");
}
render() {
return <WrappedComponent {...this.props} />;
}
};
}
// 装饰器:权限校验
function withAuthCheck<P extends { isAuthenticated: boolean }>(
WrappedComponent: React.ComponentType<P>
) {
return class extends React.Component<P> {
render() {
if (this.props.isAuthenticated) {
return <WrappedComponent {...this.props} />;
} else {
return <div>请先登录</div>;
}
}
};
}
// 组合装饰器
const EnhancedButton = withLogging(withAuthCheck(Button));
// 使用装饰后的组件
const App = () => (
<EnhancedButton
label="点击我"
onClick={() => console.log("按钮点击")}
isAuthenticated={true}
/>
);
适用场景总结
- 需要动态添加或撤销功能:如运行时配置日志、缓存等。
- 避免多层继承导致代码臃肿:如多种可选功能的组合。
- 扩展第三方库对象:无法修改源码时,通过装饰器增强功能。
适配器模式
代理模式
责任链模式
Q & A
Q: 观察者模式和发布-订阅模式的主要区别?
特性 | 观察者模式 | 发布订阅模式 |
---|---|---|
耦合性 | 松耦合:被观察者直接持有观察者的引用。 | 完全解耦:通过事件通道间接通信。 |
通信方式 | 同步:被观察者直接调用观察者的方法。 | 异步:通过事件通道分发消息。 |
关系管理 | 静态注册:需显式添加/移除观察者。 | 动态订阅:可随时订阅或取消事件。 |
典型应用 | UI组件更新、状态管理。 | 跨模块通信、微服务事件驱动。 |
相关文章/资料:
- 掘金:前端中的设计模式
- Waiting for api.github.com...
- Waiting for api.github.com...
- Waiting for api.github.com...
- 《图解设计模式》——【日】结城浩 人民邮电出版社
- 《大话设计模式》——程杰 清华大学出版社