设计模式(三):行为型模式与综合设计
设计模式(三):行为型模式与综合设计
导语:行为型模式是面试最常问"项目怎么用"的一章。本篇讲透观察者、策略、状态、责任链、模板方法五个高频模式的原理与落地,说清「策略 vs 状态」这组最易混的对比、状态机框架选型、其余行为型速览;最后给出三组高频综合设计题(支付系统、订单系统,以及如何判断该用哪个模式)与"你项目里用了什么设计模式"的高分答法,共 12 题。
一、高频行为型模式
1. 观察者模式是什么?项目里怎么落地?
答:
核心意图:定义对象间的一对多依赖——当一个对象(被观察者/主题,Subject)状态改变时,所有依赖它的对象(观察者,Observer)自动收到通知并更新,从而实现发布方与订阅方的解耦。
项目落地(必答的"支付成功"场景):
❌ 不用模式:核心方法被业务分支「污染」
违反 OCP;一处报错可能影响后续所有步骤;一次支付要串行等待所有下游。
✅ 用观察者/事件:核心只负责「宣布事实」
各监听器独立注册、独立实现、独立异常处理;核心代码一行不动(符合 OCP)。
Spring 中的落地方式(写出来显专业):
// ① 定义事件(继承 ApplicationEvent,Spring 4.2+ 可直接用普通 POJO)
public class PaymentSuccessEvent {
private final Long orderId;
private final BigDecimal amount;
// 构造器 + getter
}
// ② 发布事件
@Service
public class PaymentService {
private final ApplicationEventPublisher publisher; // 构造器注入
@Transactional
public void pay(Long orderId) {
// ... 核心支付逻辑
publisher.publishEvent(new PaymentSuccessEvent(orderId, amount));
}
}
// ③ 监听(异步 + 事务后触发,生产必备的两个注解)
@Component
public class PointListener {
@Async // 异步执行,不阻塞支付主流程
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) // ★ 主事务提交成功后才执行
public void onPaymentSuccess(PaymentSuccessEvent event) {
// 加积分
}
}⚠️
@EventListener与@TransactionalEventListener的区别是关键加分点:前者在事件发布的那一刻就执行(此时主事务可能还没提交),如果监听器去查库可能查不到数据;后者可以指定AFTER_COMMIT(提交后)/BEFORE_COMMIT/AFTER_ROLLBACK,保证"事务成功后才做后续动作"——这是"支付成功了但积分没加"这类问题的根治方式。
观察者的优缺点:
| 优点 | 缺点 |
|---|---|
| 发布方与订阅方解耦,符合 OCP | 观察者太多或 update 耗时会阻塞主线程(需异步化) |
| 支持广播,一对多天然扩展 | 容易产生级联/循环调用(A 监听 B、B 又监听 A) |
| 各观察者可独立实现与测试 | 难以调试(调用链不体现在代码里,"谁触发了这个方法"不好追) |
顺序不确定(除非显式指定 @Order) |
2. 观察者模式的落地追问:如何保证"不丢、不阻塞、不出错"?
答: 这是把"背模式"与"做过项目"区分开的一组追问,建议按"四问"回答。
| 追问 | 答案 |
|---|---|
| ① 同步观察者会不会阻塞? | 会。默认是同步(publishEvent 同步调用所有监听器),只要有一个监听器慢(发邮件、调外部接口),支付接口的 RT 就被拖长。解法:@Async 异步化、或把非核心逻辑改为发 MQ |
| ② 观察者抛异常怎么办? | 默认情况下一个监听器抛异常会影响其他监听器(甚至回滚主事务)。解法:① 监听器内部自己 try-catch(非核心动作绝不能拖垮主流程);② 异步执行(异常不再影响主线程,但要自己记录并告警);③ 对关键监听器,反而要让异常冒出来(如"扣积分失败必须回滚支付")——要区分"核心/非核心" |
| ③ 需要异步 MQ 吗? | 看一致性要求与可靠性要求: · 进程内事件( ApplicationEvent):简单,但应用重启/宕机时事件会丢,只适合"丢了也无所谓或可重算"的场景· MQ:跨应用、可持久化、可重试、可削峰,但引入最终一致性与幂等要求 |
| ④ 如何保证事件不丢? | 进程内事件没有"不丢"的保证(无持久化)。要可靠性就得用 MQ + 本地消息表/事务消息(见《消息队列》与《Seata》),并配幂等消费 + 重试 + 死信队列 + 对账 |
「观察者模式 vs 消息队列」的准确关系(面试高频):
✅ 观察者是【设计层面的"一对多通知关系"】——描述"谁通知谁"
✅ MQ 是【基础设施层面的"跨进程消息传递"】——解决"怎么可靠地传"
→ 两者可以结合(观察者的实现可以是 MQ),但【不能画等号】
对照表:
维度 进程内观察者(ApplicationEvent) MQ
范围 同一 JVM 进程内 跨进程/跨机器
可靠性 重启即丢 可持久化、可重试
耦合 编译期/容器内注册 仅依赖 Topic 契约
延迟 微秒级 毫秒~秒级
适用 同应用内的横切逻辑(缓存刷新、日志) 跨服务、需要可靠与削峰的流程
⚠️ 一句话:**"观察者模式 + MQ" ≠ "观察者模式",把事件发到 MQ 后,就进入了分布式一致性的话题。**3. 策略模式是什么?项目里怎么落地?
答:
核心意图:定义一系列算法,分别封装起来,使它们可以相互替换——让算法的变化独立于使用它的客户端。
项目落地(电商两个经典场景):
// 场景一:促销计算
public interface PromotionStrategy {
BigDecimal calculate(BigDecimal originalPrice, OrderContext ctx);
}
@Component("FULL_REDUCTION")
class FullReductionStrategy implements PromotionStrategy { /* 满减 */ }
@Component("DISCOUNT")
class DiscountStrategy implements PromotionStrategy { /* 折扣 */ }
@Component("SECOND_HALF")
class SecondHalfStrategy implements PromotionStrategy { /* 第二件半价 */ }
// 场景二:支付渠道(用【注册表】避免 if/else 选择)
public interface PaymentStrategy {
PayResult pay(PayRequest request);
String channel(); // ★ 每个策略声明自己支持的渠道
}
@Component
public class PaymentStrategyFactory {
// Spring 把同类型的所有实现注入为 List
private final Map<String, PaymentStrategy> registry = new HashMap<>();
public PaymentStrategyFactory(List<PaymentStrategy> strategies) {
strategies.forEach(s -> registry.put(s.channel(), s)); // 自注册
}
public PaymentStrategy get(String channel) {
PaymentStrategy strategy = registry.get(channel);
if (strategy == null) {
throw new IllegalArgumentException("不支持的支付渠道: " + channel);
}
return strategy;
}
}
// 调用方(核心逻辑不再有 if/else,新增渠道不改这里)
public PayResult pay(PayRequest request) {
return strategyFactory.get(request.getChannel()).pay(request);
}策略模式的三个价值:
| 价值 | 说明 |
|---|---|
| 消除冗长条件分支 | 把 if-else/switch 转成一组独立的策略类(可读性、可测试性都提升) |
| 符合 OCP | 新增算法 = 新增策略类 + 注册,不改 Context |
| 算法可独立测试与复用 | 每个策略是一个独立单元,单测非常容易(不用构造一堆前置状态) |
策略模式的代价(要主动说):
- 策略类数量膨胀(10 种算法 = 10 个类),小逻辑上策略反而更啰嗦;
- 调用方需要知道有哪些策略("选择逻辑"没被消除,只是被移到了工厂/注册表);
- 策略之间通常无状态、无关联——如果需要"状态记忆"或"自动转移",那应该看状态模式。
4. 策略模式和状态模式有什么区别?(最易混的一组)
答: 两者结构几乎完全相同(Context + 抽象接口 + 多个实现类),区别完全在意图:
| 维度 | 策略模式 | 状态模式 |
|---|---|---|
| 解决的问题 | 同一行为有多种算法,怎么选 | 对象在不同状态下行为不同 |
| 关注点 | How(怎么做) | Next State(下一个状态是什么) |
| 谁决定切换 | 客户端/上层主动设置(setStrategy(x)) | 状态内部逻辑触发(对客户端透明) |
| 实现类之间关系 | 相互独立、彼此不知道对方 | 彼此知道(知道自己在什么事件下转到哪个状态) |
| 是否保存状态转移 | 不涉及 | 核心是状态转移图 |
| 典型场景 | 支付渠道、促销规则、排序规则、压缩算法 | 订单状态机、工作流引擎、TCP 连接状态、游戏角色状态 |
| 一句话 | 「选哪种算法」 | 「现在处于什么状态,允许做什么」 |
面试表述模板:「两者结构很像,但意图不同:策略强调"灵活选择算法",切换由客户端或上层主动决定,策略之间彼此独立、互不感知;状态强调"状态变化导致行为变化",状态的转移由状态自身的内部逻辑决定,对客户端透明,各状态之间构成了一个转移图。所以订单流转该用状态,支付渠道选择该用策略。」
一个实战组合(很加分):
现实中两者常常【一起用】:
订单状态机(State)负责"当前状态允许什么操作"
│ 例如"已支付"状态下允许发起退款
▼
具体算法(Strategy)负责"这个操作怎么做"
│ 例如退款用"原路退回"还是"退到余额"
▼
责任链(Chain)负责"操作前的校验"
│ 权限 → 金额上限 → 风控5. 责任链模式是什么?为什么 Filter / Interceptor 是典型?
答:
核心意图:把请求的发送者与接收者解耦——把多个处理者串成一条链,每个处理者决定「自己处理」「交给下一个」「直接中断」。
每个处理器只需关心三件事:① 自己是否处理 ② 处理完是否继续传递 ③ 是否中断。
为什么 Filter / Interceptor 是典型(面试要点):
| 对比 | Filter | Interceptor |
|---|---|---|
| 所属规范 | Servlet 规范(javax/jakarta.servlet.Filter) | Spring MVC(HandlerInterceptor) |
| 执行位置 | 在 DispatcherServlet 之前/之后(属于 Servlet 容器层) | 在 DispatcherServlet 内部(能拿到 HandlerMethod) |
| 能否拿到 Controller 方法信息 | ❌ 不能(只有 ServletRequest) | ✅ 能(preHandle 有 HandlerMethod) |
| 能否访问 Spring 容器 Bean | 可以(但注入时机需注意) | ✅ 天然支持(本身是 Bean) |
| 典型用途 | 编码设置、CORS、请求日志、XSS 过滤、FilterChainProxy(Spring Security 的整条安全链) | 权限校验、登录态校验、接口耗时统计、灰度路由 |
| 方法 | doFilter(req, resp, chain) —— 必须显式调用 chain.doFilter() 才继续 | preHandle / postHandle / afterCompletion |
// Filter:不调用 chain.doFilter 就"中断"了后续所有处理
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) {
if (!checkAuth(req)) {
writeError(resp, 401);
return; // ★ 不传下去 = 短路
}
chain.doFilter(req, resp); // ★ 传给下一个
}责任链的四个关键设计问题(面试常追问):
| 问题 | 说明与做法 |
|---|---|
| 顺序如何确定? | ① 显式 @Order / Ordered 接口(Spring 的 Filter/Interceptor 都支持);② 配置文件里声明;③ 代码里显式 setNext() 组装。顺序错乱会导致"鉴权在限流之后"这类安全/逻辑问题 |
| 是否允许短路? | 允许(这是责任链的价值之一)。要明确定义每个节点的"能否中断"语义——例如"日志节点必须放行"、"鉴权节点失败即中断" |
| 异常怎么传递? | 两种风格:① 抛出异常(Spring 的 FilterChainProxy 会把安全异常转换成响应);② 返回状态对象(如 Result.fail())。要统一,避免一部分抛异常、一部分返回错误码导致调用方漏判 |
| 如何构建动态链? | 按运行时条件组装(如"灰度用户多走一个校验节点")。注意:动态链会让"到底走了哪些节点"变得难以追踪 → 必须打日志/埋点记录实际链路 |
责任链的代价(要主动说):
- 调试困难:请求"经过了哪些处理器、在哪一步被短路"不体现在代码里,必须靠日志;
- 性能:链太长会有累积开销(每节点都可能查库/发 RPC);
- 链的顺序是隐式契约:改动顺序可能引发意外行为。
6. 模板方法模式是什么?为什么 Spring 用 Callback 配合它?
答:
核心意图:在父类(或模板类)中定义算法的骨架,把可变的步骤延迟到子类实现(或通过回调传入)——复用不变流程,约束可变部分。
经典实例:
| 实例 | 骨架(固定) | 可变步骤 |
|---|---|---|
AbstractList / AbstractCollection | addAll() 遍历调用 add() | add()、size()、get() |
InputStream | read(byte[], off, len) 循环调用 read() | read()(单字节读) |
Spring JdbcTemplate | 获取连接 → 执行 → 处理结果 → 释放资源 | SQL 与结果映射(用 Callback 传入) |
Servlet HttpServlet.service() | 按 HTTP 方法分发到 doGet/doPost | doGet/doPost |
为什么 Spring 用"模板方法 + Callback"而不是纯继承(这是本题的核心):
❌ 纯继承的模板方法:为了写 3 行逻辑也要建一个类
class MyQuery extends AbstractQuery {
protected void doInQuery(Connection conn) { ... } // 只为了这一小段逻辑就要建类
}
// 结果:类爆炸 + 无法用 Lambda + 复用粒度太粗✅ Spring 的 Callback 改造:把「可变部分」从「子类覆写」变成「参数传入」
模板方法仍然是模板方法:
JdbcTemplate依然固定了整条流程;只是「如何映射结果」这一步,由回调而非子类提供——无需建类,而且能直接写 Lambda。
| 维度 | 纯继承模板方法 | 模板方法 + Callback |
|---|---|---|
| 可变部分提供方式 | 子类覆写抽象方法 | 调用时传 Lambda/匿名类 |
| 类数量 | 每个变体一个子类 | 0 个额外类 |
| 代码位置 | 逻辑分散在子类 | 逻辑就在调用处(可读性更好) |
| 复用粒度 | 类级(粗) | 方法调用级(细) |
| 典型 | HttpServlet、AbstractList | JdbcTemplate、RedisTemplate、RestTemplate、TransactionTemplate |
一句话:「Template 固定流程、Callback 注入变化」——这是 Spring 大量
XxxTemplate的统一设计口径。面试时能说出「为什么用 Callback 代替子类」,比只报"JdbcTemplate 用了模板方法"有价值得多。
7. 状态模式是什么?状态机有哪四个核心概念?怎么选框架?
答:
核心意图:允许对象在内部状态改变时改变它的行为——把每个状态的行为局部化到对应的状态类中,消除 Context 里庞大的条件分支(if (status == PENDING) ... else if (status == PAID) ...)。
状态机(有限状态自动机)四大概念(必背):
| 概念 | 含义 | 订单示例 |
|---|---|---|
| 状态(State) | 系统当前所处的阶段 | 待支付、已支付、已发货、已完成、已取消 |
| 事件(Event) | 触发状态转换的外部动作 | 支付成功、发货、确认收货、取消订单 |
| 动作(Action) | 状态转换时/后执行的操作 | 扣库存、发消息、记录日志 |
| 转换(Transition) | 「某状态下收到某事件 → 转移到某状态」的规则 | (待支付, 支付成功) → 已支付 |
为什么业务代码需要状态机(面试价值点):
❌ 手写 if/else 维护状态流转的问题:
① 分支分散在多处方法里(pay() 里判一次、ship() 里再判一次),无法一眼看全合法流转
② 新增状态要改所有方法,极易漏改 → 出现"已取消的订单还能发货"
③ 无法统一做:状态流转日志、幂等、并发控制(同一订单并发两个事件)
✅ 状态机带来的:
① **流转规则集中定义**(一处声明,全局生效),非法转移直接拒绝
② 状态流转可**统一打点/审计**(谁、何时、从什么状态到什么状态)
③ 便于做**并发控制**(对订单 ID 加锁或 CAS 更新 `where status = ?`)
④ 状态图可**可视化**,与业务方沟通成本低状态机框架选型(面试常问"你用过什么"):
| 框架 | 特点 | 适用 |
|---|---|---|
| 手写枚举 + switch/Map | 最轻量、零依赖、完全可控 | 状态少(≤5)且流转简单时,其实够用(不要过度设计) |
| Spring StateMachine | 功能完整(层级状态、伪状态、持久化、监听器);但偏重、学习成本高,且状态机实例有状态、非线程安全(需注意作用域) | 复杂流程、需要完整状态机语义 |
阿里 Cola-StateMachine | 轻量、无状态(线程安全)、实现成本低、支持 DSL 与外部配置 | 业务状态流转的主流选择 |
| 自研状态机(枚举 + 转移表) | 用 Map<(状态, 事件), 状态> 定义转移表,配合动作回调 | 想"可控 + 可配置"的团队 |
| 工作流引擎(Flowable/Activiti) | 支持人工节点、会签、长时间流程 | 跨多系统、含人工审批的长流程(见《Seata》的 Saga 场景) |
选型建议:「状态少于 5 个、流转简单 → 别上框架(枚举 + 转移表足够);状态多、流转复杂、需要审计与幂等 → Cola-StateMachine 或自研转移表;含人工节点、跨系统长流程 → 工作流引擎。」
三个实战要点(加分):
- 并发控制:同一订单可能同时收到"支付成功"和"取消",必须用
UPDATE ... WHERE status = 'PENDING'(CAS) 或分布式锁保证只有一个转换成功(affectedRows == 1才是真的转换成功); - 幂等:同一事件可能重复到达(MQ 重投),转换前要判断"当前状态是否已处理过该事件";
- 状态与业务字段的分离:状态机只管"状态流转合法性与触发动作",不要把所有业务逻辑都塞进状态机,否则状态机会变成新的"大泥球"。
8. 其他行为型模式速览(命令 / 迭代器 / 中介者 / 备忘录 / 访问者 / 解释器)
答: 这几个属于"知道即可",但被问到要能一句话说清意图 + 一个实例。
| 模式 | 核心意图 | 一句话实例 |
|---|---|---|
| 命令(Command) | 把请求封装成对象,从而支持参数化、排队、记录日志、撤销 | Runnable:把一段任务封装成对象交给 Thread/ExecutorService 执行,任务与执行者解耦;ThreadPoolExecutor 的任务队列;事务的 undo/redo |
| 迭代器(Iterator) | 顺序访问集合元素而不暴露内部表示 | 所有集合的 iterator()(hasNext/next/remove);Fail-Fast 机制(遍历时修改会抛 ConcurrentModificationException) |
| 中介者(Mediator) | 用一个中介对象封装一组对象之间的交互,把网状依赖变成星形 | ExecutorService(线程与任务之间的中介)、MVC 的 Controller(Model 与 View 的中介)、聊天室服务器、消息总线 |
| 备忘录(Memento) | 捕获并保存对象的内部状态,以便之后恢复(不破坏封装) | 编辑器的撤销(Ctrl+Z)、Connection 的事务回滚点(Savepoint)、游戏存档、Serializable 快照 |
| 访问者(Visitor) | 在不修改元素类的前提下,为元素定义新操作 | 编译器 AST 遍历(语法树节点接受不同访问者做语义分析/代码生成)、BeanDefinitionVisitor(Spring 解析 Bean 定义)、报表导出(同一对象模型导出不同格式) |
| 解释器(Interpreter) | 给定语言,定义其文法表示与解释器 | java.util.regex.Pattern、SpEL/OGNL 表达式引擎、SQL 解析器、规则引擎(Drools) |
面试策略:这些模式被问到的概率远低于前面五个。如果时间有限,只需记住"一句话意图 + 一个实例";如果面试官追问某个(如访问者在 Spring 里),能答出「
BeanDefinitionVisitor用来遍历并修改 Bean 定义」就足够。
二、综合设计题(区分度最高)
9. 综合:设计一个支付系统,你会用到哪些模式?
答: 这题的正确答法不是先抛模式,而是先识别变化点与职责——面试一开始就说"我会用策略、工厂、观察者、责任链、代理"会显得在"堆名词"。推荐顺序:先拆职责 → 再点模式 → 最后说取舍。
| 模式 | 在这一系统里解决的具体问题 | 不用它会怎样 |
|---|---|---|
| 责任链 | 把鉴权/限流/幂等/风控等横切逻辑串成可配置的链 | 核心方法里塞满校验代码,且各处顺序不一致 |
| 模板方法 | 各渠道流程骨架相同、细节不同,避免复制粘贴 | 每个渠道复制一份主流程,改一处要改 N 处 |
| 策略 + 工厂 | 渠道可插拔(新增渠道不改核心) | 巨大 if-else,每加渠道都改核心并重新回归 |
| 观察者/事件 | 支付成功后的下游动作解耦、可独立扩展 | 支付核心串行调用所有下游,一处慢/错全受影响 |
| 代理 | 事务/缓存/重试/RPC 等横切关注点 | 业务代码里到处写事务模板与重试逻辑 |
| 状态机 | 状态流转的合法性与审计 | 分散的 if (status == X),出现非法流转 |
| 单例 | 渠道 SDK 客户端、配置、连接池全局共享 | 重复创建客户端(连接/内存浪费) |
高分收尾表达(可直接背):
「我不会为了用设计模式而用设计模式,而是先识别"哪些地方会变、变化的方向是什么":渠道会不停增加 → 策略 + 工厂;支付成功后的下游会不停增加 → 观察者/事件;流程骨架稳定但校验会变 → 责任链 + 模板方法;状态流转必须严谨 → 状态机。如果某个支付方式永远只有一种、也不会有下游扩展,那直接写方法就好,不需要这些抽象。」
10. 综合:一个订单系统里,策略、状态、责任链怎么配合?
答: 这题考察"多个模式协作"的理解,用一张流程图标清各自的边界最清楚。
| 模式 | 回答"它负责什么"的一句话 |
|---|---|
| 责任链 | 这一请求要经过哪些检查、在哪一步被拦下 |
| 状态机 | 订单现在处于什么状态、允许做什么、做完变成什么状态 |
| 策略 | 当前业务应该采用哪一种算法/实现 |
| 观察者 | 状态变化后,有哪些下游需要被通知 |
| 模板方法 + 代理 | 统一流程骨架 + 横切关注点 |
面试加分点:主动说清"边界"——「责任链不该管业务状态判断(那是状态机的事),状态机不该管"用哪个渠道"(那是策略的事)。如果三者混在一起,就会退化成"用模式包装过的 if-else",比不用还难维护。」这句话能立刻显示你是"设计过"而不是"背过"。
11. 综合:如何判断该用哪个模式?(四组判断准则)
答: 这是从"知道模式"到"会用模式"的分水岭,四组准则建议背下来。
① 一个 if/else 该不该改成策略?——不要机械地数分支数
⚠️ 错误答法:「超过 3 个
if就用策略」—— 这是背诵,不是判断。
「看着像策略、其实是别的模式」的三种情况(面试陷阱):
② 装饰器还是继承?
⚠️ 提醒:装饰器会增加对象层级、调试栈很深;且会改变对象的运行时类型(
instanceof判断会失效)——这和第 6 题"JDK 代理导致注入实现类失败"是同一类陷阱。
③ 观察者还是责任链?
要「通知多人」→ 观察者;要「逐级处理一个请求」→ 责任链。
④ 该不该引入设计模式?(元判断,最容易被追问)
三个"不要":
① 只有一个实现、且永远不会有第二个 → **不要抽接口**("为将来可能的扩展"是最大的过度设计)
② 变化点还没出现 → **先用最直白的代码,等第二次/第三次出现同类变化时再重构**
③ 团队不熟悉模式 → **引入的抽象成本可能高于收益**(可读性是团队资源)
一个"要":
④ 当你发现**同一个方法被反复修改**(每次需求都改它)、或**同一个 if-else 在多个地方重复**时
→ 这才是需要抽象的信号
⚠️ 面试时的安全表达:
「**我会先写能工作的简单实现,识别出真实的变化点后再引入模式**;
如果变化点不存在,引入模式反而是负债。」12. 综合:面试问"你项目里用了什么设计模式",怎么答最高分?
答: 这是最容易被问、也最容易答砸的一题。答砸的典型:列一堆模式名("我们用了策略、工厂、观察者、责任链、模板方法、单例……")——面试官立刻会追问细节,然后发现你其实说不清。
高分答法:只讲 2~3 个"有故事"的,每个按「场景 → 痛点 → 方案 → 效果 → 取舍」讲。
示例(可直接套用,把业务名换成你的):
① 【场景 + 痛点】
「我们的**支付渠道**原来是一个大 if-else,每接一个新渠道(支付宝/微信/银联/数字人民币)
都要改这个核心方法,一次改动要回归全部渠道,测试成本很高。」
② 【方案 + 关键实现细节】
「后来重构成**策略模式 + 注册表**:
· 每个渠道实现 `PaymentStrategy`,并声明自己支持的 `channel()`;
· 用一个 `Map<String, PaymentStrategy>` 做注册表(Spring 启动时把同类型的 Bean
全部注入为一个 List,各自的 `channel()` 作为 key 自注册);
· 调用方只写 `registry.get(channel).pay(req)`,**核心逻辑里再没有 if-else**。
· 新增渠道只要加一个 `@Component` 实现类,**不改任何老代码**。」
③ 【效果(尽量量化)】
「接入一个新渠道从原来的『改核心方法 + 全渠道回归』变成**只加一个类 + 加一次单测**,
上线风险显著下降。」
④ 【取舍(这一句最加分)】
「**代价是类变多了、调用链多一跳**,而且我们对不支持的渠道做了**显式抛异常**(而不是静默返回 null),
避免参数错误被掩盖。另外我们用**注册表自注册**而不是再写一个 switch 去选策略,
否则等于把 if-else 挪了个地方。」六个"加分细节"(体现真的做过):
| 细节 | 说明 |
|---|---|
| 说出"避免的坑" | 例如"策略注册表重复 key 会覆盖 → 我们在启动时校验并抛异常,让问题在启动期暴露而不是运行期" |
| 说出"边界与异常" | "参数非法时抛业务异常,而不是返回 null 让调用方判空" |
| 说出"并发/顺序" | 责任链要说清顺序怎么定(@Order)、观察者要说清是否异步、异常是否影响主流程 |
| 说出"取舍" | 任何模式都有代价,主动说出来 = 成熟度 |
| 说出"为什么不用别的模式" | 例如"这里没用状态模式,因为状态只有 2 个且不会转移" |
| 抛出可深挖的钩子 | 结尾带一句"这里还涉及幂等和事务边界的问题"——引导面试官问你会的东西(但必须真会) |
万能收尾模板(背下来):
「设计模式在我这里的价值不是"用了几种",而是"把变化点收拢到一处、把稳定部分保护起来"。
我的判断顺序是:① 先识别变化的方向 → ② 再看变化的频率 → ③ 最后选最小的抽象手段。
如果一段代码不会变,我倾向不抽象——因为抽象本身也是要还债的。」
设计模式系列小结:(一)设计原则与创建型 →(二)结构型与框架源码 →(三)行为型与综合设计。三条主线是「先有原则(SOLID)→ 再有模式(GoF 23)→ 最后是判断力(该不该用)」。真正拉开差距的从来不是"知道多少模式",而是能否在面试里说清"变化点在哪、代价是什么"。
