1. 为什么我们需要建造者模式
上周团队里有个刚毕业的新同事提交了一段创建订单对象的代码,我一看就乐了——构造方法足足有15个参数!这种代码别说维护了,连调用的时候都容易传错参数顺序。这正是建造者模式要解决的典型场景。
建造者模式(Builder Pattern)属于创建型设计模式,它特别适合以下三种情况:
- 当对象需要多个初始化参数,且这些参数之间存在复杂的组合关系时
- 当对象的构造过程需要分步骤进行,且不同场景需要不同的构造流程时
- 当需要创建不可变对象但又想保持代码可读性时
提示:建造者模式与工厂模式最大的区别在于,工厂模式关注整体对象的创建,而建造者模式关注对象各部分的有序组装。
1.1 建造者模式的核心思想
想象你在麦当劳点餐:你不需要记住所有套餐组合,而是通过"汉堡+薯条+可乐"这样的分步选择完成点单。建造者模式也是这样工作的:
- 将复杂对象的构造过程拆分为多个独立步骤
- 通过统一的指挥者(Director)控制组装流程
- 最终产出完整产品
这种模式完美遵循了单一职责原则——对象构造的每个步骤都独立变化,也符合开闭原则——新增产品类型时无需修改现有代码。
2. 建造者模式的四种实现方式
2.1 经典建造者实现
这是《设计模式》原书中标准的实现方式,包含四个角色:
// 产品类 class Computer { private String cpu; private String memory; // 其他组件... // 只提供setter方法确保不可变 public void setCpu(String cpu) { this.cpu = cpu; } } // 抽象建造者 interface ComputerBuilder { void buildCpu(); void buildMemory(); Computer getResult(); } // 具体建造者 class GamingComputerBuilder implements ComputerBuilder { private Computer computer = new Computer(); @Override public void buildCpu() { computer.setCpu("i9-13900K"); } // 其他实现... } // 指挥者 class Director { public Computer construct(ComputerBuilder builder) { builder.buildCpu(); builder.buildMemory(); return builder.getResult(); } }这种实现最规范,但代码量也最大。适合需要支持多种产品类型的复杂场景。
2.2 流式建造者(链式调用)
这是Java开发中最常见的实现方式,Lombok的@Builder注解就是这种风格:
public class User { private final String name; // final确保不可变 private final int age; private User(Builder builder) { this.name = builder.name; this.age = builder.age; } public static class Builder { private String name; private int age; public Builder name(String name) { this.name = name; return this; // 返回this实现链式调用 } public User build() { return new User(this); } } } // 使用示例 User user = new User.Builder() .name("张三") .age(25) .build();实测发现这种写法有三大优势:
- 代码可读性极强,就像在说"创建一个用户,名字叫张三,年龄25"
- 参数顺序灵活,可以任意组合
- 天然支持参数校验(在build()方法中)
2.3 静态内部类变体
这是Joshua Bloch在《Effective Java》中推荐的写法:
public class NutritionFacts { private final int servingSize; private final int calories; public static class Builder { // 必选参数 private final int servingSize; // 可选参数(带默认值) private int calories = 0; public Builder(int servingSize) { this.servingSize = servingSize; } public Builder calories(int val) { calories = val; return this; } public NutritionFacts build() { return new NutritionFacts(this); } } private NutritionFacts(Builder builder) { servingSize = builder.servingSize; calories = builder.calories; } }这种写法特别适合既有必选参数又有可选参数的场景。我在电商系统中创建订单对象时就采用了这种模式。
2.4 函数式实现(Java 8+)
利用Supplier函数接口可以实现非常灵活的建造者:
public class FunctionalBuilder<T> { private final Supplier<T> instantiator; private List<Consumer<T>> modifiers = new ArrayList<>(); public FunctionalBuilder(Supplier<T> instantiator) { this.instantiator = instantiator; } public static <T> FunctionalBuilder<T> of(Supplier<T> instantiator) { return new FunctionalBuilder<>(instantiator); } public <U> FunctionalBuilder<T> with(BiConsumer<T, U> consumer, U value) { modifiers.add(instance -> consumer.accept(instance, value)); return this; } public T build() { T value = instantiator.get(); modifiers.forEach(modifier -> modifier.accept(value)); modifiers.clear(); return value; } } // 使用示例 Person person = FunctionalBuilder.of(Person::new) .with(Person::setName, "李四") .with(Person::setAge, 30) .build();这种写法虽然炫酷,但团队新人理解成本较高,建议只在函数式编程经验丰富的团队中使用。
3. 建造者模式的实战应用
3.1 MyBatis中的建造者模式
MyBatis的SqlSessionFactoryBuilder就是建造者模式的经典案例:
SqlSessionFactory factory = new SqlSessionFactoryBuilder() .build(inputStream); // 背后其实是分步构建: public SqlSessionFactory build(InputStream inputStream) { // 1. 解析配置 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream); // 2. 创建配置对象 Configuration config = parser.parse(); // 3. 创建工厂 return build(config); }这种设计让配置解析和工厂创建解耦,后续MyBatis新增XML以外的配置方式(如注解配置)时,完全不需要修改SqlSessionFactoryBuilder的代码。
3.2 JDK中的建造者模式
Java标准库中也有不少建造者模式的应用:
- StringBuilder/StringBuffer:通过append()方法逐步构建字符串
- Stream API:通过流式操作构建最终结果
- DateTimeFormatterBuilder:构建复杂的时间格式化器
特别值得注意的是,这些JDK内置的建造者都采用了可变对象设计,因为字符串、时间格式这些对象本身就需要频繁修改。
3.3 Spring中的建造者模式
Spring框架中有个不太为人知但非常有用的建造者——BeanDefinitionBuilder:
BeanDefinitionBuilder builder = BeanDefinitionBuilder .rootBeanDefinition(MyService.class) .addPropertyValue("timeout", 1000) .setInitMethodName("init"); // 注册到容器 context.registerBeanDefinition("myService", builder.getBeanDefinition());在需要动态注册Bean的场景(如自定义Starter开发)中,这个建造者能大幅简化代码。
4. 建造者模式的进阶技巧
4.1 与工厂模式结合使用
建造者模式负责对象组装,工厂模式负责对象创建,二者可以完美配合:
public interface PizzaFactory { PizzaBuilder createBuilder(PizzaType type); } public class PizzaShop { private PizzaFactory factory; public Pizza orderPizza(PizzaType type) { PizzaBuilder builder = factory.createBuilder(type); return new PizzaDirector().construct(builder); } }这种组合在需要创建复杂对象家族时特别有用,比如电商系统中的订单子系统。
4.2 实现级联建造者
当对象包含嵌套对象时,可以设计级联建造者:
public class Order { private List<OrderItem> items; public static class Builder { private List<OrderItem.Builder> itemBuilders = new ArrayList<>(); public OrderItem.Builder addItem() { OrderItem.Builder builder = new OrderItem.Builder(); itemBuilders.add(builder); return builder; } public Order build() { Order order = new Order(); order.items = itemBuilders.stream() .map(OrderItem.Builder::build) .collect(Collectors.toList()); return order; } } }这样在创建订单时,可以流畅地添加多个订单项:
Order order = new Order.Builder() .addItem().product(p1).quantity(2).up() .addItem().product(p2).quantity(1).up() .build();4.3 线程安全的建造者实现
如果建造者需要在多线程环境下使用,可以采用这些策略:
- 防御性拷贝:在build()方法中创建新对象时复制所有参数
- 不可变建造者:所有setter方法都返回新建造者实例
- 同步控制:对关键方法加synchronized(性能较差)
5. 常见问题与性能优化
5.1 建造者模式的内存开销
每个建造者实例都会持有产品对象的引用,在需要创建大量对象时可能引发内存问题。解决方案:
- 重用建造者实例(调用build()后重置状态)
- 使用对象池管理建造者
- 对于简单对象,改用静态工厂方法
5.2 与Lombok @Builder的兼容问题
Lombok的@Builder很方便,但有几个坑需要注意:
- 与@AllArgsConstructor冲突:需要显式添加@AllArgsConstructor(access = AccessLevel.PRIVATE)
- 继承问题:父类的字段不会自动包含在建造者中
- 默认值设置:需要在字段上直接初始化,而不是在建造者中
5.3 建造者模式的单元测试
测试建造者时应该关注:
- 建造者方法是否正确地设置了属性
- build()方法是否执行了必要的参数校验
- 不同建造者组合是否产生预期结果
建议采用行为验证而非状态验证:
@Test void shouldSetCpuWhenBuildComputer() { ComputerBuilder builder = mock(ComputerBuilder.class); Director director = new Director(); director.construct(builder); verify(builder).buildCpu(); verify(builder).buildMemory(); }6. 什么情况下不该用建造者模式
虽然建造者模式很强大,但也不是银弹。以下情况不建议使用:
- 对象创建非常简单(少于4个参数)
- 参数之间没有复杂的依赖关系
- 不需要创建不可变对象
- 性能敏感的场景(建造者会有额外对象创建开销)
我在实际项目中的经验法则是:当发现自己在写超过3个参数的构造函数时,就该考虑改用建造者模式了。