ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Java建造者模式实战:从复杂对象创建到Fluent API设计

Java建造者模式实战:从复杂对象创建到Fluent API设计 1. 从“一锅乱炖”到“精工细作”为什么我们需要建造者模式如果你写过一些稍微复杂点的对象创建代码大概率遇到过这样的场景一个类有十几个属性其中有些是必填的有些是选填的有些属性之间还有依赖关系或者约束条件。比如你要创建一个“电脑配置单”对象它有CPU型号、内存大小、硬盘容量、显卡型号、电源功率、机箱类型等等。最直接的写法可能就是提供一个“巨无霸”构造函数public class Computer { private String cpu; // 必填 private String memory; // 必填 private String storage; // 必填 private String gpu; // 可选 private String powerSupply; // 可选但依赖是否有独立显卡 private String cooling; // 可选依赖CPU型号和是否超频 private String motherboard; // 可选但必须兼容CPU // ... 还有更多属性 // 噩梦般的构造函数 public Computer(String cpu, String memory, String storage, String gpu, String powerSupply, String cooling, String motherboard, ...) { this.cpu cpu; this.memory memory; // ... 一堆赋值 // 这里可能还需要一堆参数校验逻辑 if (gpu ! null powerSupply null) { throw new IllegalArgumentException(使用独立显卡必须指定电源); } // ... 更多校验 } // 你可能还需要提供多个重载的构造函数来应对不同参数组合 }这种写法的问题显而易见。首先可读性极差。调用方看到new Computer(i7, 16G, 1T SSD, RTX 4080, 850W, 水冷, Z790, ...)这一长串参数时根本分不清哪个参数对应哪个属性除非你不停地去查文档或看源码。其次灵活性极低。如果你想创建一个只有CPU、内存、硬盘的“丐版”电脑你也得为那些可选参数传null代码变得冗长且容易出错。最后难以维护和扩展。一旦需要增加或删除一个属性构造函数签名就要改变所有调用它的地方都得修改这违反了开闭原则。建造者模式Builder Pattern就是为了优雅地解决这类“复杂对象创建”问题而生的。它的核心思想是将复杂对象的构建过程与其表示分离。简单说就是别把所有配料一股脑倒进锅里而是找个“大厨”Builder让他按照你的要求一步一步、清晰有序地把菜做好。这个模式在Java世界里尤其是当Fluent API链式调用风格流行起来后变得无处不在比如StringBuilder、OkHttpClient.Builder、AlertDialog.Builder等等。它让代码的创建过程像搭积木一样清晰、灵活且安全。2. 建造者模式的四重角色与协作机制要理解建造者模式不能只停留在“链式调用很酷”的表面必须深入到它的角色设计和协作流程中。这个模式通常涉及四个关键角色它们各司其职共同完成对象的构建。2.1 产品Product最终要构建的复杂对象这是我们要构建的目标也就是前面例子中的Computer类。在建造者模式中这个产品类通常具有较多的属性并且其构建过程属性赋值、校验、组装可能比较复杂。// 产品类 - Computer public class Computer { // 使用final修饰保证对象一旦构建完成就是不可变的Immutable这是建造者模式的一个常见优势。 private final String cpu; private final String memory; private final String storage; private final String gpu; private final String powerSupply; private final String cooling; // 构造函数设为private防止外部直接new。产品只通过Builder来构建。 private Computer(Builder builder) { this.cpu builder.cpu; this.memory builder.memory; this.storage builder.storage; this.gpu builder.gpu; this.powerSupply builder.powerSupply; this.cooling builder.cooling; // 可以在这里集中进行最终的状态校验 validate(); } private void validate() { if (cpu null || memory null || storage null) { throw new IllegalStateException(CPU、内存、存储为必选配置); } if (gpu ! null powerSupply null) { throw new IllegalStateException(配置了独立显卡必须同时指定电源); } } // 省略getter方法... public static class Builder { // Builder内部持有与Product相同的属性或状态 private String cpu; private String memory; private String storage; private String gpu; private String powerSupply; private String cooling; // 必选参数的Builder构造方法 public Builder(String cpu, String memory, String storage) { this.cpu cpu; this.memory memory; this.storage storage; } public Builder gpu(String gpu) { this.gpu gpu; return this; // 返回this支持链式调用 } public Builder powerSupply(String powerSupply) { this.powerSupply powerSupply; return this; } public Builder cooling(String cooling) { this.cooling cooling; return this; } // 最终的build方法创建并返回Product实例 public Computer build() { return new Computer(this); } } }为什么产品类的属性常用final且构造函数是private这是一种被称为“构建器模式”Builder Pattern的一种实现的最佳实践。final确保了对象的不变性Immutable这意味着对象一旦创建其状态就无法被更改。不可变对象是线程安全的无需同步更容易推理和维护。将构造函数私有化则强制所有客户端代码都必须通过Builder来创建对象这保证了构建逻辑的统一性和封装性你可以在Builder的build()方法或产品的私有构造函数中集中进行所有校验。2.2 抽象建造者Builder定义构建步骤的接口可选在标准的Gof设计模式中定义了一个Builder接口它声明了创建产品各个部分的抽象方法。当需要构建不同表示例如石头房子和木头房子的复杂对象时这个接口非常有用。它让具体建造者去实现这些方法而指挥者则面向接口编程。// 抽象建造者接口 public interface ComputerBuilder { ComputerBuilder buildCpu(String cpu); ComputerBuilder buildMemory(String memory); ComputerBuilder buildStorage(String storage); ComputerBuilder buildGpu(String gpu); Computer build(); // 最终构建方法 }然而在大量实际应用场景中特别是当一种产品的构建过程只有一种标准方式时我们常常省略这个接口直接使用一个静态内部类作为具体建造者就像上面Computer例子中的Builder类。这种变体被称为“静态内部类建造者”或“流式建造者”它更简洁也是Java领域最流行的写法。是否需要抽象接口取决于你的系统是否需要支持多种不同的构建过程或产品表示。2.3 具体建造者ConcreteBuilder实现构建过程这是实现Builder接口如果有的话或直接定义构建步骤的类。它负责装配产品的各个部件。定义并实现这些部件的构建步骤即那些gpu(),powerSupply()等方法。提供一个返回最终产品的方法build()。在我们上面的例子中Computer.Builder这个静态内部类就扮演了具体建造者的角色。它知道如何一步步设置Computer的各个属性并在build()方法中调用Computer的私有构造函数来生成最终产品。链式调用Fluent API的秘诀注意看Builder中的gpu(),powerSupply()等方法都返回了Builder自身return this;。这就是链式调用的关键。它允许你写出像new Computer.Builder(i7, 16G, 1T).gpu(RTX 4080).powerSupply(850W).build()这样流畅的代码极大提升了代码的可读性和编写体验。2.4 指挥者Director控制构建流程可选指挥者是一个可选的角色。它负责使用Builder接口来构建产品定义了构建的步骤和顺序。指挥者将构建过程与具体的Builder解耦客户端只需要告诉指挥者需要什么类型的产品或者传递一个具体的Builder给指挥者即可。// 指挥者 public class ComputerDirector { public Computer constructGamingComputer(Computer.Builder builder) { return builder .gpu(高性能独立显卡) .powerSupply(大功率金牌电源) .cooling(液冷散热系统) .build(); } public Computer constructOfficeComputer(Computer.Builder builder) { // 办公电脑可能不需要独立显卡和高级散热 return builder .gpu(集成显卡) // 或者不调用gpu方法 .powerSupply(标准电源) .build(); } }指挥者用还是不用如果你的对象的构建流程非常固定有几种明确的“套餐”或“模板”那么使用指挥者可以复用这些构建逻辑避免客户端代码重复编写相同的链式调用。例如“游戏电脑套餐”、“办公电脑套餐”。如果每次构建的差异很大客户端更倾向于自由组合那么通常省略指挥者让客户端直接操作Builder会更灵活。在实际项目中直接使用Builder的场景远多于使用指挥者。这四个角色的协作流程可以概括为客户端创建或获取一个具体建造者Builder然后通过调用其一系列设置方法可能通过指挥者来组织调用顺序来逐步配置产品最后调用build()方法得到最终构建好的、不可变的产品对象。3. 深入实践建造者模式的四种经典实现变体理解了基本结构我们来看看在实际编码中建造者模式有哪些常见的实现方式以及它们各自的适用场景和坑。3.1 经典Gof风格适用于构建过程复杂且多变这是最教科书式的实现严格区分产品、抽象建造者、具体建造者和指挥者。当你的系统需要构建多个不同表示的复杂对象时这种形式最合适。假设我们要构建一份不同格式的文档Markdown和HTML。// 1. 产品 - 文档 public class Document { private String title; private String body; private String footer; // 省略getter和构造函数 } // 2. 抽象建造者 public interface DocumentBuilder { void buildTitle(String title); void buildBody(String body); void buildFooter(String footer); Document getResult(); } // 3. 具体建造者 - Markdown文档建造者 public class MarkdownDocumentBuilder implements DocumentBuilder { private StringBuilder content new StringBuilder(); Override public void buildTitle(String title) { content.append(# ).append(title).append(\n\n); } Override public void buildBody(String body) { content.append(body).append(\n\n); } Override public void buildFooter(String footer) { content.append(---\n*).append(footer).append(*); } Override public Document getResult() { Document doc new Document(); // 这里简化处理实际可能将StringBuilder内容设给Document doc.setContent(content.toString()); return doc; } } // 4. 指挥者 public class DocumentDirector { public Document construct(DocumentBuilder builder) { builder.buildTitle(设计模式报告); builder.buildBody(建造者模式是一种创建型模式...); builder.buildFooter(报告人张三); return builder.getResult(); } } // 客户端使用 public class Client { public static void main(String[] args) { DocumentDirector director new DocumentDirector(); DocumentBuilder mdBuilder new MarkdownDocumentBuilder(); Document mdDocument director.construct(mdBuilder); System.out.println(mdDocument.getContent()); // 输出 // # 设计模式报告 // // 建造者模式是一种创建型模式... // // --- // *报告人张三* } }这种方式的优缺点优点完全符合开闭原则。要新增一种文档格式如PDF只需新增一个PdfDocumentBuilder无需修改指挥者和其他建造者。指挥者固定了构建流程。缺点代码量较大结构略显繁琐。对于大多数只需要构建一种产品、但产品本身属性很多的场景有点“杀鸡用牛刀”。3.2 静态内部类建造者流式建造者Java领域的绝对主流这是目前Java社区最流行、最实用的实现方式也就是我们在第2节Computer类中展示的写法。它将具体建造者作为产品类的静态内部类。再谈Computer例子的几个关键细节必选参数的处理通常通过Builder的构造函数来强制要求必选参数。public Builder(String cpu, String memory, String storage)确保了在创建Builder对象时这三个核心参数必须提供。可选参数的流式设置通过返回this的方法提供优雅的链式调用。build()方法中的校验构建的最后一步在Computer的私有构造函数或build()方法内部进行最终状态的一致性校验。这是保证产品合法性的重要关口。一个更贴近业务的例子订单创建public class Order { private final String orderId; private final String customerId; private final ListOrderItem items; private final BigDecimal totalAmount; private final String shippingAddress; private final String status; private final LocalDateTime createTime; private Order(Builder builder) { this.orderId builder.orderId; this.customerId builder.customerId; this.items Collections.unmodifiableList(new ArrayList(builder.items)); // 防御性拷贝 this.totalAmount builder.totalAmount; this.shippingAddress builder.shippingAddress; this.status builder.status; this.createTime builder.createTime; validate(); } private void validate() { if (orderId null) throw new IllegalStateException(订单ID不能为空); if (customerId null) throw new IllegalStateException(客户ID不能为空); if (items null || items.isEmpty()) throw new IllegalStateException(订单项不能为空); if (totalAmount null || totalAmount.compareTo(BigDecimal.ZERO) 0) { throw new IllegalStateException(订单总金额必须大于0); } // 可以计算items总价并与totalAmount校验这里省略 } public static class Builder { // 必选 private String orderId; private String customerId; private ListOrderItem items new ArrayList(); private BigDecimal totalAmount; // 可选有默认值 private String shippingAddress 默认地址; private String status CREATED; private LocalDateTime createTime LocalDateTime.now(); // 强制必选参数的Builder构造函数 public Builder(String orderId, String customerId, BigDecimal totalAmount) { this.orderId orderId; this.customerId customerId; this.totalAmount totalAmount; } public Builder items(ListOrderItem items) { this.items items; return this; } // 也常提供添加单个item的方法 public Builder addItem(OrderItem item) { this.items.add(item); return this; } public Builder shippingAddress(String shippingAddress) { this.shippingAddress shippingAddress; return this; } public Builder status(String status) { this.status status; return this; } public Builder createTime(LocalDateTime createTime) { this.createTime createTime; return this; } public Order build() { return new Order(this); } } // 省略getter... } // 使用 Order order new Order.Builder(ORD123, CUST456, new BigDecimal(5999.00)) .addItem(new OrderItem(商品A, 1, new BigDecimal(2999))) .addItem(new OrderItem(商品B, 1, new BigDecimal(3000))) .shippingAddress(北京市海淀区...) .status(PAID) .build();这种方式的威力线程安全由于Order对象是不可变的多个线程读取它是安全的。表达力强链式调用清晰地表达了构建意图。灵活可选参数可以自由组合不必再面对冗长的构造函数或大量的setter方法。3.3 Lombok Builder极简主义的生产力工具如果你在项目中使用Lombok那么实现建造者模式简单到令人发指。一个注解就能搞定。import lombok.Builder; import lombok.Value; import java.util.List; Value // 生成一个所有字段都是final的不可变类并自动生成getter、equals、hashCode、toString Builder public class ImmutableComputer { NonNull // 结合Builder会在build()时校验非空 String cpu; NonNull String memory; NonNull String storage; String gpu; String powerSupply; String cooling; } // 使用 ImmutableComputer computer ImmutableComputer.builder() .cpu(i7) .memory(16G) .storage(1T SSD) .gpu(RTX 4080) .build();Lombok Builder 的优缺点与坑优点代码极其简洁无需手动编写Builder内部类。Lombok会自动生成一个名为[YourClass]Builder的静态内部类以及对应的builder()静态方法。缺点与坑默认值问题Lombok生成的Builder其字段的默认值是Java的默认值如null、0、false。如果你想为可选字段设置业务默认值比如status CREATED需要手动在字段上初始化但要注意这个默认值是在Builder对象被创建时就赋予的而不是在build()时。如果你在链式调用中设置了该字段它会覆盖默认值。集合类型处理对于集合字段如ListStringLombok生成的setter方法通常是直接赋值。这意味着如果你传一个ArrayList进去Builder内部持有的就是这个ArrayList的引用在build()时如果直接用它来初始化产品产品的集合就可能被外部修改除非你在产品构造函数里做防御性拷贝。更安全的做法是使用Singular注解Lombok会生成item()和items()方法并在build()时生成一个不可修改的集合。Builder public class Order { Singular // 重要 private ListOrderItem items; } // 使用 Order order Order.builder() .item(new OrderItem(...)) // 可以逐个添加 .items(existingList) // 也可以批量添加 .build(); // build() 时会生成一个不可变的List必选参数约束Lombok的Builder默认不强制必选参数。虽然你可以结合NonNull在build()时进行空值检查并抛出NullPointerException但这是一种运行时检查而非编译时约束。经典的静态内部类Builder通过构造函数强制必选参数能在编译期就发现问题。定制化构建逻辑如果构建过程需要复杂的校验或计算Lombok生成的build()方法可能不够用。虽然你可以自己写一个build()方法来覆盖它但这失去了部分便利性。个人建议对于简单的DTO数据传输对象或配置类LombokBuilder非常方便。但对于核心领域模型尤其是构建逻辑复杂、有严格不变性要求和校验规则的对象我仍然倾向于手写静态内部类Builder因为它能提供更强的编译时安全性和更清晰的构建逻辑控制。3.4 继承场景下的建造者模式处理父类属性当产品类处于继承体系中时建造者模式需要一些特殊处理以确保子类的Builder也能设置父类的属性。public abstract class Animal { protected final String name; protected final int age; // 抽象建造者 protected abstract static class BuilderT extends BuilderT { protected String name; protected int age; public T name(String name) { this.name name; return self(); } public T age(int age) { this.age age; return self(); } protected abstract T self(); // 关键返回具体子类Builder类型 public abstract Animal build(); } protected Animal(Builder? builder) { this.name builder.name; this.age builder.age; } } public class Dog extends Animal { private final String breed; public static class Builder extends Animal.BuilderBuilder { private String breed; public Builder breed(String breed) { this.breed breed; return this; } Override protected Builder self() { return this; // 返回Dog.Builder自己 } Override public Dog build() { return new Dog(this); } } private Dog(Builder builder) { super(builder); // 初始化父类属性 this.breed builder.breed; } } // 使用可以链式调用父类和子类的方法 Dog dog new Dog.Builder() .name(Buddy) // 来自Animal.Builder .age(3) // 来自Animal.Builder .breed(Golden Retriever) // 来自Dog.Builder .build();这里的技巧是使用递归泛型Recursive Generic TypeBuilderT extends BuilderT并在抽象父类Builder中定义一个抽象的self()方法。子类Builder继承时指定泛型为自身类型Dog.Builder extends Animal.BuilderDog.Builder并实现self()返回this。这样在父类Builder的方法如name()中返回类型就是T即子类Builder类型从而实现了在子类Builder上也能流畅地调用父类Builder方法的链式调用。这个技巧在《Effective Java》中被称为“模拟自我类型simulated self-type”。4. 实战中的抉择、陷阱与性能考量了解了多种实现在实际项目中该如何选择又会遇到哪些坑4.1 何时用何时不用使用建造者模式的典型场景产品类属性众多且很多是可选的这是最直接的信号。当你的构造函数参数超过4个尤其是很多是同类型的比如多个String并且其中大部分参数是可选的建造者模式能显著提升代码可读性和安全性。产品对象需要不可变Immutable如果你希望对象一旦创建就不能被修改这是函数式编程和并发编程中的良好实践建造者模式配合final字段和私有构造函数是绝配。build()方法返回的就是一个“完成态”的、线程安全的对象。对象的构建过程复杂或存在依赖关系比如构建一个对象需要分步骤或者某些属性的设置依赖于其他属性如设置了独立显卡就必须设置大电源。你可以在Builder的方法中或最终的build()方法里进行这些逻辑校验。需要构建不同“风味”或“表示”的对象虽然指挥者不常用但如果你确实有几种固定的构建模板如“高配版”、“标准版”使用指挥者可以很好地封装这些模板逻辑。不建议使用建造者模式的场景属性很少3个且都是必填直接使用构造函数即可引入Builder反而增加了复杂度。KISS原则Keep It Simple, Stupid优先。对象需要频繁修改建造者模式创建的是不可变对象。如果你的业务模型需要频繁改变状态比如一个购物车随时添加删除商品那么使用传统的setter方法或领域驱动设计中的聚合根变更方法可能更合适。不过即使是可变对象如果创建时参数很多也可以使用Builder来初始化之后再用setter修改但这破坏了不可变性需权衡。对性能有极端要求建造者模式需要先创建Builder对象再设置属性最后构建产品对象。比直接调用构造函数多了一次对象分配Builder对象本身。在绝大多数应用中这点开销微不足道。但在某些底层框架或性能极其敏感的循环中可能需要考虑。4.2 常见陷阱与最佳实践线程安全问题Builder对象本身通常不是线程安全的因为它内部持有可变状态。所以不要将Builder实例作为共享变量在多线程间传递和修改。正确的做法是在每个线程中创建自己的Builder实例。构建完成的产品如果是不可变的则是线程安全的。参数校验的时机早期校验Eager Validation在Builder的setter方法中进行。例如age(int age)方法中可以检查age是否大于0。这能尽早失败但有时校验需要依赖多个字段的组合如显卡和电源。最终校验Final Validation在build()方法或产品类的私有构造函数中进行。这是进行跨字段逻辑校验和完整性校验的最佳位置。强烈建议将核心的业务规则校验放在这里确保出厂的产品是合法的。组合使用对于简单的格式校验非空、范围可以在setter中做对于复杂的业务规则在build()中做。默认值设置在静态内部类Builder中可以在字段声明处直接给可选字段赋默认值。这比在build()方法里赋值更好因为客户端可以通过链式调用清晰地覆盖它们。public static class Builder { private String status PENDING; // 默认值 private int retryCount 0; // ... }与Jackson等序列化框架的集成如果你用建造者模式创建不可变对象同时还需要JSON反序列化比如Spring MVC接收请求体需要做一些配置。Jackson默认通过无参构造函数和setter来反序列化。对于建造者模式你需要确保Builder有一个无参构造函数或者所有参数都有默认值。在产品的类上使用JsonDeserialize(builder YourClass.Builder.class)注解。在Builder的build()方法上使用JsonPOJOBuilder注解如果命名不是标准前缀with。或者更简单的方式是使用Jacksonized注解Lombok 1.18.14 提供它会自动配置好这一切。“ Telescoping Constructor” 反模式的彻底解决建造者模式是解决“重叠构造器”Telescoping Constructor反模式即提供多个参数数量不同的构造函数的终极方案。它用一套统一的、可读的API取代了那些令人困惑的重载构造函数。4.3 性能考量与对象创建开销这是一个经常被问到的问题多创建一个Builder对象会不会有性能问题在99%的应用场景下完全不需要担心。一次额外的对象分配在现代JVM上的开销是纳秒级的。与之带来的代码可读性、可维护性、安全性的提升收益是巨大的。只有在以下极端情况才需要考虑你正在编写底层库如网络协议解析、序列化框架并且该代码路径会被每秒调用数百万次。你处于一个内存和GC压力极大的嵌入式环境。即使在这些场景也有优化手段。例如可以考虑重用Builder对象但要注意线程安全和状态清理或者对于极其简单的对象直接使用构造函数。但在业务代码中请优先考虑代码清晰度和健壮性建造者模式带来的那点微乎其微的性能开销绝对是值得的投入。从我个人的经验来看建造者模式是Java中改善对象创建代码质量最有效的模式之一。它强迫你思考对象的合法状态促进不可变对象的使用并产生自描述性极强的API。下次当你面对一个参数众多的构造函数时别犹豫拿起建造者模式这把“瑞士军刀”你的代码和未来的维护者都会感谢你。
返回列表