工厂方法模式和建造者模式应该如何取舍?
简化版
工厂方法模式关注“创建哪一种产品”,适合产品类型可扩展、调用方不想依赖具体类的场景;建造者模式关注“一个复杂对象如何分步骤组装”,适合参数多、可选项多、构造过程复杂的场景。前者解决产品选择,后者解决对象组装。
详细版
工厂方法和建造者都属于创建型模式,但关注点不同。工厂方法把实例化延迟给具体工厂,用多态选择具体产品;建造者把复杂对象的构造步骤拆开,避免长构造器和参数组合混乱。
例如支付渠道 WechatPayment、AlipayPayment、CardPayment 的选择,更适合工厂方法或注册表工厂;而创建一个 HttpRequest,需要 URL、headers、timeout、body、retry、proxy 等大量可选参数,更适合 Builder。
| 维度 | 工厂方法 | 建造者模式 |
|---|---|---|
| 核心问题 | 选择哪种产品 | 如何组装复杂对象 |
| 产品数量 | 多种产品/实现 | 通常是一个复杂产品 |
| 参数复杂度 | 不一定复杂 | 通常参数多、步骤多 |
| 扩展点 | 新增具体产品和工厂 | 新增构造步骤或配置项 |
它们也可以组合:工厂方法先选择具体产品类型,产品内部再用 Builder 组装复杂参数。
完整版教学
一、两个模式的关注点完全不同
工厂方法模式的关键词是“多态选择”。调用方知道自己要一个抽象产品,但不想知道具体类。建造者模式的关键词是“分步组装”。调用方知道要构造某一类复杂对象,但不想面对巨大的构造器参数列表。
工厂方法:我要 Parser,但具体是 JsonParser 还是 XmlParser 由工厂决定
建造者:我要 HttpRequest,但它的 headers、body、timeout 要逐步配置
如果面试时只说“它们都是创建对象的模式”,答案会显得太浅。真正要比较的是创建复杂度来自类型选择,还是来自参数组装。
二、工厂方法更适合产品类型变化
当系统里有多种实现,并且新增实现是常态,就适合把创建逻辑放到工厂层。比如消息发送支持短信、邮件、站内信、企业微信,业务流程只依赖 MessageSender。
public interface SenderFactory {
MessageSender createSender();
}
public class SmsSenderFactory implements SenderFactory {
public MessageSender createSender() {
return new SmsSender();
}
}
假设 6 个月新增 5 种发送渠道,如果业务代码到处 new SmsSender(),每次都要修改调用方;如果依赖工厂抽象,新增具体工厂和产品即可减少对旧流程的影响。
三、建造者更适合参数组合复杂
当产品类型不多,但构造参数很多时,Builder 更合适。比如一个 HTTP 请求对象可能有 3 个必填参数和 10 个可选参数。用构造器会出现参数顺序难记、重载爆炸和可读性差的问题。
HttpRequest request = HttpRequest.builder()
.url("https://api.example.com")
.method("POST")
.timeoutMillis(3000)
.header("Trace-Id", "abc")
.body("{\"id\": 1}")
.build();
这里的重点不是选择 HttpRequest 的子类,而是让构造过程清晰、可校验、可维护。Builder 还能在 build() 阶段统一检查必填项,比如 URL 不能为空、超时时间必须大于 0。
四、用数字看构造器重载为什么会失控
假设一个对象有 8 个可选参数,每个参数都有“传或不传”两种状态,理论组合数是:
2^8 = 256
当然不会真的写 256 个构造器,但这说明重载构造器很快会失控。Builder 通过链式设置只表达用户真正关心的参数,避免大量无意义重载。
工厂方法面对的不是这个问题。工厂方法即使产品有 8 个参数,也不天然解决参数可读性;它只是决定创建哪个具体产品。必要时,具体产品创建过程内部仍可以用 Builder。
五、两者可以组合,不必二选一
复杂系统里经常先用工厂选择产品,再用 Builder 组装参数。例如不同云厂商的客户端创建:
CloudClientFactory -> 选择 AwsClient / AliyunClient / TencentClient
ClientBuilder -> 配置 region、timeout、retry、credential
代码可能是:
CloudClient client = factory.createBuilder()
.region("cn-hangzhou")
.timeoutMillis(5000)
.retry(3)
.build();
这种组合说明模式不是标签比赛。工厂解决“哪一类”,Builder 解决“怎么配”。只要职责清晰,组合使用是合理的。
记忆钩子:工厂方法管“选型号”,建造者管“配参数”。
六、错误取舍会带来什么问题
如果把 Builder 用来做大量类型选择,可能会出现 builder.type("sms") 这种隐藏分支,最后还是一个参数化工厂,只是披着 Builder 外衣。如果把工厂方法用来处理大量可选参数,又会出现工厂方法参数越来越长的问题。
对比:
| 错误做法 | 后果 |
|---|---|
用 Builder 的 type 字段选择大量产品 | 分支隐藏,扩展仍要改旧代码 |
| 用工厂方法接收十几个可选参数 | 参数混乱,调用可读性差 |
| 为简单对象强行 Builder | 代码膨胀,收益很低 |
| 为固定类型强行多工厂 | 类数量增加,理解成本上升 |
设计模式的取舍要看变化点。类型是变化点,用工厂;构造步骤是变化点,用 Builder。
七、面试答题顺序
可以按四步回答:
- 先给结论:工厂方法管产品选择,Builder 管复杂组装。
- 给例子:支付渠道适合工厂,HTTP 请求适合 Builder。
- 讲组合:工厂返回具体 Builder 或产品内部使用 Builder。
- 讲误用:不要用 type 字段把 Builder 写成大分支,也不要让工厂方法带十几个参数。
这样回答既有概念区分,又有工程判断,比只背定义更像真实项目经验。
八、常见误区与追问
- 误区:工厂方法和 Builder 都是创建对象,所以可以随便替换。 它们解决的变化点不同,不能只按创建型模式归类。
- 误区:Builder 一定比工厂方法更高级。 简单产品选择用工厂更直接,Builder 主要解决参数和步骤复杂。
- 误区:工厂方法可以优雅解决长参数列表。 它不解决参数可读性,长参数仍应考虑 Builder 或参数对象。
- 误区:Builder 里加 type 字段就是工厂方法。 这通常只是把 if-else 藏进 Builder。
- 追问:两者能否一起使用? 可以,工厂选择具体产品或 Builder,Builder 负责组装细节。
- 追问:抽象工厂和 Builder 又怎么区分? 抽象工厂关注产品族一致创建,Builder 关注一个复杂对象的分步构造。
- 追问:简单对象是否要 Builder? 参数少且语义清楚时构造器或静态工厂方法更轻量。
九、加强记忆
把创建复杂度分成两类:类型复杂和参数复杂。类型复杂用工厂方法或注册表工厂,参数复杂用 Builder;两者同时存在时可以组合。面试时用“支付渠道”和“HTTP 请求”两个例子,基本能把取舍讲清楚。