本文总结工作中遇到过的设计模式。

策略模式 + 工厂模式(组合使用)

这两个模式单独讲都没什么意思,在工作中几乎永远成对出现。
验证码除了短信,还要支持邮件验证码和语音验证码。最原始的写法是一堆if-else:

1
2
3
4
5
6
7
8
// 反例:每次新增渠道都要改这里
if (type == EMAIL) {
// 发邮件逻辑
} else if (type == VOICE) {
// 发语音逻辑
} else if (type == SMS) {
// 发短信逻辑
}

每次新增一个渠道,就得改这个方法,违反开闭原则,而且测试起来很头疼。

策略模式的解法

先定义统一接口:

1
2
3
4
5
6
7
8
9
10
interface VerCodeService {
// 发送验证码
boolean send(String target, String code, String scene);

// 校验验证码
boolean verify(String target, String code, String scene);

// 声明支持的验证码类型(支持多个)
VerCodeTypeEnum[] supportTypes();
}

每种渠道各自实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Service
class EmailVerCodeServiceImpl implements VerCodeService {
@Override
public boolean send(String email, String code, String scene) {
// 发邮件的具体逻辑
emailClient.send(email, buildContent(code, scene));
return true;
}

@Override
public VerCodeTypeEnum[] supportTypes() {
return new VerCodeTypeEnum[]{ EMAIL };
}
// ...
}

@Service
class VoiceVerCodeServiceImpl implements VerCodeService {
@Override
public boolean send(String phone, String code, String scene) {
// 调用语音外呼平台
voiceClient.call(phone, code);
return true;
}

@Override
public VerCodeTypeEnum[] supportTypes() {
return new VerCodeTypeEnum[]{ VOICE };
}
// ...
}

工厂模式负责”选哪个”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
class VerCodeServiceFactory {
// 启动时自动收集所有实现,注册到Map
private final Map<VerCodeTypeEnum, VerCodeService> registry = new HashMap<>();

@Autowired
public VerCodeServiceFactory(List<VerCodeService> services) {
for (VerCodeService svc : services) {
for (VerCodeTypeEnum type : svc.supportTypes()) {
registry.put(type, svc);
}
}
}

public VerCodeService getService(VerCodeTypeEnum type) {
VerCodeService svc = registry.get(type);
if (svc == null) {
throw new UnsupportedOperationException("不支持的验证码类型: " + type);
}
return svc;
}
}

业务调用时只面向工厂:

1
2
3
// 调用方完全不知道具体是哪个实现
VerCodeService svc = factory.getService(request.getType());
svc.send(request.getTarget(), generateCode(), request.getScene());

工厂在Spring启动时扫描所有实现,通过接口的supportTypes()自动注册。新增一种验证码渠道,只需要新加一个实现类,工厂和业务代码都不用改,这才是真正的”对扩展开放,对修改关闭”。

类似的场景:不同租户的Kafka topic消费策略、不同业务线的消息发送渠道(短信/Push/公众号/小程序/站内信)。


模板方法模式

用户注销账号前,需要各业务线做预检查:理财线检查是否有在投产品,贷款线检查是否有未结清借款,教育线检查是否有未完成订单…
这些检查的骨架完全相同:校验参数 → 调用业务方检查 → 返回结果。但每个业务线的检查逻辑天差地别,不能统一实现。

模板方法的解法

父类定义骨架(final方法),抽象方法留给子类实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
abstract class PreCheckCancelAbstract {

// 模板方法,骨架固定,子类不能覆盖
public final Response<String> execute(long userId) {
// 1. 通用参数校验
if (userId <= 0) {
return Response.fail("userId 非法");
}
// 2. 调用子类实现的具体检查
return cancelPreCheck(userId);
}

// 子类必须实现:业务线自己的注销前检查逻辑
protected abstract Response<String> cancelPreCheck(long userId);
}

各业务线各自实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Service
class FundPreCheckService extends PreCheckCancelAbstract {
@Override
protected Response<String> cancelPreCheck(long userId) {
boolean hasPosition = fundService.hasActivePosition(userId);
if (hasPosition) {
return Response.fail("您有基金持仓,请先赎回再注销");
}
return Response.ok();
}
}

@Service
class LoanPreCheckService extends PreCheckCancelAbstract {
@Override
protected Response<String> cancelPreCheck(long userId) {
boolean hasActiveLoan = loanService.hasActiveLoan(userId);
if (hasActiveLoan) {
return Response.fail("您有未结清借款,请先还款再注销");
}
return Response.ok();
}
}

这个模式解决了”流程固定、步骤可变”的问题。父类execute()final是关键——不允许子类绕过通用校验,只允许子类定制核心业务逻辑。

另一个典型场景是消息发送处理:无论是短信、Push还是站内信,都要经历去重检测 → 黑名单过滤 → 白名单过滤 → 真实发送这四步。前三步框架固定,只有过滤规则和最终发送实现因渠道而异:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
abstract class AbstractSendHandler<T> {

public final boolean handle(List<T> targets) {
// 1. 去重
List<T> deduped = deduplicate(targets);
// 2. 黑名单过滤(子类提供具体的黑名单逻辑)
List<T> filtered = getBlacklistFilter().filter(deduped);
// 3. 白名单过滤(子类提供具体的白名单逻辑)
List<T> whitelisted = getWhitelistFilter().filter(filtered);
if (whitelisted.isEmpty()) return true;
// 4. 真实发送(子类实现)
return doHandle(whitelisted);
}

protected abstract boolean doHandle(List<T> targets);
protected abstract BlacklistFilter<T> getBlacklistFilter();
protected abstract WhitelistFilter<T> getWhitelistFilter();
}

责任链模式

每次调用Redis时,需要做三件独立的事情:记录调用统计、执行路由选主从、异常时上报告警。这三件事情的先后顺序有要求,但逻辑之间没有强依赖,理想状态是分开维护、灵活组合。

责任链的解法

每个关注点是一个独立的处理器(Handler),处理器之间形成链:

1
请求 → StatisticsHandler → RouteHandler → AlertHandler → 结束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 处理器接口
interface Handler {
// 返回 true 表示终止链,false 表示继续传递
boolean execute(Context ctx);
}

class StatisticsHandler implements Handler {
@Override
public boolean execute(Context ctx) {
ctx.setStartTime(System.currentTimeMillis());
// 记录开始,不拦截,继续传递
return false;
}

// 链执行完后的后处理(记录耗时、更新统计)
public void postProcess(Context ctx) {
long elapsed = System.currentTimeMillis() - ctx.getStartTime();
metricsCollector.record(ctx.getCommand(), elapsed);
}
}

class RouteHandler implements Handler {
@Override
public boolean execute(Context ctx) {
// 根据读写类型选择主库或从库
RedisNode node = router.select(ctx.isWriteOperation());
Object result = node.execute(ctx.getCommand(), ctx.getArgs());
ctx.setResult(result);
// 执行完毕,终止链
return true;
}
}

class AlertHandler implements Handler {
@Override
public boolean execute(Context ctx) {
if (ctx.hasException()) {
alertService.notify("Redis异常: " + ctx.getException().getMessage());
}
return false;
}
}

链的组装:

1
2
3
4
5
6
7
Chain redisProxyChain = new Chain();
redisProxyChain.addHandler(new StatisticsHandler());
redisProxyChain.addHandler(new RouteHandler());
redisProxyChain.addHandler(new AlertHandler());

// 执行
redisProxyChain.execute(context);

责任链最大的好处是各Handler之间完全解耦,可以任意调整顺序、增删Handler。工作中最常见的责任链是Servlet Filter链,不过自建的责任链更灵活,可以控制是否”短路”(拦截后不再往下传)。


装饰器模式

Spring的HttpServletRequest的Body InputStream只能读一次,读完就没了。日志过滤器想读Body、签名验证过滤器也想读Body,结果后面的过滤器读到的是空流。

装饰器的解法

不改原始类,包一个装饰层,把Body缓存起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class RepeatableRequestWrapper extends HttpServletRequestWrapper {

private final byte[] cachedBody;

public RepeatableRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
// 构造时一次性把 Body 读完缓存到内存
this.cachedBody = IOUtils.toByteArray(request.getInputStream());
}

@Override
public ServletInputStream getInputStream() {
// 每次都从缓存重新创建流,可以反复读
return new CachedServletInputStream(this.cachedBody);
}

@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}

public String getBodyAsString() {
return new String(cachedBody, StandardCharsets.UTF_8);
}
}

在过滤器里包装一下,后续所有过滤器都用包装后的对象:

1
2
3
4
5
6
7
8
class BodyCacheFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) {
// 用装饰器包住原始 request,传给后续链
RepeatableRequestWrapper wrappedRequest = new RepeatableRequestWrapper((HttpServletRequest) req);
chain.doFilter(wrappedRequest, resp);
}
}

装饰器模式的核心是”增强而不修改”——继承自同一个类/接口,构造函数接收被装饰的原始对象,在方法里做增强后委托给原始对象(或者像这里一样直接替换行为)。Java标准库里的BufferedInputStream包装InputStream,就是最经典的装饰器。


观察者模式(事件驱动)

用户完成实名认证后,需要触发一系列后续动作:同步到风控系统、发欢迎短信、更新会员等级…这些动作和”完成实名”本身关系不大,如果全堆在一个方法里,会变成一个几百行的大方法,改任何一块都可能出事。

进程内:Guava EventBus

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 定义事件类(只是一个数据容器)
class RealNameCompletedEvent {
private final long userId;
private final String idCard;
// ...
}

// 发布方:完成实名后发布事件,不关心谁来消费
class RealNameService {
@Autowired
private EventBus eventBus;

public void complete(long userId, String idCard) {
// 核心逻辑:保存实名信息
saveRealNameInfo(userId, idCard);

// 发事件,解耦后续动作
eventBus.post(new RealNameCompletedEvent(userId, idCard));
}
}

// 监听方 A:风控系统
@Component
class RiskSyncListener {
@Subscribe
@AllowConcurrentEvents // 允许并发处理
public void onRealNameCompleted(RealNameCompletedEvent event) {
riskClient.syncUserRealName(event.getUserId(), event.getIdCard());
}
}

// 监听方 B:短信通知
@Component
class WelcomeSmsListener {
@Subscribe
public void onRealNameCompleted(RealNameCompletedEvent event) {
smsService.sendWelcome(event.getUserId());
}
}

跨服务:Kafka

进程内EventBus解决了单服务内的解耦,但如果下游是另一个微服务,就需要Kafka:

1
2
3
4
5
6
7
8
9
账号服务(生产者)             Token服务(消费者 A)     Session服务(消费者 B)

用户登录成功

发布 Kafka 消息: user-login-topic
↓ ──────────────────────────────────→ TokenKafkaListener.onMessage()
→ 刷新Token有效期
↓ ──────────────────────────────────→ TraceKafkaListener.onMessage()
→ 记录登录轨迹

EventBus适合同进程内的松耦合,发布和订阅是同步的(可以用异步线程池配置为异步)。Kafka适合跨服务的事件通知,天然异步,但引入了消息积压和消费失败的复杂性。两者都解决同一个本质问题:让事件的发布方和处理方互不知晓彼此的存在。


建造者模式

构造一个复杂对象时,参数有十几个,有些必填有些选填,顺序很容易搞错,还不好阅读:

1
2
// 反例:new 一个有七八个参数的对象,哪个是哪个?
new SmsRecord(userId, mobile, templateCode, content, null, 0, System.currentTimeMillis());

建造者的解法

Lombok@Builder是工作中最省力的做法:

1
2
3
4
5
6
7
8
9
10
11
12
@Builder
@Data
class SmsRecord {
private long id;
private long userId;
private String mobile;
private String templateCode;
private String content;
private int status; // 0:待发送 1:已发送 2:失败
private String gatewayMsgId;
private long createTime;
}

构造时链式调用,语义清晰:

1
2
3
4
5
6
7
8
SmsRecord record = SmsRecord.builder()
.userId(userId)
.mobile(mobile)
.templateCode("VERIFY_CODE")
.content("您的验证码是 " + code)
.status(0)
.createTime(System.currentTimeMillis())
.build();

更新时也可以用 Builder,只设置需要变更的字段:

1
2
3
4
5
6
SmsRecord update = SmsRecord.builder()
.id(originalId)
.status(1)
.gatewayMsgId(gatewayResponse.getMsgId())
.build();
smsDao.updateById(update);

Builder适合字段多、可选字段多的对象。如果一个类只有两三个字段,Builder就是过度设计。另外,Lombok生成的Builder默认不校验必填字段,业务代码需要在build()前自己检查。


总结

场景 设计模式
“我有多种实现,运行时按条件选” 策略模式 + 工厂模式
“骨架固定,某些步骤因人而异” 模板方法模式
“多个处理步骤,顺序可调、随时可增删” 责任链模式
“想给一个类加功能,但不想修改它” 装饰器模式
“发生了某件事,多方需要响应,但互不依赖” 观察者/事件模式
“复杂对象的构造参数太多、太乱” 建造者模式