本文总结工作中遇到过的设计模式。
策略模式 + 工厂模式(组合使用)
这两个模式单独讲都没什么意思,在工作中几乎永远成对出现。
验证码除了短信,还要支持邮件验证码和语音验证码。最原始的写法是一堆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 { 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) { if (userId <= 0) { return Response.fail("userId 非法"); } 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) { List<T> deduped = deduplicate(targets); List<T> filtered = getBlacklistFilter().filter(deduped); List<T> whitelisted = getWhitelistFilter().filter(filtered); if (whitelisted.isEmpty()) return true; 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 { 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); 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) { 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)); } }
@Component class RiskSyncListener { @Subscribe @AllowConcurrentEvents public void onRealNameCompleted(RealNameCompletedEvent event) { riskClient.syncUserRealName(event.getUserId(), event.getIdCard()); } }
@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 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; 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()前自己检查。
总结
| 场景 |
设计模式 |
| “我有多种实现,运行时按条件选” |
策略模式 + 工厂模式 |
| “骨架固定,某些步骤因人而异” |
模板方法模式 |
| “多个处理步骤,顺序可调、随时可增删” |
责任链模式 |
| “想给一个类加功能,但不想修改它” |
装饰器模式 |
| “发生了某件事,多方需要响应,但互不依赖” |
观察者/事件模式 |
| “复杂对象的构造参数太多、太乱” |
建造者模式 |
文章作者:米兰
原始链接:https://blog.milanchen.site/posts/design-patterns.html
版权声明:转载请声明出处