你写代码这么多年,有没有遇到过这种对象:构造函数十几个参数、一半可以填一半可以不填、字段之间还有依赖关系,每次 new 的时候对着 IDE 的提示数半天参数顺序?我有段时间只要看到超过五个参数的构造方法就头疼,直到真正把建造者模式(Builder Pattern)用熟了,才发现之前很多"设计得太冗余"的类,其实换一种创建方式就能瞬间清爽。
这篇文章,我想把建造者模式从头到脚扯清楚。不止是理论上的"将一个复杂对象的构建与表示分离",更重要的是:这个模式到底解决什么真实的研发痛点、写代码的时候怎么一步一步落地、有哪些必须踩过才知道的坑,以及为什么 StringBuilder、AlertDialog.Builder、Retrofit 这些你每天都在用的东西,骨子里全是它的影子。
如果你是正在准备设计模式期末、或者在做 Java 设计模式相关大作业,又或者已经在项目里写了不少业务代码但总觉得实体类创建很别扭,这篇文章应该刚好对你胃口。
1. 构造函数参数爆炸:Builder 到底在解决什么问题
1.1 从一个真实到让人头疼的订单类说起
很多教材讲设计模式喜欢一上来就画类图,但我觉得得先让你看到一个"没有用建造者模式时,代码会烂成什么样"的场景,你才能真正理解这个模式的价值。
假设你在写一个电商系统的订单实体。订单这个对象天生就复杂:订单号、用户 ID、商品列表、收货地址、优惠券信息、支付方式、配送时间、买家备注、卖家备注、发票信息……而且很多字段不是必填的。你很可能写出下面这种构造方法:
java复制public class Order {
private String orderId;
private long userId;
private List<Item> items;
private String shippingAddress;
private String couponId;
private String paymentMethod;
private Date deliveryTime;
private String buyerRemark;
private String sellerRemark;
private boolean needInvoice;
private String invoiceTitle;
public Order(String orderId, long userId, List<Item> items, String shippingAddress,
String couponId, String paymentMethod, Date deliveryTime,
String buyerRemark, String sellerRemark, boolean needInvoice,
String invoiceTitle) {
this.orderId = orderId;
this.userId = userId;
this.items = items;
this.shippingAddress = shippingAddress;
this.couponId = couponId;
this.paymentMethod = paymentMethod;
this.deliveryTime = deliveryTime;
this.buyerRemark = buyerRemark;
this.sellerRemark = sellerRemark;
this.needInvoice = needInvoice;
this.invoiceTitle = invoiceTitle;
}
}
这段代码有问题吗?语法上完全没问题,代码审查的时候也能通过。但等你真正去调用的时候,痛苦就来了。你在业务层创建订单,可能只想设置订单号、用户 ID、商品列表,其他全都用默认值。可是构造函数要求你把 11 个参数全部传进去,哪怕你传 null、传 0、传 false,你也得一个个占住位置。
更要命的是哪些字段是必填的,哪些是选填的,构造函数完全没法表达。你只能靠读代码、靠注释、靠同事口口相传去理解。要是哪天参数顺序调整了,或者中间插入一个参数,所有调用方全都编译报错。这种代码写多了,你会觉得每次创建对象都像在做一次小心翼翼的排雷。
1.2 为什么重载构造方法也是一种妥协
有人说,参数多那就重载呗,写几个精简版本的构造方法不就行了。听起来有道理,但你试过就知道,重载构造方法其实是另一种灾难。
比如你写一个 3 参数的构造方法(orderId, userId, items),再写一个 5 参数的(orderId, userId, items, shippingAddress, paymentMethod),再写一个 6 参数的……你会发现参数列表越来越长,而且经常出现类型相同、含义不同的参数。两个 String 类型的相邻参数,到底谁是谁,人脑根本记不住。调用的时候一不留神就把 couponId 和 buyerRemark 传反了,编译能过,运行结果错得莫名其妙。
JavaBean 的 setter 模式倒是能解决参数可读性的问题,先 new 一个空对象,然后一个一个 set。但这样会带来一个新的问题:一个对象的创建过程被强行切成了很多步,而且字段之间的约束关系很难保证。比如某些字段相互依赖,或者在对象刚创建出来的那一刻就必须处于合法状态,你用 setter 就很难控制。总不能在每一个 setter 里面校验别的字段是不是已经设置好了吧?那样逻辑会散得到处都是。
建造者模式走的是另一条路:把对象的构建过程封装到独立的 Builder 里面,用链式调用一步步设置参数,最后统一调用 build() 完成创建和校验。这样既保留了 setter 方式的清晰可读,又能在 build() 里面完成完整性校验和不可变对象的构建,可以说是兼顾了两种方案的长处。
1.3 Builder 模式的适用边界:什么情况下才值得用
需要说明的是,建造者模式并不是什么场景都用。如果有一天你去面试,面试官让你说说 Builder 的优缺点,你如果回答"它能让代码更好看",那说明你还没有真正理解这个模式的适用边界。
从我个人的经验来看,建造者模式真正适用的场景有几个明确特征:首先是对象字段足够多,起码五六个以上,而且有不少选填项;其次是对象的创建过程有步骤、有顺序,或者字段之间存在依赖关系;最后是你希望对象创建出来之后就是完整的、不可变的,而不是先 new 出来再靠 setter 一点一点填。
反过来,如果类只有两三个必填字段,那你直接用 constructor 就行,甚至可以考虑用简单的静态工厂方法命名一下意图,比如 createDefaultUser()。硬生生套 Builder,反而会引入不必要的代码量,让简单的事情变复杂。很多初学者学完一个模式之后容易走火入魔,看到什么类都想给它配一个 Builder,这个毛病得戒。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 建造者模式的骨架拆解:四个角色的职责必须分清楚
2.1 Product、Builder、ConcreteBuilder、Director 的标准结构
如果你去翻经典的《设计模式》那本书,建造者模式的标准类图里有四个角色:Product(产品)、Builder(抽象建造者)、ConcreteBuilder(具体建造者)、Director(指挥者)。我在学习的时候最大的困惑是:这个 Director 到底有什么用?写代码的时候真的要定义一套这么完整的接口吗?
先把这个经典结构梳理清楚。Product 就是你要构建的那个复杂对象,比如订单、电脑、套餐。Builder 是一个接口(或者抽象类),里面定义了一系列构建部件的方法,比如 buildMainBoard()、buildCpu()、buildMemory()。ConcreteBuilder 是 Builder 接口的实现,具体实现每个部件的构建逻辑,同时它还得维护一个被构建产品的引用。Director 则负责编排构建顺序,决定先调哪个方法后调哪个方法。
在这套经典结构中,客户端不直接跟 ConcreteBuilder 打交道,而是告诉 Director "我要一个游戏本",Director 就知道应该依次调用 `bui
