简介:本文以一次真实的业务迭代为线索,复盘一个“批量发放权益”接口的演进过程,提炼出Java工程中最常用来写出漂亮代码的几种模式:模板方法、策略、工厂注册表以及链式流式API。文章给出可直接落地的代码示例与设计权衡,帮助你把通用逻辑沉淀下来、把变化点隔离出去,写出易读、易扩展、易维护的Java代码。
接到一个需求:把前阵子刚实现的赠品卡,接入到既有的权益发放链路里。回到工位按类名一搜,发现已经有一个现成的RPC接口,抽象出了“批量发放权益”的能力,支持对用户批量发放优惠券、返现卡等多种权益。换句话说,不需要从零写一套,在既有结构上迭代即可。
初看有些错愕,但顺着调用链往下读,很快找到了门道。这里存在一个清晰的继承体系:
batchSend(List<...> requests);第一反应是利用多态:最终由子类实现发放动作。但仔细一看,子类里只有 send,并没有 batchSend。点进抽象类才恍然大悟——原来作者把通用的记录操作(发放成功/失败明细的落库)抽取到了抽象类中,而这些记录在“部分失败”和“后续重试”时非常关键。流程骨架在父类、细节在子类,这正是模板方法模式;每种权益各自实现发放动作,是策略/多态的自然运用;而结果对象的链式设计,则体现了流式API(Builder思想)的巧思。下面结合这个场景逐个拆解。
模板方法模式指:父类定义算法骨架,把其中某些步骤延迟到子类实现。批量发放就是一个典型的“流程固定、细节多变”的场景:
这四步对不同权益都一样,变的只有第2步,于是可以这样组织代码:
public abstract class AbstractRightsSender<T extends RightsRequest> {// final 防止子类破坏流程public final BatchResult batchSend(List<T> requests) {checkParams(requests);BatchContext ctx = initContext(requests);for (T req : requests) {try {doSend(req); // 变化点:交给子类recordSuccess(ctx, req); // 通用逻辑:沉淀在父类} catch (Exception e) {recordFailure(ctx, req, e); // 失败明细,便于后续重试}}return summarize(ctx); // 成功/部分失败统一收口}protected abstract void doSend(T request);}
final,任何人不能随手改动流程,代码评审时也更容易守住底线。protected boolean needRetry(...) 之类的默认实现,让子类按需覆盖,而不是逼所有子类实现一堆空方法。策略模式把“同一目的、不同做法”的行为封装成一组可互相替换的实现。优惠券要扣库存、返现卡要绑定账户、赠品卡要走卡密核销——流程相同,动作不同:
public interface RightsSender {RightsType supportType(); // 声明自己处理哪种权益SendResult send(RightsRequest req); // 差异化发放}public class GiftCardSender extends AbstractRightsSender<GiftCardRequest>implements RightsSender {@Overridepublic RightsType supportType() { return RightsType.GIFT_CARD; }@Overrideprotected void doSend(GiftCardRequest req) {// 赠品卡专属逻辑:校验卡密、绑定用户、发送通知……}}
模板方法负责“做一件事的固定流程”,策略负责“每个环节的具体做法”,两者叠加是业务系统里最常见的组合拳:抽象类管骨架,叶子类管动作,互不越界。
策略一多,调用方的 if-else 就会膨胀。更好的做法是利用依赖注入容器自动收集所有实现,建一张注册表:
public class RightsSenderFactory {private final Map<RightsType, RightsSender> registry;public RightsSenderFactory(List<RightsSender> senders) {this.registry = senders.stream().collect(Collectors.toMap(RightsSender::supportType, Function.identity()));}public RightsSender get(RightsType type) {return Optional.ofNullable(registry.get(type)).orElseThrow(() -> new IllegalStateException("不支持的权益类型: " + type));}}
新增权益类型时,工厂代码一行不改——注册表靠容器注入自动扩容。这就是“对扩展开放、对修改关闭”在工程里的具体落地方式,也避免了路由逻辑散落在各个调用点。
读代码时还有个惊喜:结果对象 BatchResult 的设计。它采用了类似许多数据访问组件中“条件构造器”的链式写法,更好玩的是,调用 finished() 和 interrupted() 之后,能继续追加的操作并不相同:
// 全部成功:追加成功后的动作batchResult.finished().message("权益发放完成").notifyBizSide();// 部分失败:先说明原因,再决定重试batchResult.interrupted().reason("部分用户库存不足").retry(failedItems);
这种写法本质是流式API(Fluent API)/ Builder思想的进阶用法:finished() 返回的不是 BatchResult 本身,而是一个“成功阶段”的类型;interrupted() 返回“中断阶段”的类型。每种类型只暴露该状态下合法的方法,把“下一步能做什么”编码进了类型系统,编译器会替你拦截非法调用——比如在“全部成功”的分支上强行设置失败原因,直接编译不过。
收益有三点:
需要注意的是,链式API的每个方法都应语义明确、避免歧义性副作用,并配套清晰文档,否则容易变成“链式炫技”,反而增加理解成本。
除了上面四种,以下模式在业务代码中出现频率也很高:
回到开头那次迭代:读懂既有设计只花了十来分钟,新增赠品卡几乎没有改动任何存量代码。所谓“漂亮的代码”,不是炫技的模式堆砌,而是让下一个接手的人——很可能是几个月后的自己——能快速读懂、放心扩展。模板方法沉淀流程,策略隔离变化,工厂集中路由,流式API表达状态迁移,再辅以责任链、观察者等模式处理横切与解耦,这套组合足以覆盖日常业务开发的绝大多数场景。这也是包括百度智能云在内许多技术团队在代码评审中反复强调的取向:可读性与可扩展性,永远优先于聪明的技巧。