对象创建的艺术:建造者模式
通常,我们要创建一个简单的对象,一个 new
关键字加上构造函数就轻松搞定。但如果我们要创建的是一个“配置复杂”的“大家伙”呢?
这时,我们可能会陷入一个由无数参数组成的“构造函数噩梦”,此时代码变得异常难以阅读且难以编写,但是通过使用建造者模式 (Builder Pattern),能让我们十分优雅地来构建复杂对象。
在本文中,我们将从最基础的构造函数出发,一步步揭示它的痛点,并最终展示建造者模式是如何优雅地解决这些问题的。
1. 构造函数的问题
假设我们正在开发一个咖啡店的点单系统。最开始,一杯咖啡很简单,只需要“大小”和“类型”。
// Coffee.java - Version 1.0
public class Coffee {
private String size; // 大小: "中杯", "大杯", "超大杯"
private String type; // 类型: "拿铁", "美式", "卡布奇诺"
// 构造函数
public Coffee(String size, String type) {
this.size = size;
this.type = type;
}
@Override
public String toString() {
return "一杯" + size + "的" + type;
}
}
创建一个对象非常直接:
Coffee latte = new Coffee("大杯", "拿铁");
System.out.println(latte); // 输出: 一杯大杯的拿铁
这看起来很完美。但很快,产品经理带来了新需求:顾客需要可选的“糖”、“牛奶种类”、“糖浆口味”等。
为了应对这些可选参数,我们最直观的想法就是增加更多的构造函数,这就是所谓的 “伸缩构造函数模式” (Telescoping Constructor Pattern)。
// Coffee.java - Version 2.0 (问题开始出现)
public class Coffee {
private String size; // 必需
private String type; // 必需
private String milk; // 可选
private boolean hasSugar; // 可选
private String syrup; // 可选
// 构造函数 1: 基础款
public Coffee(String size, String type) {
this(size, type, "标准牛奶", false, null);
}
// 构造函数 2: 加糖
public Coffee(String size, String type, boolean hasSugar) {
this(size, type, "标准牛奶", hasSugar, null);
}
// 构造函数 3: 自定义牛奶
public Coffee(String size, String type, String milk) {
this(size, type, milk, false, null);
}
// 构造函数 4: 终极完整版
public Coffee(String size, String type, String milk, boolean hasSugar, String syrup) {
this.size = size;
this.type = type;
this.milk = milk;
this.hasSugar = hasSugar;
this.syrup = syrup;
}
// ... 可能还有几十种组合
}
现在,问题暴露无遗:
- 参数地狱 (Parameter Hell): 构造函数的参数列表变得越来越长。如果有10个可选参数,为了覆盖所有情况,我们需要创建多少个构造函数?这简直是场灾难。
- 可读性极差 (Poor Readability): 当看到
new Coffee("超大杯", "美式", null, true, "香草")
这行代码时,我们可能明白null
代表什么,但true
又是什么意思呢?此时我们必须去翻阅类的源码才能理解,这大大降低了代码的可读性和可维护性。 - 容易出错 (Error-Prone): 如果两个参数类型相同,比如
boolean
,我们会很容易把它们的顺序搞混,而编译器却无法发现这个逻辑错误。 - 僵化不灵活 (Inflexible): 用户必须按照我们预设的“套餐”来创建对象。如果用户想要一个“无牛奶但加糖浆”的组合,而我们恰好没有提供对应的构造函数,那就无能为力了。
2. 建造者模式登场
为了解决上述所有问题,建造者模式应运而生。它将一个复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。
听起来很抽象?别担心,我们还是用咖啡的例子来改造它。建造者模式的核心思想是:不要一次性把所有参数都给我,而是给我一张“订单”(Builder),你一项一项地填写,填好了再交给我来“制作”(build)。
// Coffee.java - Version 3.0 (使用建造者模式)
public class Coffee {
private final String size; // 必需
private final String type; // 必需
private final String milk; // 可选
private final boolean hasSugar; // 可选
private final String syrup; // 可选
// 构造函数是私有的!只能被Builder调用
private Coffee(Builder builder) {
this.size = builder.size;
this.type = builder.type;
this.milk = builder.milk;
this.hasSugar = builder.hasSugar;
this.syrup = builder.syrup;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("一杯").append(size).append("的").append(type);
if (milk != null) sb.append(",使用").append(milk);
if (hasSugar) sb.append(",加糖");
if (syrup != null) sb.append(",加入").append(syrup).append("糖浆");
return sb.toString();
}
// 静态内部类 Builder
public static class Builder {
// 必需参数在Builder的构造函数中强制传入
private final String size;
private final String type;
// 可选参数提供默认值
private String milk = "标准牛奶";
private boolean hasSugar = false;
private String syrup = null;
public Builder(String size, String type) {
this.size = size;
this.type = type;
}
// 每个可选参数都有一个"with"或"set"方法
// 返回Builder本身,以支持链式调用
public Builder withMilk(String milk) {
this.milk = milk;
return this;
}
public Builder withSugar(boolean hasSugar) {
this.hasSugar = hasSugar;
return this;
}
public Builder withSyrup(String syrup) {
this.syrup = syrup;
return this;
}
// 最后,调用build()方法,返回一个Coffee实例
public Coffee build() {
// 在这里可以进行参数校验
if (size == null || type == null) {
throw new IllegalStateException("咖啡的大小和类型是必需的!");
}
return new Coffee(this);
}
}
}
现在,让我们看看如何使用新的方式来点一杯“超大杯、燕麦奶、加糖、香草糖浆的拿铁”:
Coffee myCustomCoffee = new Coffee.Builder("超大杯", "拿铁")
.withMilk("燕麦奶")
.withSugar(true)
.withSyrup("香草")
.build();
System.out.println(myCustomCoffee);
// 输出: 一杯超大杯的拿铁,使用燕麦奶,加糖,加入香草糖浆
这种链式调用的代码就像在读一段自然语言的描述,十分优雅!
建造者模式的优势:
- 极佳的可读性: 代码即文档,每个方法调用都在清楚地描述它在做什么。
- 高度的灵活性: 我们可以按任意顺序调用设置方法。
- 更强的健壮性: 对象的创建被延迟到
build()
方法被调用的那一刻。我们可以在build()
方法中对所有参数进行统一的校验,确保创建出的对象永远是合法的。 - 不变性: 一旦对象通过
build()
创建出来,它的状态就不能再改变(所有字段都是final
的),这使得它在多线程环境下是安全的。
3. 选择哪种模式
现在,我们应该很清楚这两种模式的差异。那么在实际开发中该如何抉择呢?
- 使用构造函数 (Constructor): 当我们的对象非常简单,只有几个必需的属性(通常少于4个),并且没有复杂逻辑时,直接使用构造函数是最简单、最直接的方式。
- 使用建造者模式 (Builder Pattern): 当我们的对象具有以下一个或多个特征时,请毫不犹豫地使用建造者模式,多一秒的犹豫就是对建造者模式的不尊重:
- 有很多构造参数。
- 大部分参数是可选的。
- 在创建对象前需要进行复杂的参数校验。
- 希望创建不可变的对象。
评论区
请登录后发表评论