惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

B
Blog
The Cloudflare Blog
J
Java Code Geeks
Apple Machine Learning Research
Apple Machine Learning Research
T
Tailwind CSS Blog
L
LangChain Blog
Recent Announcements
Recent Announcements
Hugging Face - Blog
Hugging Face - Blog
Microsoft Security Blog
Microsoft Security Blog
F
Fortinet All Blogs
Microsoft Azure Blog
Microsoft Azure Blog
V
V2EX
I
InfoQ
博客园 - 司徒正美
T
The Blog of Author Tim Ferriss
G
Google Developers Blog
云风的 BLOG
云风的 BLOG
aimingoo的专栏
aimingoo的专栏
小众软件
小众软件
H
Help Net Security
博客园 - 三生石上(FineUI控件)
S
SegmentFault 最新的问题
B
Blog RSS Feed
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知

博客园 - buguge

交易成功还要记 3 笔账?我们直接砍到 1 笔—— 网络红包记账的一次自顶向下业务层重构 分享一次针对hardcode的小重构:不要把 package 名写死在代码里 聚合系统设计:如何为银行代付通道抽象出公共的代付接口能力 如何自定义 MyBatis枚举处理器(EnumTypeHandler):分享从 “同包同名 shadow 覆盖” 到 “官方扩展点” 的重构经历 这儿也改状态,那儿也改状态,连AI都理不清楚了… 不妨试试这个状态机good-practice 业务解耦的经典实践:从订单表解耦开票业务说起(以滴滴网约车开票为例) 从 `int` 到 `Duration`:一个缓存 API 的三次演进教会我的事 最差实践(bad-practice):开发者在方法里直接实例化线程池对象,然后...(应用gg了) 【HttpClient最差实践(bad-practice)】开发者在 http 工具方法中直接实例化 HttpClient,然后…… Crypto、Cipher与Password:Java加密开发的三个核心概念 知识VS技能:如何优雅判空? 20260604SR超时问题排查 推敲见文章:从 `try..catch` 看异常日志打印的正确姿势 #解决问题要彻底# 慢SQL治理完成后,如何防止同类问题“死灰复燃”? 从合同甲方是荒谬的“JD”谈起:软件开发不应遗忘的“常识” 别留小尾巴/尽快剪掉小尾巴:从一次“ABA”字段重命名,谈谈“解决问题要彻底” 常见的OOM错误 ( OutOfMemoryError全类型详解) 开发者暴露了一个无需授权访问的裸接口,我问:如果有人暴力请求怎么办? 【SQL性能优化篇】有了!治理慢SQL“WHERE create_time ORDER BY id”的良药---规避“Using filesort”性能杀手 高效查询商户日终余额:一个SQL的优化实践 Hutool 的 `TimedCache` 到期会自动清理吗? ——————hutool cache的"惰性清理"和"定期清理" Fastjson枚举反序列化:当字符串不是枚举常量名时,会发生什么? fastjson-EnumDeserializer类及源码分析 随笔20260309:我们都是围城里的人 `UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only` 异常解析 认识2个单词:goal/target —————— 为什么Maven是 "goal" 而不是 "target"? 聚合系统设计:策略模式(Strategy Pattern)在银行通道对接场景中的应用 注意!字段数据类型不匹配,这个sql会很慢 还在用ArrayList?用HashSet吧!--性能对比 分页查询还在用create_time去做降序?
这样构建对象,太帅了!—— 阶梯式Builder模式与代码整洁之道
buguge · 2026-02-26 · via 博客园 - buguge

在Java的世界里,优雅地构建一个复杂对象,就像精心冲泡一杯手冲咖啡——每一步都恰到好处,最终呈现完美的风味。

一、传统对象构建的痛点

想象一下,你需要构建一个记账请求对象,它有8个字段,其中一些是必填的,一些有依赖关系,一些有业务规则约束。传统的做法会让你面临什么困境?

1. 全参构造函数的灾难

// 灾难般的调用方式
new AccountingVO(accountNo, transOrderNo, transType, amount, 
                 whatBalanceDoes, accountingType, unfreezeAccountingType, 
                 memo, operator);
  • 🔢 参数顺序容易出错:8个参数,你能记住正确顺序吗?
  • 可读性极差:调用时完全看不出每个参数代表什么
  • 🚫 无法处理可选参数:所有参数都必须提供

2. Setter方法的隐患

AccountingVO vo = new AccountingVO();
vo.setAccountNo(accountNo);
vo.setTransOrderNo(transOrderNo);
// ... 省略6个setter
vo.setOperator(operator);
  • ⚠️ 对象状态不确定:在setter调用完成前,对象处于无效状态
  • 🔍 容易遗漏必填字段:没有编译时检查
  • 🔗 无法保证字段依赖关系:比如必须先设置账户号才能设置交易信息

二、阶梯式建造者模式:优雅的解决方案

让我们看看示例代码中展示的精妙设计。这不是普通的建造者模式,而是带有强制顺序的阶梯式建造者模式

设计精妙之处

1. 清晰的构建步骤接口

// 定义明确的构建步骤
public interface BuildSteps {
    interface AccountStep {
        TransOrderStep withAccountNo(String accountNo);  // 第一步:设置账户
    }
    
    interface TransOrderStep {
        AccountingStep withTransOrderNo(String transOrderNo, AcTransTypeEnum transType);  // 第二步:设置交易
    }
    
    // ... 更多步骤
}

每个步骤接口只暴露当前步骤可用的方法,强制调用者按照正确的顺序构建对象

2. 流畅的API调用体验

// 像说故事一样构建对象
AccountingVO vo = AccountingVO.builder()
    .withAccountNo("ACC001")                    // 第一步:必须有账户
    .withTransOrderNo("TXN2025001", DEBIT)     // 第二步:必须有交易
    .withAccounting(amount, INCOME, BALANCE_UP) // 第三步:记账逻辑
    .withOperation("张三", "工资收入")          // 第四步:操作信息
    .build();                                   // 最终构建

3. 处理复杂业务场景

代码中特别处理了解冻并扣减这种复杂场景:

// 专门的方法处理特定业务逻辑
.withUnfreezeAndDownAccounting(
    amount, 
    UNFREEZE_ACCOUNTING, 
    DEDUCTION_ACCOUNTING
)

这种方法不仅提供了便捷性,更封装了业务知识,确保调用者不会用错参数组合。

三、为什么这种设计"太帅了"?

1. 🛡️ 编译时安全性

错误的构建顺序会在编译时报错:

// 编译错误!必须先设置账户才能设置交易
AccountingVO.builder()
    .withTransOrderNo("TXN001", DEBIT)  // ❌ 编译错误
    .withAccountNo("ACC001")
    .build();

2. 📖 自文档化的API

方法名清晰表达了意图,不需要额外注释:

// 读起来就像业务需求文档
.withAccounting(amount, INCOME, BALANCE_UP)  // 记账:收入,余额增加
.withOperation("张三", "工资发放")            // 操作人:张三,备注:工资发放

3. 🔒 不可变对象的优雅构建

虽然示例中VO是可变的(使用了setter),但我们可以轻松改造为不可变对象:

// 结合建造者模式创建不可变对象
@Getter
@ToString
public class AccountingVO {
    private final String accountNo;
    private final String transOrderNo;
    // ... 所有字段都是final
    
    // 私有构造函数,只能通过建造者创建
    private AccountingVO(AccountVOBuilder builder) {
        this.accountNo = builder.accountNo;
        this.transOrderNo = builder.transOrderNo;
        // ...
    }
}

4. 🧩 灵活应对变化

当需要增加新字段时,只需在建造者中添加新步骤,不影响现有调用:

// 新增一个渠道字段
public interface BuildSteps {
    // 在适当的位置插入新步骤
    interface ChannelStep {
        BuildStep withChannel(String channel);
    }
}

四、实战应用场景

场景1:金融交易系统

// 构建一个转账请求
AccountingVO transfer = AccountingVO.builder()
    .withAccountNo("6225880123456789")          // 付款账户
    .withTransOrderNo("TRANS2025001", TRANSFER) // 转账交易
    .withAccounting(
        Money.of(1000.00, "CNY"),               // 转账金额
        EXPENSE,                                // 支出类型
        BALANCE_DOWN                            // 余额减少
    )
    .withOperation("系统", "向用户B转账")        // 操作信息
    .build();

场景2:复杂业务组合

// 处理解冻并扣减的复杂场景
AccountingVO unfreeze = AccountingVO.builder()
    .withAccountNo("ACC002")
    .withTransOrderNo("UNFREEZE001", SETTLEMENT)
    .withUnfreezeAndDownAccounting(            // 专门处理解冻扣减
        Money.of(500.00, "CNY"),
        UNFREEZE_ACCOUNTING,                   // 先解冻
        DEDUCTION_ACCOUNTING                   // 再扣减
    )
    .withOperation("风控系统", "风险解冻扣款")
    .build();

五、实现要点与最佳实践

1. 保持VO的简洁性

// VO本身应该保持简单
@Getter
@ToString
public class AccountingVO {
    // 只有字段和getter,没有setter
    private String accountNo;
    private String transOrderNo;
    // ...
    
    // 私有构造函数
    private AccountingVO() {}
    
    // 公开的建造者入口
    public static BuildSteps.AccountStep builder() {
        return new AccountVOBuilder();
    }
}

2. 建造者的内部实现

// 建造者内部类实现所有步骤接口
private static class AccountVOBuilder 
    implements BuildSteps.AccountStep, 
               BuildSteps.TransOrderStep,
               BuildSteps.AccountingStep,
               BuildSteps.OperationStep,
               BuildSteps.BuildStep {
    
    private final AccountingVO accountingVO;
    
    private AccountVOBuilder() {
        this.accountingVO = new AccountingVO();
    }
    
    @Override
    public BuildSteps.TransOrderStep withAccountNo(String accountNo) {
        this.accountingVO.accountNo = accountNo;
        return this;  // 返回下一步的接口
    }
    
    // ... 其他实现
}

3. 验证逻辑的放置

可以在build()方法中添加验证:

@Override
public AccountingVO build() {
    // 构建前的验证
    validate();
    return this.accountingVO;
}

private void validate() {
    if (accountingVO.accountNo == null) {
        throw new IllegalStateException("账户号不能为空");
    }
    if (accountingVO.transOrderNo == null) {
        throw new IllegalStateException("交易订单号不能为空");
    }
    // ... 更多验证
}

六、与其他模式的对比

模式 优点 缺点 适用场景
全参构造函数 简单直接 参数多时难以使用 字段少于4个的简单对象
Setter方法 灵活,可分步设置 对象状态不确定,线程不安全 需要频繁修改的对象
传统建造者 可读性好,支持可选参数 无法强制构建顺序 大多数复杂对象构建
阶梯式建造者 强制正确顺序,编译时安全 实现稍复杂 有严格构建顺序的业务对象

七、何时使用这种模式?

✅ 推荐使用场景:

  1. 字段多且有依赖关系的对象(如必须先有A才能有B)
  2. 业务规则复杂,需要确保构建顺序
  3. API需要自文档化,提高代码可读性
  4. 团队协作,减少误用可能

❌ 不推荐使用场景:

  1. 字段少于3个的简单对象
  2. 没有构建顺序要求的场景
  3. 性能极其敏感的场景(有轻微开销)

结语:优雅代码的艺术

这种阶梯式建造者模式不仅仅是一种技术实现,更是一种设计哲学的体现。它告诉我们:

  1. 优秀的API设计应该引导用户正确使用,而不是依赖文档或记忆
  2. 编译时检查比运行时检查更可靠
  3. 代码应该表达业务意图,而不仅仅是技术实现

同时,这个阶梯式Builder模式,更是整洁代码理念的完美体现:

  1. 它尊重读者的智慧:通过清晰的命名和结构,让代码不言自明
  2. 它尊重维护者的时间:通过编译时检查和良好的封装,减少调试时间
  3. 它尊重业务的变化:通过灵活的设计,优雅地应对需求变更
  4. 它尊重团队的合作:通过一致的约定,降低沟通成本

在《代码整洁之道》中,Robert C. Martin说:"整洁的代码只做好一件事。" 这个Builder模式正是如此——它专注于一件事:以最优雅、最安全的方式构建对象。

当你下次需要构建一个复杂对象时,不妨想想这个"太帅了"的设计。它可能多写了几行代码,但带来的可维护性、安全性和开发体验的提升,绝对是值得的。

记住:好的代码不是让机器容易理解,而是让人容易理解。 当你的代码既能正确运行,又能清晰表达业务意图时,你也许就真正掌握了整洁代码的艺术。


"编程的艺术在于创造简单,而非忍受复杂。"

※如需要AccountingVO完整代码,可私聊,或通过微信公众号《靠谱的程序员》找到我。※