写好Java代码,这几种设计模式最常用

作者:搬砖的石头2026.11.07 02:33浏览量:0

简介:本文以一次真实的业务迭代为线索,复盘一个“批量发放权益”接口的演进过程,提炼出Java工程中最常用来写出漂亮代码的几种模式:模板方法、策略、工厂注册表以及链式流式API。文章给出可直接落地的代码示例与设计权衡,帮助你把通用逻辑沉淀下来、把变化点隔离出去,写出易读、易扩展、易维护的Java代码。

一、从一次接手他人代码的经历说起

接到一个需求:把前阵子刚实现的赠品卡,接入到既有的权益发放链路里。回到工位按类名一搜,发现已经有一个现成的RPC接口,抽象出了“批量发放权益”的能力,支持对用户批量发放优惠券、返现卡等多种权益。换句话说,不需要从零写一套,在既有结构上迭代即可。

初看有些错愕,但顺着调用链往下读,很快找到了门道。这里存在一个清晰的继承体系:

  • 顶层接口:声明批量发放能力,例如 batchSend(List<...> requests);
  • 中间抽象类:沉淀通用流程与公共操作;
  • 叶子实现类:只负责自己差异化的那部分逻辑。

第一反应是利用多态:最终由子类实现发放动作。但仔细一看,子类里只有 send,并没有 batchSend。点进抽象类才恍然大悟——原来作者把通用的记录操作(发放成功/失败明细的落库)抽取到了抽象类中,而这些记录在“部分失败”和“后续重试”时非常关键。流程骨架在父类、细节在子类,这正是模板方法模式;每种权益各自实现发放动作,是策略/多态的自然运用;而结果对象的链式设计,则体现了流式API(Builder思想)的巧思。下面结合这个场景逐个拆解。

二、模板方法模式:把不变的流程沉淀到抽象类

2.1 定义与场景

模板方法模式指:父类定义算法骨架,把其中某些步骤延迟到子类实现。批量发放就是一个典型的“流程固定、细节多变”的场景:

  1. 参数与幂等校验;
  2. 逐条(或分批)执行发放;
  3. 记录发放明细(成功、失败各一条);
  4. 汇总结果,决定整体成功还是部分失败。

这四步对不同权益都一样,变的只有第2步,于是可以这样组织代码:

  1. public abstract class AbstractRightsSender<T extends RightsRequest> {
  2. // final 防止子类破坏流程
  3. public final BatchResult batchSend(List<T> requests) {
  4. checkParams(requests);
  5. BatchContext ctx = initContext(requests);
  6. for (T req : requests) {
  7. try {
  8. doSend(req); // 变化点:交给子类
  9. recordSuccess(ctx, req); // 通用逻辑:沉淀在父类
  10. } catch (Exception e) {
  11. recordFailure(ctx, req, e); // 失败明细,便于后续重试
  12. }
  13. }
  14. return summarize(ctx); // 成功/部分失败统一收口
  15. }
  16. protected abstract void doSend(T request);
  17. }

2.2 为什么它让代码“漂亮”

  • 复用:像发放明细 record/recordDetail 这类操作写在抽象类里,所有子类免费获得;排查部分失败、执行失败重试都依赖这份数据,而逻辑只写一遍。
  • 稳定:骨架方法加 final,任何人不能随手改动流程,代码评审时也更容易守住底线。
  • 演进:以后要统一加监控埋点、限流或审计日志,只需改父类一处,所有权益类型同时生效。

2.3 实践要点

  • 提供钩子方法:父类可给出 protected boolean needRetry(...) 之类的默认实现,让子类按需覆盖,而不是逼所有子类实现一堆空方法。
  • 警惕层级过深:继承体系两层足矣,超过三层就应考虑改用组合。

三、策略模式:让“变化的部分”各自安家

3.1 定义与场景

策略模式把“同一目的、不同做法”的行为封装成一组可互相替换的实现。优惠券要扣库存、返现卡要绑定账户、赠品卡要走卡密核销——流程相同,动作不同:

  1. public interface RightsSender {
  2. RightsType supportType(); // 声明自己处理哪种权益
  3. SendResult send(RightsRequest req); // 差异化发放
  4. }
  5. public class GiftCardSender extends AbstractRightsSender<GiftCardRequest>
  6. implements RightsSender {
  7. @Override
  8. public RightsType supportType() { return RightsType.GIFT_CARD; }
  9. @Override
  10. protected void doSend(GiftCardRequest req) {
  11. // 赠品卡专属逻辑:校验卡密、绑定用户、发送通知……
  12. }
  13. }

3.2 收益

  • 开闭原则:新增赠品卡只是“增加一个类”,对优惠券、返现卡的代码零侵入,回归范围极小。
  • 可测试:每个实现类可以独立单测,不牵连其他权益。
  • 可读性:一个类只讲一种权益的故事,接手的人按类名就能定位。

3.3 与模板方法的配合

模板方法负责“做一件事的固定流程”,策略负责“每个环节的具体做法”,两者叠加是业务系统里最常见的组合拳:抽象类管骨架,叶子类管动作,互不越界。

四、工厂与注册表:把“选哪个实现”集中管理

策略一多,调用方的 if-else 就会膨胀。更好的做法是利用依赖注入容器自动收集所有实现,建一张注册表:

  1. public class RightsSenderFactory {
  2. private final Map<RightsType, RightsSender> registry;
  3. public RightsSenderFactory(List<RightsSender> senders) {
  4. this.registry = senders.stream()
  5. .collect(Collectors.toMap(RightsSender::supportType, Function.identity()));
  6. }
  7. public RightsSender get(RightsType type) {
  8. return Optional.ofNullable(registry.get(type))
  9. .orElseThrow(() -> new IllegalStateException("不支持的权益类型: " + type));
  10. }
  11. }

新增权益类型时,工厂代码一行不改——注册表靠容器注入自动扩容。这就是“对扩展开放、对修改关闭”在工程里的具体落地方式,也避免了路由逻辑散落在各个调用点。

五、链式结果对象:用流式API表达“下一步能做什么”

读代码时还有个惊喜:结果对象 BatchResult 的设计。它采用了类似许多数据访问组件中“条件构造器”的链式写法,更好玩的是,调用 finished() 和 interrupted() 之后,能继续追加的操作并不相同:

  1. // 全部成功:追加成功后的动作
  2. batchResult.finished()
  3. .message("权益发放完成")
  4. .notifyBizSide();
  5. // 部分失败:先说明原因,再决定重试
  6. batchResult.interrupted()
  7. .reason("部分用户库存不足")
  8. .retry(failedItems);

这种写法本质是流式API(Fluent API)/ Builder思想的进阶用法:finished() 返回的不是 BatchResult 本身,而是一个“成功阶段”的类型;interrupted() 返回“中断阶段”的类型。每种类型只暴露该状态下合法的方法,把“下一步能做什么”编码进了类型系统,编译器会替你拦截非法调用——比如在“全部成功”的分支上强行设置失败原因,直接编译不过。

收益有三点:

  1. 可读性:代码顺着读下来就是业务流程,几乎不需要注释;
  2. 类型安全:非法状态在编译期暴露,而不是等到线上;
  3. 易扩展:新增一种终态(比如“已降级”),加一个阶段类型即可,不影响既有调用。

需要注意的是,链式API的每个方法都应语义明确、避免歧义性副作用,并配套清晰文档,否则容易变成“链式炫技”,反而增加理解成本。

六、其他高频模式速览

除了上面四种,以下模式在业务代码中出现频率也很高:

  • 责任链模式:请求进来先过参数校验、幂等检查、风控过滤,每个节点只关心自己那一环,节点可插拔、顺序可编排;
  • 观察者/事件机制:发放完成后要发通知、加积分、写统计?把“发放成功”发布成事件,下游各自订阅,主流程不必认识任何下游;
  • 装饰器模式:不改原类的前提下叠加能力,比如给发送器包一层限流、一层加密;
  • 建造者模式:参数多、可选配置多的对象(如重试策略、客户端配置),用建造者让构造过程分步且可读;
  • 代理模式:日志、事务、监控等横切能力交给动态代理统一处理,业务代码保持纯净。

七、模式是手段,不是目的:四条实践建议

  1. 先有变化,再上模式。相似逻辑第二次出现时再抽象也不迟,不要一上来就为了模式而模式,过度设计比没有设计更难维护;
  2. 抽象要沉得对地方。判断标准是“这个逻辑在可预见的变化方向上是否稳定”:发放明细记录之所以下沉到抽象类,是因为无论哪种权益,部分失败与重试都依赖它;
  3. 优先组合,慎用继承。继承层次一深,理解成本指数上涨;能用策略加组合表达的变化,别用四层继承硬扛;
  4. 用测试守住边界。抽象类里的流程骨架、注册表的路由逻辑、链式API的状态迁移,都应有单测覆盖,重构时才有底气。

八、结语

回到开头那次迭代:读懂既有设计只花了十来分钟,新增赠品卡几乎没有改动任何存量代码。所谓“漂亮的代码”,不是炫技的模式堆砌,而是让下一个接手的人——很可能是几个月后的自己——能快速读懂、放心扩展。模板方法沉淀流程,策略隔离变化,工厂集中路由,流式API表达状态迁移,再辅以责任链、观察者等模式处理横切与解耦,这套组合足以覆盖日常业务开发的绝大多数场景。这也是包括百度智能云在内许多技术团队在代码评审中反复强调的取向:可读性与可扩展性,永远优先于聪明的技巧。