设计模式 商业场景训练营
这页把设计模式放到真实商业项目里学习。目标不是背 UML,而是能判断什么时候该用策略、工厂、模板、责任链、观察者、代理、适配器;也能判断什么时候不要用,避免过度设计。
训练目标
学完这页,你要能做到:
- 解释设计模式的核心是隔离变化点,不是套模板。
- 用策略模式改造支付方式、折扣规则、医院接口差异。
- 用工厂模式统一创建不同业务处理器。
- 用模板方法固定导入流程,开放校验、转换、落库步骤。
- 用责任链组织订单校验、接口安全校验、采集数据校验。
- 用观察者模式做领域事件通知,但知道它和 MQ 的边界。
- 用代理模式理解 Spring AOP、事务、权限和日志增强。
- 用适配器模式屏蔽第三方医院接口差异。
商业场景总览
mermaid
flowchart TD
A["商业系统变化点"] --> B["支付和折扣规则"]
A --> C["采集数据导入流程"]
A --> D["接口安全校验链"]
A --> E["第三方医院接口差异"]
A --> F["下单后通知多方"]
B --> G["策略 + 工厂"]
C --> H["模板方法 + 责任链"]
D --> I["责任链"]
E --> J["适配器 + 策略"]
F --> K["观察者 / MQ"]设计模式的判断顺序:
- 变化点是否真实存在。
- 变化是否会持续发生。
- 抽象后是否减少修改老代码。
- 新人是否能理解。
- 测试是否更容易。
场景一:支付方式选择
如果这样写:
java
public PayResult pay(PayCommand command) {
if ("ALI".equals(command.channel())) {
return aliPay(command);
}
if ("WECHAT".equals(command.channel())) {
return wechatPay(command);
}
if ("BANK".equals(command.channel())) {
return bankPay(command);
}
throw new IllegalArgumentException("不支持的支付方式");
}问题:
- 新增支付方式要改老方法。
- 每个分支参数校验、签名、调用、异常处理混在一起。
- 单元测试困难。
- 分支越来越多后主流程变得臃肿。
策略接口
java
public interface PayStrategy {
String channel();
PayResult pay(PayCommand command);
}支付策略
java
@Component
public class AliPayStrategy implements PayStrategy {
@Override
public String channel() {
return "ALI";
}
@Override
public PayResult pay(PayCommand command) {
// 参数校验、签名、调用支付宝、解析响应
return PayResult.success(command.orderNo());
}
}策略工厂
java
@Component
public class PayStrategyFactory {
private final Map<String, PayStrategy> strategyMap;
public PayStrategyFactory(List<PayStrategy> strategies) {
this.strategyMap = strategies.stream()
.collect(Collectors.toMap(PayStrategy::channel, Function.identity()));
}
public PayStrategy get(String channel) {
PayStrategy strategy = strategyMap.get(channel);
if (strategy == null) {
throw new IllegalArgumentException("不支持的支付方式: " + channel);
}
return strategy;
}
}业务使用
java
public PayResult pay(PayCommand command) {
PayStrategy strategy = payStrategyFactory.get(command.channel());
return strategy.pay(command);
}这不是为了“高级”,而是因为支付方式确实会变化。新增一种支付方式时,只新增一个策略类,主流程不改。
场景二:采集数据导入流程
采集导入通常流程固定:
- 读取原始数据。
- 校验格式。
- 转换字段。
- 去重。
- 批量入库。
- 发送资产变更事件。
不同医院、不同数据类型只是在某些步骤不同。适合模板方法。
java
public abstract class AbstractCollectImportTemplate<T> {
public final ImportResult importData(ImportContext context) {
List<String> rawRows = read(context);
List<T> records = parse(rawRows);
validate(records);
List<T> cleaned = clean(records);
saveBatch(cleaned);
afterSaved(cleaned);
return ImportResult.success(cleaned.size());
}
protected abstract List<String> read(ImportContext context);
protected abstract List<T> parse(List<String> rawRows);
protected void validate(List<T> records) {
}
protected List<T> clean(List<T> records) {
return records;
}
protected abstract void saveBatch(List<T> records);
protected void afterSaved(List<T> records) {
}
}好处:
- 主流程固定,不会每个导入类都写一遍。
- 变化步骤留给子类实现。
- 统一加日志、耗时、异常处理更容易。
- 不同数据源可以复用同一流程骨架。
代价:
- 父类流程变动会影响所有子类。
- 继承层次过深会难理解。
- 如果流程并不稳定,不适合过早抽模板。
场景三:接口安全校验链
开放接口校验通常有顺序:
mermaid
flowchart TD
A["请求进入"] --> B["appId 校验"]
B --> C["timestamp 校验"]
C --> D["nonce 防重放"]
D --> E["bodyHash 校验"]
E --> F["signature 验签"]
F --> G["业务权限校验"]适合责任链。
java
public interface OpenApiCheckHandler {
void check(OpenApiRequest request);
}java
@Component
public class TimestampCheckHandler implements OpenApiCheckHandler {
@Override
public void check(OpenApiRequest request) {
long diff = Math.abs(System.currentTimeMillis() - request.timestamp());
if (diff > Duration.ofMinutes(5).toMillis()) {
throw new SecurityException("请求已过期");
}
}
}java
@Service
public class OpenApiCheckChain {
private final List<OpenApiCheckHandler> handlers;
public OpenApiCheckChain(List<OpenApiCheckHandler> handlers) {
this.handlers = handlers;
}
public void check(OpenApiRequest request) {
for (OpenApiCheckHandler handler : handlers) {
handler.check(request);
}
}
}为什么责任链合适:
- 每个校验点职责单一。
- 新增校验不改主流程。
- 可以控制顺序。
- 可以单独测试每个 Handler。
不适合的情况:只有一两个固定校验,直接写清楚就行。
场景四:第三方医院接口适配
不同医院接口协议不同:
| 医院 | 入参 | 出参 | 鉴权 |
|---|---|---|---|
| A 医院 | JSON | JSON | appKey 签名 |
| B 医院 | XML | XML | token |
| C 医院 | 文件 | CSV | SFTP |
业务系统希望统一调用:
java
public interface HospitalAdapter {
String hospitalCode();
List<PatientRecord> fetchPatients(FetchCommand command);
}java
@Component
public class HospitalAAdapter implements HospitalAdapter {
@Override
public String hospitalCode() {
return "HOSPITAL_A";
}
@Override
public List<PatientRecord> fetchPatients(FetchCommand command) {
// 把统一 FetchCommand 转成 A 医院 JSON 请求
// 调用 A 医院接口
// 把 A 医院响应转换成统一 PatientRecord
return List.of();
}
}适配器的作用是屏蔽外部差异,让内部模型保持稳定。
如果不用适配器,医院差异会散落在业务代码里,后续新增医院会到处改。
场景五:下单成功后的通知
下单成功后可能要:
- 发积分。
- 发优惠券。
- 发站内信。
- 同步 ES。
- 记录审计。
如果都写在下单方法里,主流程会很长。可以用观察者或领域事件。
java
public record OrderCreatedEvent(String orderNo, Long userId) {
}java
@Component
public class OrderAuditListener {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
// 记录审计日志
}
}注意边界:
| 方式 | 适合 |
|---|---|
| Spring 本地事件 | 同进程、轻量扩展、失败影响可控 |
| MQ 事件 | 跨服务、异步最终一致、可重试 |
不要用本地观察者替代分布式消息。订单服务和积分服务是不同服务时,应使用 MQ 或本地消息表。
场景六:代理模式理解 Spring 事务
Spring 事务常见基于代理:
mermaid
flowchart TD
A["调用 Service 方法"] --> B["事务代理"]
B --> C["开启事务"]
C --> D["执行目标方法"]
D --> E{"是否异常"}
E -- "否" --> F["提交事务"]
E -- "是" --> G["回滚事务"]为什么自调用会失效?因为 this.method() 没经过代理。
java
public void outer() {
this.inner(); // 没经过代理,inner 上的 @Transactional 可能不生效
}
@Transactional
public void inner() {
}这就是设计模式和框架原理的连接点:理解代理模式,才能理解 Spring AOP、事务、权限、日志增强为什么这样工作。
模式选择表
| 问题 | 推荐模式 | 不用会怎样 | 用过头会怎样 |
|---|---|---|---|
| 多种业务规则切换 | 策略 | if/else 膨胀 | 一两个规则也拆很多类 |
| 创建对象有选择逻辑 | 工厂 | new 到处散落 | 简单对象也绕工厂 |
| 固定流程部分步骤变化 | 模板方法 | 流程复制粘贴 | 继承层次太深 |
| 多个校验顺序执行 | 责任链 | 主方法臃肿 | 链路分散难排查 |
| 接口不兼容 | 适配器 | 外部差异污染内部模型 | 过度包装 |
| 一件事后通知多方 | 观察者 | 主流程耦合很多后置逻辑 | 事件太多难追踪 |
| 横切增强 | 代理 | 日志、事务、权限重复写 | 代理链太复杂 |
面试标准回答
设计模式怎么理解
text
设计模式不是为了套类图,而是为了隔离稳定变化点。比如多种算法或业务规则切换用策略,创建实现类变化用工厂,复杂对象构建用建造者,固定流程开放部分步骤用模板方法,多步校验用责任链,外部接口不兼容用适配器,横切增强用代理。使用时要避免过度设计,只有变化真实存在、抽象能减少修改面时才引入。策略模式和工厂模式怎么配合
text
策略模式负责把不同算法或业务规则封装成独立实现,工厂模式负责根据类型找到对应策略。比如支付场景中,支付宝、微信、银行卡分别是不同 PayStrategy,PayStrategyFactory 根据 channel 返回对应策略,新增支付方式时只新增实现类,不改主支付流程。责任链适合什么
text
责任链适合多个处理步骤按顺序执行,并且每个步骤职责相对独立的场景,例如开放接口验签链、订单创建校验链、采集数据校验链。它能让每个 Handler 单独测试和扩展,但如果链路太长、顺序不清晰,也会增加排查成本。