少女祈祷中...
残云的学习笔记

建造者模式:从 GoF 经典到流式 Builder,以及什么时候不该用它

建造者(Builder)是创建型模式里"看起来最简单、实际最容易用错"的一个。它有两种截然不同的形态——GoF 经典建造者(关注"分步装配、同一过程产出不同表示")和流式建造者(关注"把一堆可选参数说得清楚"),工程里 90% 的 XxxBuilder 其实是后者。这篇文章把两种形态讲清、给出可运行代码,并说明为什么《Effective Java》只在参数 ≥ 4 个时才推荐它。

一、问题的起点:构造器参数爆炸

Java 没有具名参数和默认参数,于是当一个对象有"2 个必填 + 3 个可选"字段时,最朴素的写法会变成这样:

import java.util.List;

/** 伸缩式构造器(telescoping constructor):能跑,但没人愿意读 */
public class TelescopingConstructorDemo {

    static class Order {
        private final String id;          // 必填
        private final String customer;    // 必填
        private final String coupon;      // 可选
        private final boolean giftWrap;   // 可选
        private final String remark;      // 可选

        Order(String id, String customer) {
            this(id, customer, null);
        }
        Order(String id, String customer, String coupon) {
            this(id, customer, coupon, false);
        }
        Order(String id, String customer, String coupon, boolean giftWrap) {
            this(id, customer, coupon, giftWrap, null);
        }
        Order(String id, String customer, String coupon, boolean giftWrap, String remark) {
            this.id = id;
            this.customer = customer;
            this.coupon = coupon;
            this.giftWrap = giftWrap;
            this.remark = remark;
        }
    }

    public static void main(String[] args) {
        // 每个参数是什么意思?只能靠数位置,而且 true 是"礼品包装"还是"加急"?
        Order order = new Order("A-1", "张三", null, true, null);
        System.out.println(order.id);
    }
}

三个后果:

  1. 调用处不可读:new Order("A-1", "张三", null, true, null) 里的 null/true 是什么?必须翻回去看构造器签名。
  2. 重载组合失控:可选参数越多,重载构造器数量越接近指数增长。真要写全,需要 2ⁿ 个构造器。
  3. 或者退化成 setter:一旦改用无参构造 + setXxx(),对象就有了"半成品状态"——谁也不敢保证 submit() 拿到的 order 字段是齐的;同时还牺牲了不可变性。

建造者模式正是为这三件事而生。注意它的价值不在"多一层",而在"让可选参数有名字、让对象只在构建完成后才存在"。

二、两种"建造者"

同一个词,工程里指两种不同的东西,先把它们分开:

形态关注点典型形态出处
经典 GoF 建造者分步装配的过程;同样的构建过程可以产出不同表示Director + 抽象 Builder + 多个 ConcreteBuilderGoF《设计模式》
流式建造者(Fluent Builder)参数的可读性与对象的不变性;解决构造器参数过多Order.builder(...).item(...).build()《Effective Java》Item 2

两者的共同点是"把构建过程拆成若干步",区别在于为什么要拆:前者是为了让"过程"可复用(换个 ConcreteBuilder 就换一种产出),后者是为了让"参数"能起名字。混着理解是很多文章讲不清建造者的根源。

三、经典 GoF 建造者:同一过程,不同表示

经典场景是"文档导出":报表结构固定(标题 → 若干章节 → 页脚),但输出格式可能是 Markdown,也可能是 HTML。结构(过程)不变,表示(产物)可变。

import java.util.ArrayList;
import java.util.List;

/** 经典 GoF 建造者:分离"构建过程"与"表示" */
public class ClassicBuilderDemo {

    /** Product:被构建的复杂对象 */
    static class Document {
        private final List<String> lines = new ArrayList<>();

        void addLine(String line) {
            lines.add(line);
        }

        String render() {
            return String.join("\n", lines);
        }
    }

    /** Builder:只声明"每一步做什么",不关心最终长什么样 */
    abstract static class DocumentBuilder {
        protected final Document doc = new Document();

        Document result() {
            return doc;
        }

        abstract void buildTitle(String title);
        abstract void buildSection(String heading, String body);
        abstract void buildFooter(String footer);
    }

    /** ConcreteBuilder A:Markdown 表示 */
    static class MarkdownBuilder extends DocumentBuilder {
        void buildTitle(String title) { doc.addLine("# " + title); }
        void buildSection(String heading, String body) {
            doc.addLine("## " + heading);
            doc.addLine(body);
        }
        void buildFooter(String footer) { doc.addLine("---\n" + footer); }
    }

    /** ConcreteBuilder B:HTML 表示 */
    static class HtmlBuilder extends DocumentBuilder {
        void buildTitle(String title) { doc.addLine("<h1>" + title + "</h1>"); }
        void buildSection(String heading, String body) {
            doc.addLine("<h2>" + heading + "</h2>");
            doc.addLine("<p>" + body + "</p>");
        }
        void buildFooter(String footer) { doc.addLine("<hr/><small>" + footer + "</small>"); }
    }

    /** Director:封装"固定的构建过程",它只知道 Builder 抽象 */
    static class ReportDirector {
        Document build(DocumentBuilder builder) {
            builder.buildTitle("季度经营简报");
            builder.buildSection("营收", "同比增长 12%");
            builder.buildSection("风险", "供应链交付周期延长");
            builder.buildFooter("机密 · 内部使用");
            return builder.result();
        }
    }

    public static void main(String[] args) {
        ReportDirector director = new ReportDirector();

        System.out.println("=== Markdown 表示 ===");
        System.out.println(director.build(new MarkdownBuilder()).render());

        System.out.println("\n=== HTML 表示 ===");
        System.out.println(director.build(new HtmlBuilder()).render());
    }
}

builder-1-classic-builder

四个角色对应得很清楚:

  • Product(产品):Document,被构建的复杂对象;
  • Builder(抽象建造者):DocumentBuilder,声明构建的每一步;
  • ConcreteBuilder(具体建造者):MarkdownBuilder / HtmlBuilder,决定每一步"长什么样",并负责最终产出;
  • Director(指挥者):ReportDirector,固定构建顺序,且只依赖抽象。

关键在于 Director 完全不知道产物格式——它调用的三个方法名是抽象的。这就是"同样的构建过程可以创建不同的表示"。

builder-2-sequence

什么时候真的需要 Director?当"构建步骤的顺序"本身就是业务规则(先校验、再装配、最后封口)。如果只有一个 ConcreteBuilder、且步骤调用方自己很清楚,Director 就是多余的一层——这也是经典建造者被诟病"过重"的原因。

四、流式建造者:工程里最常见的形态

日常写代码时遇到的 XxxBuilder,几乎都是下面这种:私有构造器 + 静态 builder() + 链式方法 + build() 一次性校验。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/** 流式建造者:让可选参数有名字,让对象构造完成后就不可变 */
public class FluentBuilderDemo {

    static final class Order {
        private final String id;            // 必填
        private final String customer;      // 必填
        private final List<String> items;   // 必填(至少一件)
        private final String coupon;        // 可选,默认 ""
        private final boolean giftWrap;     // 可选,默认 false

        /** 私有:外部只能通过 Builder 创建,构造器同时承担"不变量校验" */
        private Order(Builder b) {
            if (b.id == null || b.id.isBlank()) {
                throw new IllegalStateException("id 不能为空");
            }
            if (b.items.isEmpty()) {
                throw new IllegalStateException("订单至少需要一件商品");
            }
            this.id = b.id;
            this.customer = b.customer;
            this.items = List.copyOf(b.items);   // 防御性拷贝 + 不可变视图
            this.coupon = b.coupon;
            this.giftWrap = b.giftWrap;
        }

        /** 静态入口:必填参数在这里收,可选参数交给链式方法 */
        static Builder builder(String id, String customer) {
            return new Builder(id, customer);
        }

        List<String> items() {
            return items;
        }

        @Override
        public String toString() {
            return "Order{id=" + id + ", customer=" + customer + ", items=" + items
                    + ", coupon='" + coupon + "', giftWrap=" + giftWrap + "}";
        }

        static final class Builder {
            private final String id;            // 必填:创建 Builder 时就固定
            private final String customer;
            private final List<String> items = new ArrayList<>();
            private String coupon = "";          // 可选:给默认值
            private boolean giftWrap = false;

            private Builder(String id, String customer) {
                this.id = id;
                this.customer = customer;
            }

            Builder item(String name) {
                items.add(name);
                return this;
            }

            Builder coupon(String code) {
                this.coupon = code == null ? "" : code;
                return this;
            }

            Builder giftWrap() {
                this.giftWrap = true;
                return this;
            }

            Order build() {
                return new Order(this);          // 校验与拷贝都发生在这一步
            }
        }
    }

    public static void main(String[] args) {
        Order o1 = Order.builder("A-1", "张三")
                .item("机械键盘")
                .item("显示器")
                .giftWrap()
                .build();
        System.out.println(o1);

        Order o2 = Order.builder("A-2", "李四").item("鼠标").coupon("SUMMER").build();
        System.out.println(o2);

        try {
            Order.builder("A-3", "王五").build();     // 忘了加商品
        } catch (IllegalStateException e) {
            System.out.println("构建失败:" + e.getMessage());
        }
    }
}

builder-3-fluent-builder

这套写法解决了第一节的三个问题:

  • 可选参数有名字:.giftWrap() 比 new Order(..., true, null) 好读得多;
  • 对象只在 build() 之后才存在,不存在"半成品"状态;
  • 最终对象字段全 final、集合不可变,天然线程安全、可放心共享。

五个值得坚持的细节

  1. 必填参数放在 builder(...) 或 Builder 构造器里——用类型系统表达"必填",而不是靠 build() 里抛异常。
  2. 可选参数给默认值,别用 null 占位(coupon 默认 "" 而不是 null)。
  3. 校验集中在 build()(或私有构造器):items 非空、金额非负、时间区间合法——一处集中,比散落在 setter 里可靠。
  4. build() 里做防御性拷贝:List.copyOf(b.items) 既复制又不可变,防止调用方拿着 builder 继续改。
  5. Builder 不要复用、不要跨线程共享:builder 自己是可变的,build() 之后继续 item(...) 会让"下一次构建"带上上一次的残留。

五、建造者 vs 工厂 vs 原型:三者到底差在哪

三个创建型模式都解决"怎么得到对象",但关注的焦点完全不同

builder-4-focus-compare

维度工厂方法 / 抽象工厂建造者原型
关注点创建哪个类(一个产品族)怎么一步步装配复制哪个已有实例
过程一次调用拿到成品分多步,最后 build()一次 copy()
交付物直接是可用对象复杂对象的不同表示原型的副本
组合关系一族产品要配套常与组合模式一起用(构建树)与注册表/缓存搭配
典型代码PaymentFactory.create("alipay")Order.builder(...).item(...).build()prototype.copy()

一句话记忆:工厂选"造哪个",建造者管"怎么造",原型管"照着谁造"。

六、现代 Java 里的替代方案

建造者不是唯一的答案,很多场景下有更轻的写法:

方案适用代价
构造器参数 ≤ 3 个、必填为主参数一多就不可读
静态工厂 + 具名方法需要表达"哪种构造方式",如 Order.giftOrder(id, customer)变体多了方法会膨胀
record + wither不可变数据载体;字段不多每次"改一个字段"都要新建对象
流式建造者参数 ≥ 4 个、大量可选参数、需要集中校验多写一个类;builder 本身可变
Kotlin / Scala 的命名参数 + 默认值语言层面已解决该问题Java 没有

record + wither 的写法值得单独看一眼:

/** Java 16+:record 天然不可变,紧凑构造器负责校验,wither 负责"改一点" */
public class RecordWitherDemo {

    record Point(int x, int y, String label) {
        Point {                                   // 紧凑构造器:集中校验
            if (label == null) label = "";
        }
        Point withX(int newX) {                   // wither:返回新对象
            return new Point(newX, y, label);
        }
    }

    public static void main(String[] args) {
        Point p = new Point(1, 2, "起点");
        System.out.println(p.withX(10));          // Point[x=10, y=2, label=起点]
    }
}

关于 Lombok @Builder:它能省掉手写 builder 的样板代码,但有两个坑需要知道——

  1. @Builder 生成的 builder 不做业务校验:items 为空、金额为负都能构建成功;需要自己加 @Builder(buildMethodName = "buildInternal") 再包一层校验,或把校验放进私有构造器。
  2. @Builder.Default 的行为容易踩:字段直接初始化 + @Builder 时,不写 @Builder.Default 会让默认值在 builder 路径下失效(变成 null/0)。

结论:@Builder 适合"纯数据载体",一旦对象有业务不变量,校验必须自己补。

七、选型:什么时候用哪一种

builder-5-selection

判断口诀:参数少就别用;只有一种表示就用流式建造者;要有多种表示、且构建顺序是业务规则,才上 Director。

八、什么时候不要用建造者

  • 参数只有 2~3 个:new Point(1, 2) 比 Point.builder().x(1).y(2).build() 清楚一百倍。《Effective Java》给的门槛是 4 个参数。
  • 对象本来就该是简单值:上建造者只会让调用链变长、调试变难(断点要跨好几个栈帧)。
  • 真正需要的是工厂:如果你纠结的是"该 new 哪个子类",那是工厂模式的活儿。
  • 真正需要的是原型:如果对象来自"复制一份现成的再微调",用原型/拷贝构造器更直接。
  • 只是为了"看起来高级":给每个实体类都配 Builder,是典型的样板代码膨胀——尤其在用了 record、或字段本来就少的情况下。

九、参考资料

  • GoF《设计模式:可复用面向对象软件的基础》——Builder 章(意图、角色、协作、适用性)
  • Joshua Bloch《Effective Java》Item 2:遇到多个构造器参数时要考虑使用构建器(4 参数门槛、与伸缩式构造器的对比)
  • 《Effective Java》Item 50:必要时做防御性拷贝(build() 里 List.copyOf 的原因,也对应上一篇文章的原型深拷贝话题)
  • JDK 中的建造者:StringBuilder、java.net.http.HttpRequest.Builder、java.util.stream.Stream.Builder
分享到

评论