建造者模式(Builder):从“盖房子”到“写代码”的工程智慧
说实话,我第一次接触建造者模式,是在一个被“构造函数爆炸”折磨的下午。
那时候我接手一个老项目,有个订单类,构造函数有十几个参数。每次 new 一个对象,都得对着参数列表数半天。更可怕的是,参数类型都是 String,传错顺序编译器也不报错。线上出过好几次 bug,都是因为参数传反了。
嗯,从那以后,我就对建造者模式有了特殊的感情。
建造者模式的结构:把“复杂构造”拆成“简单步骤”
建造者模式的核心思想,说白了就是:把一个复杂对象的构建过程,和它的最终表示分离开。
你想想看,盖一栋房子,你不会一次性把所有材料堆在一起。你会先打地基,再砌墙,再封顶。每一步都是独立的工序。建造者模式也是这个道理。
它的结构通常包含四个角色:
- 产品(Product):最终要创建的那个复杂对象。比如房子、订单、查询条件。
- 抽象建造者(Builder):定义构建产品的抽象接口。比如“打地基”、“砌墙”、“封顶”这些方法。
- 具体建造者(ConcreteBuilder):实现抽象建造者,负责具体的构建逻辑。比如“砖房建造者”、“木屋建造者”。
- 指挥者(Director):控制构建流程。它不关心具体怎么建,只关心“先打地基再砌墙”这个顺序。
我画了一张图,帮你理清它们的关系:
你看,指挥者只负责“编排流程”,具体怎么建,由具体建造者说了算。这样,换一种建造者,就能产出完全不同的产品。
建造者模式 vs 工厂模式:到底有啥区别?
这个问题,我面试的时候经常问。很多人答不上来,或者背概念背得很溜,一写代码就混。
我个人的理解是这样的:
- 工厂模式:关注的是“创建谁”。你给我一个类型,我直接给你一个完整的产品。适合创建单一、完整的对象。
- 建造者模式:关注的是“怎么创建”。你一步步告诉我怎么搭,我最后拼出产品。适合创建复杂、多部件的对象。
举个例子:
你去餐厅吃饭。工厂模式就像点一份“套餐”——厨师直接给你端上来,你不需要知道里面有什么。建造者模式就像“自助沙拉”——你拿着盘子,自己选生菜、加番茄、放酱料,每一步都是你自己控制的。
核心区别一句话:工厂模式是“一步到位”,建造者模式是“分步组装”。
我在项目中遇到过这样一个场景:需要构建一个复杂的 SQL 查询条件。如果只用工厂模式,你得为每一种查询组合写一个工厂方法,那代码量就爆炸了。用建造者模式,我可以链式调用:new QueryBuilder().select(...).from(...).where(...).build(),清爽多了。
Lombok @Builder 注解:写代码可以更优雅
说实话,手动写建造者模式挺累的。尤其是那些只有几个字段的简单 POJO,你也要写一个 Builder 内部类,代码量翻倍。
这时候,Lombok 的 @Builder 注解就派上用场了。
你只需要在类上加上 @Builder,Lombok 会自动帮你生成建造者代码。看个例子:
import lombok.Builder;
import lombok.ToString;
@Builder
@ToString
public class User {
private String name;
private Integer age;
private String email;
private String phone;
}
// 使用
User user = User.builder()
.name("张三")
.age(28)
.email("zhangsan@example.com")
.phone("13800138000")
.build();
System.out.println(user);
// 输出: User(name=张三, age=28, email=zhangsan@example.com, phone=13800138000)
你看,代码简洁到令人发指。而且 Lombok 生成的 Builder 是线程安全的,内部类也是静态的,不会造成内存泄漏。
小技巧:如果你想让某些字段有默认值,可以用 @Builder.Default 注解。比如 @Builder.Default private Integer status = 1;,这样不设置 status 时,默认就是 1。
不过要注意一点:@Builder 会生成一个全参的构造函数,如果你同时用了 @NoArgsConstructor 或 @AllArgsConstructor,可能会冲突。我建议你显式加上 @AllArgsConstructor(access = AccessLevel.PACKAGE),让 Lombok 生成的构造器只在包内可见,避免外部直接 new。
建造者模式在 MyBatis 中的应用:SqlSessionFactoryBuilder
MyBatis 是 Java 生态里最流行的 ORM 框架之一。它里面就大量使用了建造者模式。
最典型的就是 SqlSessionFactoryBuilder。你看它的名字,后缀就是 Builder。
它的工作流程是这样的:
- 读取 MyBatis 的配置文件(XML 或 Java 配置)
- 解析配置,构建一个
Configuration对象 - 用
Configuration对象创建SqlSessionFactory
这个过程中,SqlSessionFactoryBuilder 就是指挥者,它控制着“读取配置 -> 解析配置 -> 构建工厂”这个流程。而具体的解析逻辑,由 XMLConfigBuilder、XMLMapperBuilder 等具体建造者完成。
我截取一段 MyBatis 源码的核心逻辑,你感受一下:
// SqlSessionFactoryBuilder 的核心方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 1. 创建 XMLConfigBuilder(具体建造者)
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 2. 解析配置,构建 Configuration 对象
Configuration config = parser.parse();
// 3. 用 Configuration 构建 SqlSessionFactory
return build(config);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// 忽略
}
}
}
// 另一个重载方法
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
你看,SqlSessionFactoryBuilder 并不关心 XMLConfigBuilder 内部怎么解析 XML,它只负责“调用 parse() -> 拿到 Configuration -> 创建 SqlSessionFactory”。这就是典型的指挥者角色。
避坑指南:我曾经在项目中看到有人直接 new 了一个 Configuration 对象,然后手动设置各种属性,最后传给 SqlSessionFactoryBuilder.build(Configuration)。这样做虽然能跑,但绕过了 MyBatis 的配置校验逻辑。一旦配置写错,排查起来非常痛苦。我建议你始终通过 XML 或 Java 配置的方式,让建造者帮你完成构建过程。
除了 SqlSessionFactoryBuilder,MyBatis 里还有很多建造者的影子:
XMLStatementBuilder:解析 Mapper 文件中的 SQL 语句XMLMapperBuilder:解析 Mapper 文件CacheBuilder:构建缓存对象
这些 Builder 类都遵循同一个模式:把复杂的解析和构建逻辑封装起来,对外提供简洁的接口。
嗯,说到这,我想起一个有意思的事。有一次我 review 代码,发现有个同事自己写了一个“万能 Builder”,试图用一个类搞定所有对象的构建。结果那个类里塞满了 if-else,代码超过 500 行。我告诉他:建造者模式的核心是“单一职责”,一个建造者只负责一种产品的构建。强行复用只会让代码变成一锅粥。
所以,我的建议是:不要为了用模式而用模式。当你发现一个对象的构造过程很复杂,或者参数很多,或者构造逻辑经常变化时,再考虑建造者模式。否则,简单的构造函数或者静态工厂方法,反而更清晰。
好了,建造者模式就聊到这。记住它的精髓:分步构建,灵活组合。下次写代码遇到“构造函数爆炸”时,不妨试试它。
一句话总结:建造者模式把“复杂对象的构造”从“使用方”手里解放出来,交给专门的“建造者”去管理。你只管告诉建造者“我要什么”,至于“怎么造”,那是建造者的事。
公众号:蓝海资料掘金营,微信deep3321