拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

建造者模式深入解析:原理、实战与面试要点

建造者模式深入解析:原理、实战与面试要点 说到创建型模式建造者模式Builder Pattern是我在实际项目里用得最频繁的一个。很多人对它有个误区觉得它不过是“写一个 Builder 然后链式调用 setter”甚至认为有 Lombok 的Builder注解就够了完全不需要理解原理。但真实情况是我在代码评审里见过太多人把建造者模式用反了——要么在一个根本不需要复杂构建的场景强行套 Builder搞得代码又长又绕要么在构造器参数已经有七八个的情况下还硬着头皮用构造函数重载最后调用方根本分不清哪个参数是哪个。如果你正准备软考、期末考、设计模式大作业或者准备面试时被问到“建造者模式和工厂模式的区别”这篇文章可以帮你一次性把这些点吃透。我会把建造者模式的演进逻辑、四个角色的标准实现、链式写法、不同语言的落地差异、面试高频题和记忆口诀全部拆开来讲。1. 建造者模式解决什么痛点构造函数参数爆炸与半成品对象1.1 从“汽车配置单”引出最原始的痛点先看一个非常典型的场景。假设你要实现一个电脑实体类它有 CPU、显卡、内存、主板、硬盘、电源、机箱、是否有WiFi模块这些属性。如果用最原始的方式构造这个对象你大概率会写出这样的构造函数public class Computer { private String cpu; private String gpu; private int memorySize; private String motherboard; private String storage; private String powerSupply; private String caseType; private boolean hasWifi; // 构造函数1全参数 public Computer(String cpu, String gpu, int memorySize, String motherboard, String storage, String powerSupply, String caseType, boolean hasWifi) { // 赋值…… } // 构造函数2没有显卡的版本 public Computer(String cpu, int memorySize, String motherboard, String storage, String powerSupply, String caseType, boolean hasWifi) { // 赋值…… } // 还可能有无WiFi版本、无独立显卡版本……无穷无尽 }这段代码的问题是显而易见的。调用处会变成一长串不知道含义的裸参数new Computer( Intel i7-13700K, NVIDIA RTX 4070, 32, Z790, 1TB NVMe SSD, 850W 金牌, ATX 中塔, true );你自己写完的代码过两周回头读还得对着参数列表一个个数更何况是别人来做维护。更麻烦的是一旦属性增加到十几个构造函数的参数列表会膨胀到无法维护的程度而且每个重载组合都是一个排列组合问题。1.2 JavaBean 模式为什么也不是好方案你可能马上会想那我不用构造函数全部用 setter 行不行这就是俗称的 JavaBean 模式Computer computer new Computer(); computer.setCpu(Intel i7-13700K); computer.setGpu(NVIDIA RTX 4070); computer.setMemorySize(32); computer.setMotherboard(Z790); computer.setStorage(1TB NVMe SSD); computer.setPowerSupply(850W 金牌); computer.setCaseType(ATX 中塔); computer.setWifi(true);一套 setter 打下来代码的意图清楚了可读性也恢复了但有一个致命问题对象的创建过程被拆成了多个步骤对象在经过 new 之后但属性还没设置完成之前处于“半成品”状态。在单线程的简单代码里这个问题不明显最多是感觉代码啰嗦。但一旦对象被发布到多线程环境或者你把它交给其他模块使用中间状态的隐患就会暴露。比如一个全局配置对象被线程 A 初始化了一半线程 B 就开始读取读到的可能是默认值也可能直接读到 null 导致空指针。除了线程安全JavaBean 模式也没法保证“必填字段”的完整性——你没有任何机制强制调用方必须设置 CPU 和内存忘记调用了编译器也发现不了只能等到运行期 NPE 来告诉你“我缺东西了”。这里的本质问题是什么是对象的构造过程缺乏约束整个流程没有结构关键信息散落在调用方手里。1.3 建造者模式的本质分清“如何构建”和“如何表示”建造者模式的核心思想就是把一个复杂对象的“构建过程”和它的“最终表示”分离开。用编程术语说相同的构造过程可以创建出不同表示的对象。什么意思还是用电脑来类比。你去店里买电脑店员给你一张配置单CPU 选哪个、显卡选哪个、内存多大、要不要 WiFi 模块、机箱喜欢什么颜色。你做完选择之后店员拿着这张配置单走到后台把零件一个个装起来最后交给你一台完整的电脑。这个场景里你的“选择行为”和“电脑内部怎么组装”是完全解耦的。你不需要知道主板怎么插、电源线怎么接你只需要提供“我要什么”的需求。这就是建造者模式在软件世界里的投影。拆成代码概念就是你要构建的产品对象电脑 Computer。抽象的构建规范接口Builder 接口定义了设置 CPU、设置显卡、设置内存等方法以及最终的 build() 方法。具体的构建者实现ConcreteBuilder负责真正把各个部件拼接成产品。指挥者Director负责编排构建步骤让整个创建流程稳定、有序。很多人学建造者模式的时候被这四个角色绕昏了头。我的经验是先忘掉 Director先把 Builder 链式调用写熟再回来看标准四角色你会发现理解成本直接降低一半。2. 标准实现与角色拆解UML 与 Java 代码全解析2.1 四个核心角色分别负责什么为了照顾正在准备设计模式期末考、软考的朋友我先把标准定义提出来Product产品角色要构建的复杂对象本身包含多个组成部件。这个类通常不负责自己的构建细节只充当数据容器。Builder抽象建造者定义构建产品各部分的统一接口声明设置各部件的方法和一个返回成品的方法。通常是接口或抽象类。ConcreteBuilder具体建造者实现 Builder 接口真实地构造产品的各个部件并负责组装、维护当前正在构建的产品对象。Director指挥者使用 Builder 接口来编排构建过程它决定“先设置 CPU 还是先设置内存”屏蔽了构建细节对调用方的影响。这里有个容易混淆的点很多教材说 Director 是建造者模式里必不可少的部分但在真实项目里Director 恰恰是最先被省略掉的。原因是链式调用本身就能表达构建顺序叠加一个 Director 反而增加一层抽象。这在后面我写链式写法时会详细展开。2.2 一份能直接跑通的 Java 标准实现先写产品类。产品类本身非常简单就是一堆私有字段加上 getter。这里我故意不写 setter强制所有属性只能通过 Builder 设置保证对象一旦构造完成就是完整、不可变的状态public class Computer { private final String cpu; private final String gpu; private final int memorySize; private final String motherboard; private final String storage; private final String powerSupply; private final String caseType; private final boolean hasWifi; private Computer(Builder builder) { this.cpu builder.cpu; this.gpu builder.gpu; this.memorySize builder.memorySize; this.motherboard builder.motherboard; this.storage builder.storage; this.powerSupply builder.powerSupply; this.caseType builder.caseType; this.hasWifi builder.hasWifi; } // getters 省略…… public static class Builder { // 必填字段不设置就不让 build private String cpu; private String motherboard; // 可选字段给默认值 private String gpu 核显; private int memorySize 16; private String storage 512GB SSD; private String powerSupply 450W; private String caseType M-ATX; private boolean hasWifi false; public Builder(String cpu, String motherboard) { this.cpu cpu; this.motherboard motherboard; } public Builder gpu(String gpu) { this.gpu gpu; return this; } public Builder memory(int memorySize) { this.memorySize memorySize; return this; } public Builder storage(String storage) { this.storage storage; return this; } public Builder power(int powerSupply) { this.powerSupply powerSupply; return this; } public Builder caseType(String caseType) { this.caseType caseType; return this; } public Builder wifi() { this.hasWifi true; return this; } public Computer build() { // 这里可以做必填字段校验 if (cpu null || motherboard null) { throw new IllegalStateException(CPU 和主板是必填项); } return new Computer(this); } } }调用方用起来是这个效果Computer gaming new Computer.Builder(Intel i7-13700K, Z790) .gpu(NVIDIA RTX 4070) .memory(32) .storage(1TB NVMe SSD) .power(850W 金牌) .wifi() .build();注意到几个设计细节。第一必填字段直接放进 Builder 的构造函数里从语法层面强制调用方提供第二可选字段都给了合理的默认值调用方可以完全不设置第三build()方法里再兜底做一次校验防止漏配置第四产品类的构造器是私有的外界只能通过 Builder 拿到对象这样对象一旦被构建出来就不会再被外部修改。这几个点正是建造者模式相比 JavaBean 模式最大的优势不可变性 构建过程可控。2.3 为什么要有 setter 返回 this 这种“离谱”写法你可能会觉得public Builder gpu(String gpu) { this.gpu gpu; return this; }这个写法很反直觉一个设置属性的方法居然还要返回对象自己。这正是链式调用的关键设计。只要每个 setter 返回this调用方就可以把多个步骤合并成一条链。每一个方法调用之后得到的是同一个 Builder 对象所以后面的方法还能继续调用。这种风格来自领域特定语言DSL的思路目的就是让代码读起来像自然语言new Computer.Builder(cpu, motherboard) .gpu(...) .memory(...) .storage(...) .build();同样是这个思路你在 Guava、OkHttp、Retrofit 等大量 Java 开源库里都能看到 Builder 的身影。它们之所以选择这种写法是因为对“配置项非常多但大多数都有默认值”的场景来说这是既保证可读性、又保证对象完整性、同时不牺牲不可变性的最优解。2.4 加一个 Director 控制标准流程标准教材里还有个 Director它的职责是封装“构建步骤”。比如“商务办公本”和“游戏主机”两种产品构建流程虽然大体相同但步骤选择不一样。Director 就是把这些流程沉淀成可复用方法的角色public class ComputerDirector { private Builder builder; public ComputerDirector(Builder builder) { this.builder builder; } public Computer buildOfficePC() { return builder.cpu(Intel i3-13100) .gpu(核显) .memory(16) .storage(512GB SSD) .power(300W) .build(); } public Computer buildGamePC() { return builder.cpu(Intel i7-13700K) .gpu(NVIDIA RTX 4070) .memory(32) .storage(1TB NVMe SSD) .power(850W 金牌) .wifi() .build(); } }你看Director 把这些组合逻辑从外部调用方手里收拢到了一处。以后如果公司推出新的机型套餐你只需要在 Director 里加一个方法调用方的代码完全不用变。这种“策略式封装”的价值在业务配置项特别多、套餐组合特别复杂的场景下很突出。不过我也要说句经验之谈如果你只是简单构建一个配置对象没有多种固定模板建议不要硬加 Director。理由有二一是多一层类就多一分理解成本二是模板一旦变得灵活多变Director 里的方法数量会爆炸反而演变成“方法爆炸模式”。3. 链式写法实战去掉 Director 的现代简化版3.1 为什么实际项目中更喜欢链式调用而非 Director打开 GitHub 上任何一个热门的 Java 项目你会看到大量 Builder 用法但很少看到 Director。现代项目里大家更倾向于直接让调用方通过链式方法编排构建过程而不是先把构建模板塞进一个独立的 Director 类里。原因很简单Director 适合“构建流程固定不变”的场景比如组装汽车需要固定的焊接、喷漆、装配顺序。但软件世界里对象的属性很少存在强顺序约束大多是“设置 A、设置 B、设置 C”顺序无所谓。这种情况下 Director 封装的就不是“流程”而只是“一组预定义配置”这种价值用静态工厂方法也能提供。所以我在业务代码里通常只保留两个角色Builder 链 build() 方法。把必填字段校验放在 build() 里把可选字段默认值放在 Builder 字段声明处配合链式方法完全能覆盖 90% 的场景。3.2 Lombok 的 Builder 与手写 Builder 怎么选很多 Java 开发者喜欢直接用 Lombok 的Builder注解一行代码解决繁琐的 Builder 类Builder public class Order { private final String orderNo; private final String userId; private final ListOrderItem items; private final BigDecimal totalAmount; private final String remark; }调用方可以这样构造Order order Order.builder() .orderNo(202501010001) .userId(u_123456) .totalAmount(new BigDecimal(199.00)) .build();这个做法在项目里很常见也的确省下大量样板代码。但你不能只在简历上写“熟悉 Lombok Builder”面试官一问“它怎么实现的和标准 Builder 模式有什么关系”就答不上来。Lombok 的逻辑就是通过注解处理器在编译期替你生成了标准 Builder 模式的内部类代码本质上它就是建造者模式的一种自动化实现。所以我一直建议先用标准手写方式学原理再用 Lombok 提高效率。如果你一上来就用注解很难理解为什么build()之前对象是不可用的。注意一点Lombok 的Builder默认不处理必填字段的强制校验功能它可以让你完全跳过某个字段来 build。所以如果你有“某些字段必须存在”的约束要么手动叠加Builder的build()方法做校验要么回到手写 Builder。我在做设计模式大作业时给老师的代码一定是手写版因为注解版看不出你对原理的理解。3.3 C# 与 C 里的建造者模式怎么落热搜词里同时出现了 C# 和 C说明不少读者在学多种语言。这两个语言实现 Builder 时写法和 Java 有些区别。在 C# 里由于有对象初始化器Object Initializer和命名可选参数很多场景下可以用更简洁的语法替代 Buildervar order new Order { OrderNo 202501010001, UserId u_123456, TotalAmount 199.00m, Remark 加急处理 };C# 的初始化器在处理“可写对象”时非常好用但它和建造者模式有个本质区别初始化器仍然允许对象创建后再被修改也无法集中执行跨字段校验。如果你要的是不可变对象和强制校验C# 照样得写 Builderpublic class Computer { public string Cpu { get; } public string Gpu { get; } public int MemorySize { get; } private Computer(ComputerBuilder builder) { Cpu builder.Cpu; Gpu builder.Gpu; MemorySize builder.MemorySize; } public class ComputerBuilder { public string Cpu { get; private set; } public string Gpu { get; private set; } 核显; public int MemorySize { get; private set; } 16; public ComputerBuilder WithCpu(string cpu) { Cpu cpu; return this; } public ComputerBuilder WithGpu(string gpu) { Gpu gpu; return this; } public ComputerBuilder WithMemory(int size) { MemorySize size; return this; } public Computer Build() { if (string.IsNullOrEmpty(Cpu)) throw new InvalidOperationException(CPU 必填); return new Computer(this); } } }C 这边要注意的是对象生命周期和拷贝开销。Builder 里保存的是各种属性值build()返回产品对象时理想情况是用智能指针或移动语义避免深拷贝。下面是一个简化示例#include string #include memory class Computer { private: std::string cpu_; std::string gpu_; int memory_size_ 16; public: class Builder { private: std::string cpu_; std::string gpu_; int memory_size_ 16; public: Builder withCpu(const std::string cpu) { cpu_ cpu; return *this; } Builder withGpu(const std::string gpu) { gpu_ gpu; return *this; } Builder withMemory(int size) { memory_size_ size; return *this; } std::unique_ptrComputer build() const { if (cpu_.empty()) { throw std::runtime_error(CPU is required); } auto comp std::make_uniqueComputer(); comp-cpu_ cpu_; comp-gpu_ gpu_; comp-memory_size_ memory_size_; return comp; } }; }; auto pc Computer::Builder() .withCpu(Intel i7) .withGpu(NVIDIA RTX 4070) .withMemory(32) .build();C 的 Builder 类也可以定义在产品类的私有嵌套类中这样产品的构造函数可以声明为私有强制只能通过 Builder 创建对象。但要注意友元关系和嵌套类的访问权限处理起来比 Java 的静态内部类要繁琐所以很多 C 项目干脆把 Builder 作为独立类并通过友元访问产品私有构造函数。不管是 Java、C# 还是 C核心思想是一致的把复杂构建过程从构造函数里抽离出来让构建过程可读、可校验、可复用。差别只在语法层。4. 建造者模式与工厂模式的边界面试必问对比4.1 一段记忆口诀帮你区分两种创建型模式设计模式一共有 23 种光是创建型就有 5 种很多人容易搞混。我的记忆方式是抓它们的核心动词简单工厂/工厂方法“你要什么我给你造什么”核心是延迟创建对象调用方只关心获取结果。抽象工厂“我要一整族东西”核心是保证多个产品之间的搭配兼容。建造者“一步一步搭起来”核心是分步构建最终得到完整产品。单例“全局只此一个”核心是控制实例数量。原型“照着模板复制一份”核心是克隆已有对象。建造者模式的速记口诀可以压缩成八个字“分步构建屏蔽装配”。再配合四个角色口诀“指挥定流程抽象定规范具体管装配产品只管存”。这个口诀对软考、期末考和面试都很有用。遇到选择题问你“下列哪个模式属于创建型模式且适用于构建复杂对象”你直接锁定 Builder 就行。4.2 工厂模式创建对象和建造者模式创建的差别面试里最常见的追问是“工厂模式和建造者模式都是创建对象的区别在哪”我用一个极致简单的例子回答。工厂模式就像你去餐厅点菜——你只需要告诉服务员“来一份宫保鸡丁”至于这道菜怎么做、需要哪些食材、下锅顺序怎么安排你完全不需要关心而且你也不会参与到菜品制作里。建造者模式就像你去 DIY 手工坊做蛋糕——从选蛋糕胚、抹奶油、放水果到写祝福语每一层你可以自己决定加还是不加最后按自己的选择组合出一个完整蛋糕。放在代码层面工厂模式通常创建过程是一步到位的一次性返回完整对象调用方不需要知道任何构造细节甚至不需要知道具体产品类型适合“创建逻辑相对固定主要变化在于类型选择”的场景。建造者模式则创建过程是多步的每步设置一个属性或部件调用方需要按需配置决定哪些部件有、哪些部件没有适合“产品结构复杂属性多且有默认值需要校验必填字段”的场景。用一句话总结工厂关注“创建哪个产品”建造者关注“怎样创建这个复杂产品”。工厂模式往往对调用方隐藏创建逻辑建造者模式则把创建逻辑拆开摆在调用方面前。4.3 哪些经典开源库用了建造者模式为了证明建造者模式的价值我来举几个主流开源项目的真实用法。OkHttpRequest对象用Request.Builder构建设置 URL、Header、Method、Body最后调用build()完成校验和生成不可变对象。RetrofitRetrofit.Builder()用来配置 baseUrl、callFactory、converterFactory 等典型的多配置项场景。Java 的 StringBuilder严格说它属于“建造者模式的变体”在字符串构建上的应用通过append()分步添加内容最后toString()得到结果。Lombok Builder编译期自动生成 Builder 内部类。Spring 的 BeanDefinitionBuilder用于编程式定义 Bean 的属性。这些库的共同特点是要么对象属性很多要么创建过程需要用户按需配置。如果你去读它们的源码会发现结构上都是标准的 Builder 模式。5. 实际项目中的完整实战从需求分析到大作业级代码5.1 一个订单场景的建模分析与代码落地为了让你有一套能直接拿去交设计模式大作业的代码我设计一个更贴近业务需求的完整案例电商订单构建。订单的特点非常契合建造者模式字段多、有必填项、有可选项、有金额计算、还有状态控制。先定义订单本体的字段必填订单号、用户ID、商品明细列表可选优惠券 ID、配送地址、发票类型、订单备注计算属性总金额、应付金额、运费订单对象要求一旦构建完成就不能修改同时必须通过 Builder 完成金额校验和天然校验。下面是简化版实现import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; public class Order { private final String orderNo; private final String userId; private final ListOrderItem items; private final String couponId; private final String shippingAddress; private final String invoiceType; private final String remark; private final BigDecimal totalAmount; private final BigDecimal freight; private final BigDecimal payableAmount; private Order(Builder builder) { this.orderNo builder.orderNo; this.userId builder.userId; this.items builder.items; this.couponId builder.couponId; this.shippingAddress builder.shippingAddress; this.invoiceType builder.invoiceType; this.remark builder.remark; this.totalAmount builder.totalAmount; this.freight builder.freight; this.payableAmount builder.payableAmount; } public static class Builder { private final String orderNo; private final String userId; private final ListOrderItem items new ArrayList(); private String couponId 无; private String shippingAddress 自提; private String invoiceType 电子普通发票; private String remark ; private BigDecimal freight BigDecimal.ZERO; private BigDecimal totalAmount BigDecimal.ZERO; private BigDecimal payableAmount BigDecimal.ZERO; public Builder(String orderNo, String userId) { this.orderNo orderNo; this.userId userId; } public Builder addItem(OrderItem item) { items.add(item); totalAmount totalAmount.add(item.getPrice().multiply(BigDecimal.valueOf(item.getCount()))); payableAmount totalAmount.add(freight); return this; } public Builder coupon(String couponId) { this.couponId couponId; return this; } public Builder address(String shippingAddress) { this.shippingAddress shippingAddress; return this; } public Builder invoice(String invoiceType) { this.invoiceType invoiceType; return this; } public Builder remark(String remark) { this.remark remark; return this; } public Builder freight(BigDecimal freight) { this.freight freight; this.payableAmount this.totalAmount.add(freight); return this; } public Order build() { if (items.isEmpty()) { throw new IllegalStateException(订单至少需要一件商品); } if (payableAmount.compareTo(BigDecimal.ZERO) 0) { throw new IllegalStateException(应付金额不能为负数); } return new Order(this); } } public static class OrderItem { private final String skuId; private final String name; private final BigDecimal price; private final int count; public OrderItem(String skuId, String name, BigDecimal price, int count) { this.skuId skuId; this.name name; this.price price; this.count count; } public BigDecimal getPrice() { return price; } public int getCount() { return count; } } }调用方写起来非常接近自然语言Order order new Order.Builder(202501010001, u_888) .addItem(new Order.OrderItem(s_1001, 机械键盘, new BigDecimal(499.00), 1)) .addItem(new Order.OrderItem(s_1002, 鼠标垫, new BigDecimal(39.90), 2)) .coupon(COUPON_100) .address(北京市朝阳区XX大厦) .invoice(增值税专用发票) .remark(请放前台) .freight(new BigDecimal(10.00)) .build();这段代码里最值得品味的是addItem的妙处每添加一件商品totalAmount和payableAmount就同步更新最终build()之前金额已经算好了不需要另外写一遍累加逻辑。这就是建造者模式“分步构建”对业务逻辑封装带来的实际好处——你可以在构建过程中夹带计算、校验、关联逻辑而调用方完全感知不到。5.2 不可变对象与线程安全性为什么重要很多人忽略一个问题为什么建造者模式倾向于生成不可变对象如果产品类全是 final 字段且没有 setter一旦build()返回对象状态就固定了。这意味着你可以放心地把同一对象传给多个线程不需要加锁不会出现某个线程改了字段导致其他线程行为异常。像订单、配置、请求参数这种对象天然就应该是不可变的——正常业务里不会有人建了订单之后再去改订单号。相比之下JavaBean 模式里对象创建之后仍然可以被任意调用setXxx()这种“可变性”在并发场景里是隐患在代码维护上也是一种负担你永远要检查是不是有某个角落偷偷改了对象状态。建造者模式配合不可变对象等于把“创建流程”和“使用流程”彻底隔离开。想创建不同配置的对象就重新走一遍 Builder一旦创建完成所有线程看到的都是稳定状态。5.3 嵌套 Builder 与指导式构建应对更复杂的产品结构如果你的产品内部还有子产品比如订单下面有收货人信息、商品、物流信息等多个子对象可以考虑在 Builder 里嵌套另一些 Builder 或使用一步到位的子构建方法。比如上面的addItem接收的是OrderItem对象外部可以给OrderItem也配一个 Builder让构建逻辑进一步下沉Order.OrderItem item new Order.OrderItem.Builder(s_1001, 机械键盘) .price(new BigDecimal(499.00)) .count(1) .build();这种嵌套 Builder 的做法在大型领域模型中非常实用比如配置中心、报表查询条件、搜索请求。每个子模块都有自己的构建规则顶层 Builder 只负责把这些子模块组合起来符合“高内聚低耦合”的设计原则。缺点是类数量会明显增加所以只有当子对象本身也足够复杂时才值得这样建模。6. 常见问题排查与避坑经验速查表6.1 面试题和期末考高频问法我把常见的坑和问法整理成一张表方便你集中复盘问题参考答案要点容易踩的坑建造者模式和工厂模式的区别工厂关注创建哪种产品建造者关注产品如何分步构建答成“建造者只是工厂的别名”建造者模式解决了什么问题构造函数参数多、JavaBean 半成品状态、对象不可变性要求只答“构造方法参数多”遗漏不可变性和校验为什么 build() 前要有校验保证对象一旦构建完成就是完整合法的避免运行期错误完全不做校验和 JavaBean 没区别Builder 的 setter 为什么要返回 this为了链式调用形成 DSL 风格写成 void导致无法链式调用建造者模式一定需要 Director 吗标准定义里有但实际项目常省略Director 适合固定流程模板照本宣科说必须有Lombok Builder 是建造者模式吗是编译期自动生成 Builder 代码的注解实现只会用注解不会手写这些问题我在面试里问过很多人也被人问过很多次。答得好的关键不是背诵定义而是能现场画出角色关系、写出一段像样的链式调用再结合实际项目说清楚“什么时候我不会用建造者模式”—— 能讲清楚边界的人才是真正理解设计模式的人。6.2 我实际踩过的坑与排查经验第一坑把 Builder 的字段设计成可变类型还共享引用。比如订单里有一个List字段如果你在 Builder 里直接让多个 Builder 共享一个 List 实例那么容易出现“构建 A 时给列表加了一个元素结果 B 也看到了”。安全的做法是构建时拷贝一份或者在build()方法里重新new ArrayList(builder.items)防止外部引用穿透。第二坑必填校验放得太晚或太早。放太晚调用方使用对象时才报错失去了 Builder 的意义放太早比如构造函数一开始就校验又没法处理链式调用过程中“字段还没设完”的情况。正确位置是在build()里统一校验因为这时该设置的全部设置完了。第三坑为不可变而不可变过度设计。某些 CRUD 场景里一个对象创建后确实可能需要修改状态比如草稿、发布、归档这种状态流转。如果不分青红皂白一律 Builder 全 final 字段反而会导致代码别扭状态更新得靠重建对象完成。设计模式是为了解决问题不是为了让代码显得“高级”。第四坑在不需要参数校验、字段不超过四五个时硬套 Builder。三个参数的对象直接用构造函数就很好代码更短可读性也不差。经验准则是字段在 6 个以上、或有必填和可选之分、或构建过程伴随计算/校验时才应当考虑建造者模式。6.3 关键细节自查清单最后分享我的一个实操习惯写建造者模式时我会过一遍自查清单确保没有漏掉关键细节[ ] 产品的构造函数是否为私有是否只有 Builder 能创建[ ] 字段是否设为 final且不提供任何 setter[ ] Builder 的必填字段是否放进了构造函数参数中[ ] 可选字段是否有合理的默认值[ ]build()里是否做了完整的合法性和必填性校验[ ] 返回 this 的方法是否都返回值类型为 Builder 本身[ ] 是否需要防御性拷贝避免外部修改内部集合或数组[ ] 是否真的是复杂对象的构建场景如果是简单对象会不会用构造函数更直接这份清单是我在代码评审时检查别人 Builder 实现的标准照着这个写代码一般不会跑偏。以我个人的使用体验来说建造者模式是少数几种“学了立刻就能用、用了立刻能感受到好处”的设计模式。它不像抽象工厂那样需要一定的架构铺垫也不需要观察者模式那样的事件基础设施只要面临复杂对象构建Builder 就能直接站出来把代码理清楚。刚开始可能觉得写一个 Builder 类很麻烦但等你调用方用起来的时候那种“设置项一目了然、必填项不会被忘”的安心感会让你觉得多花的这几行代码完全值得。支持多语言实现、面试爱考、项目常用这也是它长期稳居热门设计模式榜首的原因。
返回列表