← 返回题目列表

建造者模式和工厂模式有什么区别?

高频 中等 第 3 / 26 题 更新于 2026/07/28
建造者模式工厂模式创建型模式设计模式对比

简化版

工厂模式关注“创建哪一种对象”,建造者模式关注“如何一步步创建一个复杂对象”。如果对象类型选择是核心问题,用工厂;如果同一个对象的构建参数和构建过程很复杂,用建造者。

详细版

两者都属于创建型模式,但解决的问题不同。

工厂模式的重点是封装对象实例化,让客户端不直接依赖具体类。例如根据支付类型创建 AliPayWechatPayPaypalPay

建造者模式的重点是封装复杂对象的构建过程。例如创建一个 HttpClientConfig,需要设置地址、连接数、超时、重试、代理、证书、拦截器等。

可以从几个维度对比:

维度工厂模式建造者模式
关注点创建哪类对象如何构建复杂对象
产品数量通常有多个具体产品通常围绕一个复杂产品
客户端输入类型、枚举、条件多个配置步骤或参数
返回对象时机调用工厂方法后立即返回多步设置后 build() 返回
典型问题消除 new 和具体类依赖消除长构造器和构建规则分散

例子:

Payment payment = PaymentFactory.create("wechat");

这是工厂模式,重点是选择微信支付这种产品。

HttpRequest request = HttpRequest.builder()
        .url("/orders")
        .method("POST")
        .timeoutMs(5000)
        .build();

这是建造者模式,重点是把一个请求对象配置完整。

它们也可以一起使用。例如工厂负责选择不同的 Builder,Builder 再负责构建复杂对象。

完整版教学

一、先看两个模式的出发点

工厂模式通常出现在“客户端不想知道具体类”的场景。比如业务里有多种通知渠道:

Notifier notifier = NotifierFactory.create(channel);
notifier.send(message);

调用方只关心 Notifier 接口,不关心返回的是短信、邮件还是企业微信。

建造者模式通常出现在“客户端知道要创建什么,但创建参数太复杂”的场景。比如客户端明确要创建一个导出任务:

ExportJob job = ExportJob.builder()
        .format("xlsx")
        .includeHeader(true)
        .pageSize(1000)
        .compress(true)
        .build();

这里不是在多个产品类型里做选择,而是在把同一个产品构造完整。

二、为什么很多人会混淆

它们都在“创建对象”,所以容易混。判断时不要看有没有 new,而要看变化点在哪里。

如果变化点是“具体类会变”,比如不同支付方式、不同数据库连接、不同消息队列客户端,那更像工厂模式。

如果变化点是“构建参数和构建步骤会变”,比如同一个请求对象有很多字段组合,那更像建造者模式。

一个简单判断方法:

  • 问题是“我要哪个产品?”——偏工厂;
  • 问题是“这个产品怎么配完整?”——偏建造者;
  • 两个问题都有——可以组合使用。

三、用代码看差异

工厂模式通常长这样:

public class ParserFactory {
    public static Parser create(String type) {
        if ("json".equals(type)) {
            return new JsonParser();
        }
        if ("xml".equals(type)) {
            return new XmlParser();
        }
        throw new IllegalArgumentException("unsupported type");
    }
}

客户端只传一个类型,工厂决定具体实现类。

建造者模式通常长这样:

public class ParserConfig {
    private final boolean ignoreUnknownFields;
    private final int maxDepth;
    private final String dateFormat;

    private ParserConfig(Builder builder) {
        this.ignoreUnknownFields = builder.ignoreUnknownFields;
        this.maxDepth = builder.maxDepth;
        this.dateFormat = builder.dateFormat;
    }

    public static class Builder {
        private boolean ignoreUnknownFields = true;
        private int maxDepth = 64;
        private String dateFormat = "yyyy-MM-dd";

        public Builder ignoreUnknownFields(boolean value) {
            this.ignoreUnknownFields = value;
            return this;
        }

        public Builder maxDepth(int value) {
            this.maxDepth = value;
            return this;
        }

        public Builder dateFormat(String value) {
            this.dateFormat = value;
            return this;
        }

        public ParserConfig build() {
            if (maxDepth <= 0) {
                throw new IllegalArgumentException("maxDepth must be positive");
            }
            return new ParserConfig(this);
        }
    }
}

这里重点不是选 JsonParser 还是 XmlParser,而是配置对象怎么安全地构造出来。

四、它们能不能一起用

可以,而且在真实项目里很常见。

比如不同云厂商都有复杂客户端配置:

CloudClientBuilder builder = CloudClientBuilderFactory.create(vendor);

CloudClient client = builder
        .endpoint(endpoint)
        .accessKey(accessKey)
        .secretKey(secretKey)
        .timeoutMs(5000)
        .build();

这里工厂解决“选哪个厂商的 Builder”,建造者解决“这个厂商客户端怎么配置”。

这类组合特别适合 SDK、基础设施组件、平台适配层。面试里能讲出组合使用,说明你不是在背模式名,而是真的理解变化点。

五、同一 HTTP Client 中先选择实现,再分步配置

工厂可以根据协议选择 NettyClient 或 JdkClient;选定实现后,两者都可能需要 host、timeout、TLS、代理等复杂配置。Factory 回答“造哪一种”,Builder 回答“这一种怎样收集参数并合法组装”,两者可前后衔接。

ClientFactory 根据 engine=netty 选择产品类型
返回 NettyClient.Builder 或调用具体 Builder
builder.host("api.example.com")
builder.timeoutMs(3000)
builder.tls(certificate)
build(): 校验 TLS 与证书组合
build(): 创建连接配置快照
new NettyClient(config)
Factory 返回 HttpClient 抽象
业务调用 send(request)

这条时间线把“可变构建阶段”和“稳定成品阶段”分开。Builder 可以反复接收参数,但 Product 只有在所有规则通过后才出现;若 Product 在第一步就被 new 出来再逐项修改,就仍然存在半初始化对象泄漏的窗口。

六、构建契约与对象不变量

评审维度本题结论
必填信息host 等目标信息由 Builder 保证完整。
默认值超时、连接数等由具体 Builder 提供适配实现的默认值。
跨字段规则Builder 处理同一具体产品内部的参数组合。
可变引用处理证书、Header 集合和拦截器列表按所有权规则复制。
Builder 生命周期Factory/容器可长寿命,Builder 临时,Client 生命周期需明确关闭。
Product 交付保证调用方最终只依赖 HttpClient 抽象且获得合法配置。

build() 不是形式上的结束标记,而是对象从“参数集合”变成“合法业务值”的原子边界。单字段输入可以提前拒绝明显错误,跨字段规则必须等信息齐全后判断,Product 私有构造器还应保留必要兜底,避免未来新增创建入口绕过不变量。

七、与构造器、JavaBean 和工厂的选择边界

  • 字段少且全部必填时,短构造器或 record 通常比 Builder 更直接。
  • 字段多、可选项多、同类型参数易错时,命名步骤能显著提升调用可读性。
  • JavaBean setter 适合某些框架绑定,但对象可能在设置完成前就被观察到。
  • Builder 关注“同一种复杂对象怎样组装”;工厂模式关注“创建哪一种产品实现”。
  • 两者可以组合:工厂选择具体产品族或 Builder,Builder 再完成复杂组装。
  • 经典 Director 只有在固定步骤序列需要复用或存在多种表示时才有价值。
  • 链式 return this 只是语法,校验、默认值、拷贝和收口才是设计语义。
  • 若 Builder 比 Product 规则还复杂,应重新拆分对象职责,而不是继续堆方法。

如果只有固定实现但参数复杂,只用 Builder;如果产品实现多但构造简单,只用 Factory;两个维度同时存在才组合。

八、交付前的代码与测试检查

  1. 缺少每个必填字段分别调用 build(),应得到明确且稳定的异常。
  2. 完全不设置可选字段,核对默认值来自唯一权威位置。
  3. 传入最小值、最大值和越界值,确认范围判断没有反向或 off-by-one。
  4. 构造两个互相冲突的字段组合,确认只在信息完整时执行跨字段校验。
  5. 传入集合或数组后修改原引用,已构建 Product 不应跟着变化。
  6. 若 getter 返回可变数据,再尝试修改返回值,内部状态仍应保持不变。
  7. 连续调用同一个 Builder 两次,确认语义是明确允许复制还是文档明确禁止复用。
  8. 两线程共享一个 Builder 做压力测试应被禁止或证明安全,不能靠偶然结果。
  9. 若使用 Lombok,检查生成代码、默认值、@Singular、构造器可见性和框架兼容。
  10. 若迁移旧 API,比较默认值、异常类型、序列化字段和所有旧调用点行为。

记忆钩子:Factory 选型号,Builder 填配置并验收,别拿一个模式回答两个问题。

九、常见误区与追问

  • 误区:Builder 和工厂都是创建对象所以可以互换。 它们隔离的变化维度不同。
  • 误区:Factory 返回 Builder 就不再是工厂。 它仍可负责选择具体 Builder/产品族,只是把后续组装交给 Builder。
  • 误区:Builder 适合在多个产品类型间选择。 它通常聚焦一个复杂 Product 的构建表示。
  • 追问:抽象工厂能创建 Builder 吗? 可以为不同产品族返回对应 Builder,再保持整族配置一致。
  • 追问:Director 与 Factory 有何不同? Director 编排固定构建步骤,Factory 选择或创建具体产品。
  • 追问:如何避免组合后过度复杂? 只有两个变化轴都真实存在时才分层,并让职责和生命周期清楚。

十、加强记忆

工厂模式像“选厂家”,建造者模式像“按配置单组装产品”。前者隐藏具体产品类型,后者隐藏复杂构建过程;判断时抓住变化点,就不容易混淆。