news 2026/10/30 8:46:11

设计模式实战:策略、观察者与装饰器模式如何应对需求变化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
设计模式实战:策略、观察者与装饰器模式如何应对需求变化

1. 设计模式的本质:不是套路,是应对变化的经验

“设计模式”这四个字,在软件开发圈子里几乎是被争论最多的技术概念之一。很多刚入行的开发者,听到的第一句劝告是“去把设计模式啃透”,紧接着可能又会听到另一句——“别生搬硬套设计模式,过度设计比没有设计更可怕”。这两句话听起来很矛盾,但实际上都对,只是对应了不同的阶段和语境。

我做了十多年开发,带过不少团队,也评审过大量代码,慢慢意识到设计模式解决的核心问题并不是“怎么写代码”,而是“怎么改代码”。大多数场景下,设计模式不会帮你把第一个版本写得更快,它帮你的是在第二版、第三版、第四次需求变更的时候,改动能不能在可控范围内发生,而不是牵一发动全身,把整个模块推倒重来。

经典的设计模式书籍会把模式分成创建型、结构型、行为型三大类,总共23种。这个分类体系很成熟,我至今仍然认可它的价值。但我在实际工作中更倾向于换一种方式去理解这些模式:创建型模式解决的是“对象怎么来的”,结构型模式解决的是“对象怎么组合的”,行为型模式解决的是“对象之间怎么协作的”。想清楚这三个问题,再回头看各种模式,你会发现它们并不是彼此孤立的技巧,而是一套完整的应对软件变化的方法论。

这篇内容,我会结合真实项目里的场景,把几个最高频、最实用的模式拆开讲透,包括它们的适用场景、实现思路、实战代码,以及我自己踩过的坑。模式不在多,真正能用好三五个,代码质量就会有质的提升。如果你是刚接触设计模式的开发者,这篇文章可以帮助你建立一个正确的认知框架;如果你已经写过一段时间业务代码,正在为频繁的需求变更头疼,这里面的实战经验应该能给你一些启发。

设计模式不是用来“炫技”的,它是用来“兜底”的。这句话是我这些年最大的体会。

2. 为什么需要设计模式:从一段“坏味道”代码说起

很多教程喜欢从定义开始讲设计模式,但我更愿意用一个实际的例子来引出这个问题。毕竟,如果没有痛点,就没有模式存在的意义。

2.1 一个真实场景:退款金额计算

假设你在做一个电商项目,里面有一个退款功能。第一版的需求很简单:用户申请退款,系统根据订单金额原路退回。代码写出来大概是这样的:

function calculateRefund(order: Order): number { return order.payAmount; }

这代码谁都能看懂,没有任何问题。但很快产品经理找过来了,说现在平台要推会员体系,会员用户退款时可以额外退积分对应的现金价值。于是你加了一个分支:

function calculateRefund(order: Order): number { let refund = order.payAmount; if (order.user.isMember) { refund += order.user.points * 0.01; } return refund; }

又过了一个月,运营说要搞大促活动,活动期间购买的商品,退款时要扣除优惠券分摊的金额。于是又加了一个分支:

function calculateRefund(order: Order): number { let refund = order.payAmount; if (order.user.isMember) { refund += order.user.points * 0.01; } if (order.promotion?.couponAmount) { refund -= order.promotion.couponAmount; } return refund; }

随着时间推移,退款规则越来越多:黑名单用户要扣除手续费、虚拟商品退款要按比例折算、特殊渠道订单退款走不同的审批逻辑……每个规则都往这个函数里塞一个 if 分支,这个函数越来越长,最后变成了一个几百行的“巨型函数”。不同规则之间还会互相影响,比如会员用户在活动期间购买虚拟商品,退款金额到底怎么算,你得把所有 if 分支的执行顺序都理一遍才能搞清楚。

2.2 这段代码的问题到底在哪里

表面上看这是“代码太乱”的问题,但本质上它反映的是变化被硬编码进了业务逻辑。每一次新增规则,都需要修改这个函数的内部实现,而修改就带来了风险——你可能影响了已有的规则,破坏了原有的行为,而且这些风险在回归测试之前是无法感知的。

用设计模式的专业术语来说,这段代码违反了开闭原则,也就是对扩展开放、对修改关闭。理想的状态是:新增一种退款规则时,原有代码一行都不用动,只需要增加一个新的规则类,然后在某个地方注册一下就行。

2.3 从坏味道到模式的推导过程

我们顺着这个思路往下走。既然每个规则是一个独立的东西,那就把“计算退款金额”这件事情抽象成一个团队:

  • 每个规则自己决定“我适不适用”,以及“我如何影响退款金额”。
  • 主流程只负责把这些规则串起来。
  • 新增规则不用碰主流程代码。

这个思路,其实就是策略模式的核心思想。我们可以定义一个品牌叫“退款计算策略”,每个具体策略实现自己的计算逻辑。主流程根据订单信息找到所有适用的策略,依次执行。

所以你看,设计模式不是凭空冒出来的,它是人们在反复踩坑之后总结出来的“标准解法”。理解了痛点,再看模式的定义,你会觉得这些概念都非常朴素,甚至有点理所当然。

3. 核心模式实战拆解:策略、观察者与装饰器

这一部分我挑三个最高频、实用性最强的模式,用真实项目的痛点和完整代码来讲清楚。每个模式都有一个核心问题要回答:它到底解决了什么变化?

3.1 策略模式:把“规则”变成“对象”

策略模式的定义很简单:定义一系列算法,把它们一一封装起来,并且使它们可以互相替换。在我上面那个退款计算的例子里,策略模式就是最直接的解法。

首先,定义一个策略接口:

interface RefundStrategy { // 判断当前策略是否适用于这笔订单 isApplicable(order: Order): boolean; // 计算策略对退款金额的影响,返回调整值(正数为加,负数为减) calculateAdjustment(order: Order): number; }

然后,把原来 if 里的每个规则,都实现成一个策略类:

class PayAmountBaseStrategy implements RefundStrategy { isApplicable(order: Order) { return true; } calculateAdjustment(order: Order) { return order.payAmount; } } class MemberPointsStrategy implements RefundStrategy { isApplicable(order: Order) { return order.user.isMember; } calculateAdjustment(order: Order) { return order.user.points * 0.01; } } class CouponDeductStrategy implements RefundStrategy { isApplicable(order: Order) { return !!order.promotion?.couponAmount; } calculateAdjustment(order: Order) { return -order.promotion.couponAmount; } }

主流程变成这样:

class RefundCalculator { constructor(private strategies: RefundStrategy[]) {} calculate(order: Order): number { const totalAdjustment = this.strategies .filter((s) => s.isApplicable(order)) .reduce((sum, s) => sum + s.calculateAdjustment(order), 0); return totalAdjustment; } }

以后新增任何退款规则,只需要新建一个类,然后在构造 RefundCalculator 时把新的策略传入即可,主流程代码完全不用改。这就是开闭原则最典型的体现。

从我实际使用的角度来看,策略模式适合的场景有几个比较明显的特征:同一件事有多种做法,且做法之间互不影响;这些做法可能会频繁扩展;调用方希望通过配置或注册的方式灵活组合行为。如果只是两三个固定的分支,硬写 if 完全没有问题,强行套策略模式反而是过度设计。

3.2 观察者模式:把“通知”从“主流程”中解耦

第二个高频场景是订单状态变化后的各种联动处理。下单成功后要发短信、发邮件、更新库存、给运营后台推送消息。第一版你可能会这么做:

function onOrderCreated(order: Order) { saveOrder(order); sendSms(order.user.mobile, "您的订单已创建"); sendEmail(order.user.email, "订单确认"); updateStock(order.items); notifyOperations(order); }

问题同样是那句话:新增一个联动动作,就要改主流程。而且主流程里堆了大量和“订单创建”本身无关的事情,代码可读性越来越差。

观察者模式的思路是把“事件发布”和“事件监听”解耦。订单创建这个事件就像一个广播电台,它只负责喊一嗓子“订单创建了”,至于谁在听、听完干什么,它不关心。各个监听者自己决定要不要响应、怎么响应。

实现如下:

// 定义事件类型和发布器 type OrderEvent = { type: string; order: Order }; class OrderEventPublisher { private listeners: Array<(event: OrderEvent) => void> = []; subscribe(listener: (event: OrderEvent) => void) { this.listeners.push(listener); } publish(event: OrderEvent) { this.listeners.forEach((listener) => listener(event)); } }

然后各个业务模块自行订阅:

const publisher = new OrderEventPublisher(); publisher.subscribe((event) => { if (event.type === 'ORDER_CREATED') { sendSms(event.order.user.mobile, '您的订单已创建'); } }); publisher.subscribe((event) => { if (event.type === 'ORDER_CREATED') { updateStock(event.order.items); } }); publisher.subscribe((event) => { if (event.type === 'ORDER_PAID') { notifyOperations(event.order); } });

主流程就一行:

function onOrderCreated(order: Order) { saveOrder(order); publisher.publish({ type: 'ORDER_CREATED', order }); }

观察者模式在真实项目里应用极广。事件总线、消息队列、前端的 addEventListener、Vue 的响应式系统、Redux 的订阅机制,本质上都是观察者模式思想在不同层面的实现。学会这个模式,你看很多框架源码时会豁然开朗。

3.3 装饰器模式:给核心逻辑“做增强”但不“动手术”

讲完行为型的策略、观察者,再看一个结构型的装饰器模式。装饰器模式解决的是“在不修改已有类代码的情况下,动态扩展对象功能”的问题,而且扩展的方式是层层包装,像给蛋糕叠奶油一样。

举一个我在实际项目中用过的例子。有一个第三方的支付客户端,提供了创建支付订单的能力。但我们的业务要求:所有支付请求都要记录日志,都要做耗时统计。你不可能去修改第三方库的源码,那怎么办?

装饰器模式的做法是:定义和原始类相同接口的包装类,在包装类里调用原始类的方法,并在前后插入额外的处理逻辑。

interface PaymentClient { createPayment(order: Order): PaymentResult; } class ThirdPartyPaymentClient implements PaymentClient { createPayment(order: Order): PaymentResult { // 调用第三方 SDK 的真实逻辑 } } class LoggingPaymentClient implements PaymentClient { constructor(private wrapped: PaymentClient) {} createPayment(order: Order): PaymentResult { console.time(`createPayment:${order.id}`); const result = this.wrapped.createPayment(order); console.timeEnd(`createPayment:${order.id}`); return result; } } class RetryPaymentClient implements PaymentClient { constructor(private wrapped: PaymentClient, private maxRetries = 3) {} createPayment(order: Order): PaymentResult { for (let attempt = 1; attempt <= this.maxRetries; attempt++) { try { return this.wrapped.createPayment(order); } catch (err) { if (attempt === this.maxRetries) throw err; } } } }

使用时可以任意组合:

let client: PaymentClient = new ThirdPartyPaymentClient(); client = new LoggingPaymentClient(client); client = new RetryPaymentClient(client, 3);

这样核心支付逻辑没有被修改,但扩展了日志、重试等能力,而且扩展还可以叠加。装饰器模式特别适合面向接口编程的体系,凡是“增强型需求”,如日志、缓存、鉴权、重试、限流,都可以用装饰器思路去做。

不过有一点需要注意,装饰器模式虽然灵活,但不要嵌套太深。超过三层以上的包装,调试起来会非常痛苦,栈信息会变得很长,排查问题的难度成倍增加。我的经验是,装饰器适合做“横切”性质的增强,不适合做复杂的业务逻辑编排。

4. 模式之间的搭配运用:工厂方法与策略的组合拳

如果你只学了单个模式,会觉得它们各自解决一个小问题。但在真实项目里,模式往往是组合在一起使用的。这一部分我讲一个我自己比较常用的组合套路,也是面试里经常会考到的场景。

4.1 问题场景:需要根据配置动态选择策略

回到第 3 节的退款例子。RefundCalculator 需要传入策略列表,那这个策略列表是怎么来的?如果策略永远固定不变,那还好说。但业务上往往是这样:不同类型的订单走不同的策略组合。普通订单只需要基础退款和优惠券抵扣规则;会员订单额外叠加积分算现金;虚拟商品订单要走特殊的比例折算。

这就面临一个问题:如何根据订单类型动态构建不同的策略组合?如果这步也用 if 硬编码在调用方,那前面策略模式带来的好处就被抵消了一半。

工厂方法模式就是用来解决“创建对象”的复杂逻辑的。我们可以做一个策略工厂:

type OrderCategory = 'PHYSICAL' | 'VIRTUAL' | 'MEMBER'; class RefundStrategyFactory { createStrategies(category: OrderCategory): RefundStrategy[] { switch (category) { case 'PHYSICAL': return [new PayAmountBaseStrategy(), new CouponDeductStrategy()]; case 'VIRTUAL': return [new VirtualGoodsProportionalStrategy()]; case 'MEMBER': return [ new PayAmountBaseStrategy(), new MemberPointsStrategy(), new CouponDeductStrategy(), ]; } } }

使用方变成:

const factory = new RefundStrategyFactory(); const calculator = new RefundCalculator(factory.createStrategies(order.category)); const refundAmount = calculator.calculate(order);

这样一来,策略的选择逻辑被收敛到工厂里,策略的执行逻辑被收敛到计算器里,调用方只需要知道“给我订单,算退款”,完全不知道背后有哪些策略在干活。新加一种订单类型,只需要改工厂的 switch 分支,不需要动计算器,也不需要动调用方。

4.2 策略+工厂:语义更清晰的另一种写法

上面这种写法是“模式组合”的入门形态。还有一种更贴近业务语义的做法,是把工厂的逻辑收进策略自身,让策略不仅知道自己“怎么算”,还知道自己“适用于谁”。这就是我之前定义 RefundStrategy 接口时为什么额外加了一个isApplicable方法的原因。

class OrderRefundProcessor { constructor(private strategies: RefundStrategy[]) {} process(order: Order): number { return this.strategies .filter((s) => s.isApplicable(order)) .reduce((sum, s) => sum + s.calculateAdjustment(order), 0); } }

这个时候你可以把策略列表注册成全局的一份,然后在处理时让每个策略自己判断是否适用。这种写法策略和策略之间没有显式的组合关系,但执行效果是等价的。哪种写法更好,取决于你的团队偏好和业务规模,没有绝对答案。

4.3 组合模式时最常见的一个误区

很多人学完多个模式后,容易陷入一种误区:把所有类都抽象出接口,所有创建过程都丢给工厂,所有事件都用观察者发布订阅。这套“全家桶”下来,代码确实很“设计模式”,但代价是极高的复杂度和极差的阅读体验。我见过有的项目,简单到只需要一个函数解决的问题,硬是抽象出了六个接口、五个类、三个工厂,最后连写出这段代码的人都解释不清调用关系了。

模式组合的核心原则是“按需使用,组合最小化”。如果你发现要组合三种以上模式才能实现一个功能,大概率是设计过头了。实际项目中,策略+工厂、观察者+发布器、装饰器+工厂,这三组是我认为性价比最高的组合方式。

5. 识别模式:阅读源码与重构遗留代码的实战心法

设计模式学习的门槛不在于“理解定义”,而在于“在真实代码里看出模式”。这一部分我讲一些阅读源码和重构遗留代码时的实战经验。

5.1 从类名和结构上识别模式

很多优秀的开源框架,类名本身就透露了模式信息。看到带Builder后缀的,基本可以判断用了建造者模式;看到带Factory后缀的,基本都是工厂方法或抽象工厂;看到带Adapter、Proxy、Decorator后缀的,结构型模式无疑。这不是巧合,而是行业惯例——命名是开发者之间最直接的沟通语言。

但更重要的是从结构上判断。我会在阅读源码时先问自己几个问题:

  • 这个接口为什么存在?它是否定义了某种“能力”的契约?
  • 这个类为什么包了一层另一个类?是为了增强、兼容还是隔离?
  • 这个方法的参数为什么传的是接口而不是具体类?它在依赖什么抽象?

举个例子,如果你看到一个类持有另一个类实例,并且方法内部调用了被持有对象的同签名方法,但调用前后还有额外的逻辑(缓存、日志、权限校验),那它大概率就是装饰器或者代理模式。如果你看到一组类都实现了同一个接口,并且某个上下文类可以动态选择使用哪个实现,那大概率是策略模式。

5.2 重构遗留代码时如何渐进式引入模式

面对一份遗留代码,我通常不建议推倒重来,也不建议一次性大改。正确的做法是滑动渐进式重构:先找到最痛苦的那一段代码,也就是改动最频繁、bug 最多的地方,分析它反复变化的原因,然后只针对这个变化点引入一种模式。

举个例子,一段订单导出的代码,里面根据导出格式分了 Excel、CSV、PDF 三种分支,而且每种格式的列名映射、数据处理都不相同。这个场景的变化点就是“导出格式”,可以直接用策略模式把三种格式拆成三个类。主流程从几十行的 if-else 压缩到几行。改完之后,新增一种导出格式只需要增加一个类,原有代码完全不受影响。

重构时我还有一个习惯:先写测试再动手。哪怕是遗留代码,也要先把当前行为用测试固定住。没有测试保护的重构,就像没有安全绳的攀岩,一旦失手代价极大。有了测试保护,每次重构完跑一遍,只要测试没过,说明你把行为改坏了,立刻回滚,安全很多。

5.3 一个适合练手的源码阅读清单

如果你想通过阅读源码来提升对设计模式的理解,我会推荐几个方向:

  • 前端框架的响应式系统,重点看它的订阅发布机制,对应观察者模式。
  • 主流 HTTP 客户端库,重点看它的拦截器机制,这类设计通常融合了装饰器模式和责任链模式。
  • 日志库的 appender 机制,重点看输出方式的扩展设计,对应策略模式。
  • 一些依赖注入容器源码,重点看对象创建流程和生命周期管理,对应各种工厂模式。

选一个你最熟悉的框架,先不要看实现细节,先画出它的整体结构,标注哪些接口、哪些类、哪些组合关系对应了什么模式。画完之后你会发现,原来天天在用的工具,底层思路就是这些经典模式的落地。这个过程对设计模式的理解提升,远大于把 23 种模式的定义背十遍。

6. 常见误用与踩坑记录:一张速查表与几条血泪经验

设计模式用不好,问题通常不在模式本身,而在于用错了场景、用错了尺度。这一部分我把这些年遇到的典型问题和排查心得整理出来,希望能帮你少走一些弯路。

6.1 一张笔记与对应解法速查表

典型症状你可能踩的坑调整方向
类数量爆炸,连自己都记不清哪些类负责什么过度拆分,一个接口对应一个实现还要套两层工厂合并相似的类,保留真正有差异的行为边界
调用链太深,排查问题时栈信息一大串装饰器、代理、责任链嵌套过多限制包装层数在 3 层以内,或者换成显式的事件处理流程
改一处业务逻辑,要动多个类把不该抽象的部分也抽象了,导致相关逻辑被拆散保持“高内聚”,强烈相关的逻辑放在同一个模块里
接口定义得特别抽象,看不懂传什么、返回什么为了“面向接口编程”而抽象,忽略业务语义接口要面向业务能力命名,而不是面向技术方法命名
工厂方法越写越复杂,开关分支几十个把工厂当成万能创建器,所有对象的创建都塞进去考虑用注册表或配置驱动替代部分 switch 分支

这张表是我复盘自己的项目时总结出来的,很多问题都是同一个根源:为了用模式而用模式,把设计模式当成了目标而不是手段。

6.2 关于“抽象时机”的一条核心经验

我见过太多开发者在功能还只有一个实现的时候,就按照“未来可能有多种实现”的假设,先把接口建好、把工厂搭好。等到未来真的来了第二个实现,发现当初的抽象方向和实际需求对不上,又推翻重来。

我的原则极为简单:第一条实现永远不要抽象,第二条实现出现时再看,第三条实现出现时才抽象。

抽象过早,你是在猜未来;抽象过晚,你是在付技术债。真正合适的时机通常是第二个实现出现的时候。那个时候你已经有了足够的信息来判断哪些是变化的、哪些是稳定的,抽象出来的接口大概率靠谱。

我举个具体例子。最早项目里只有一个短信通知渠道,我直接写了一个sendSms()函数。后来要加微信通知,我用 if 分支加了一个sendWechat()。到了第三个渠道邮件出现的时候,我才把通知抽象成Notifier接口,用工厂构建具体的渠道实例。前两次需求的改动虽然有点笨,但成本很低;第三次抽象的时候,因为有了两个现成的实现做参照,接口设计一次性就到位了。

6.3 模式学习与应用的正确姿势

最后给刚入门的朋友一点学习路径上的建议。先不要追求把所有 23 种模式都看完,先吃透三到五个最高频的:策略模式、观察者模式、装饰器模式、工厂方法模式、单例模式。这五个模式覆盖了大部分日常开发中的变化场景。

然后带着问题去读源码,从你日常使用的框架里找对应模式的落地案例。最后是在自己的项目里刻意练习,每次遇到“这里又要加一个分支”的时候,停下来想一想:这一处变化未来还会出现几次?如果会,能不能用策略模式拆出去?观察者模式能不能让主流程更干净?

设计模式不是考试题,它是一种思维习惯。真正理解它们之后,你会发现自己写代码时本能地开始考虑“未来这个点会怎么变”,这种意识比背熟任何模式定义都值钱。我到现在写一个新模块时,还是会经常停下来想想变化点在哪里,然后再决定要不要引入模式。大多数时候答案是“不用”,但这每一次思考都在帮我保持对代码结构的敏感度,这本身,就是设计模式带给我最大的收益。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/30 8:45:47

差分进化算法优化阵列天线副瓣电平的工程实践

1. 差分进化算法在阵列天线优化中的应用价值 阵列天线设计中&#xff0c;副瓣电平控制一直是个让人头疼的问题。传统设计方法往往需要反复试错&#xff0c;效率低下不说&#xff0c;效果还难以保证。作为一名长期从事天线优化的工程师&#xff0c;我亲身体会过手动调整参数时的…

作者头像 李华
网站建设 2026/10/30 8:44:40

SpringAI框架实战:简化Java应用AI集成

1. SpringAI项目概述SpringAI是Spring官方推出的AI集成框架&#xff0c;它让Java开发者能够以熟悉的Spring方式调用各类AI服务。我在最近的一个客服系统升级项目中首次接触这个框架&#xff0c;发现它确实大幅降低了AI能力接入门槛。举个例子&#xff0c;原本需要几十行代码才能…

作者头像 李华
网站建设 2026/10/30 5:56:37

openclaw优先读取机制:让爬虫先抓重点,稳定应对动态渲染

上个月我调一个抓取任务时遇到了一件让人头疼的事&#xff1a;页面内容明明几秒钟就返回了&#xff0c;可最重要的正文直到最后才被解析出来&#xff0c;又赶上那个区块走了动态渲染&#xff0c;超时后整个任务直接失败&#xff0c;重试了三轮才把数据补齐。后来我仔细看了日志…

作者头像 李华
网站建设 2026/10/24 22:38:51

DiT架构详解:从U-Net到Diffusion Transformer的视频生成革命

Sora 火了之后&#xff0c;视频生成这个赛道一下变得拥挤起来。每天刷 arXiv、GitHub Trending&#xff0c;你会发现一个新信号&#xff1a;不管闭源厂商还是开源社区&#xff0c;新一代视频模型的架构选择都惊人统一——DiT&#xff0c;Diffusion Transformer。Sora 的技术报告…

作者头像 李华
网站建设 2026/10/18 3:18:16

2026哪个平台小程序和网站都能做呢,看过来啦!

想知道哪个平台小程序和网站都能做&#xff1f;这三家平台值得摆上桌。艾瑞咨询《2026年中国企业数字化建站行业白皮书》里有个挺现实的数&#xff1a;国内 AI 建站渗透率已破 68%&#xff0c;但抽样超 1200 家中小企业中&#xff0c;仅 31% 在生成站点半年后仍持续续费且搜索流…

作者头像 李华
网站建设 2026/10/18 3:25:55

豆瓣电影爬虫与数据分析可视化系统设计与实现

简介&#xff1a;这是一套面向计算机专业本科生的毕业设计与课程大作业实战项目&#xff0c;基于PythonFlask构建豆瓣电影数据采集、存储、分析与可视化全流程系统&#xff0c;适用于期末大作业、毕业设计选题及爬虫与Web开发综合实践。资源包含110个文件&#xff0c;涵盖5个核…

作者头像 李华