上一篇讲了抽象工厂模式。
它解决的问题是:
一整套相关对象,应该由同一套工厂统一创建。
比如不同车型平台下,制动控制器、转向控制器、电池管理器、通信对象要成套创建,不能随便混用。
但真实工程里,还有另一类创建问题。
这类问题不是"创建哪一个对象",也不是"创建哪一族对象",而是:
一个对象本身太复杂,不能简单地靠一个构造函数一次性创建出来。
比如车企软件里很常见的对象:
- 一个诊断请求报文
- 一个整车配置对象
- 一个自动化测试工况
- 一个 OTA 升级任务
- 一个仿真场景配置
- 一个数据采集任务参数
这些对象往往不是只有两三个字段。
它们可能包含:
- 必填参数
- 可选参数
- 默认参数
- 依赖参数
- 校验规则
- 构建顺序
- 不同构建模板
- 构建完成后的不可变状态
这时,如果还用一个巨大的构造函数,代码很快就会变得难读、难改、难校验。
建造者模式,就是为了解决这个问题。
一、先从一个车企场景说起
假设我们现在要构建一个诊断请求对象。
一个诊断请求可能包含:
- 服务 ID
- 数据标识 DID
- 目标 ECU
- 请求 payload
- 是否需要安全访问
- 安全等级
- 超时时间
- 重试次数
- 是否记录 trace
- 是否允许功能寻址
- 响应校验规则
如果直接写成构造函数,可能会变成这样:
cpp
DiagnosticRequest request(
0x22,
0xF190,
"VCU",
payload,
true,
0x03,
500,
3,
true,
false,
ResponseCheckMode::Strict
);
这段代码能跑。
但问题很明显。
第一,参数太多,调用方很难看懂每个参数是什么意思。
第二,几个 true/false 连在一起时,几乎没有可读性。
第三,参数顺序一旦写错,编译器不一定能发现。
第四,某些参数之间有约束,比如只有需要安全访问时,安全等级才有意义。
第五,如果未来新增一个参数,所有构造点可能都要跟着改。
所以,这里的问题不是"对象不能创建"。
真正的问题是:
复杂对象的构建过程没有被清晰表达出来。
二、为什么巨大构造函数会出问题?
很多人一开始会觉得:
构造函数不就是用来初始化对象的吗?
参数多一点也没关系吧?
简单对象当然没问题。
比如:
cpp
Point p(10, 20);
这种写法很清楚。
但如果对象变复杂,构造函数参数越来越多,就会出现典型问题。
1. 参数含义不清楚
比如:
cpp
TestScenario scenario(80, 120, 30, true, false, 3);
这几个数字和布尔值分别是什么?
是车速?
是 SOC?
是温度?
是持续时间?
是重试次数?
是安全等级?
调用方必须去看构造函数定义才能知道。
这就说明对象创建代码本身没有表达业务语义。
2. 可选参数会让构造函数膨胀
很多复杂对象都有默认值。
比如测试工况:
- 默认环境温度 25℃
- 默认路面附着系数 0.8
- 默认驾驶模式 Normal
- 默认不注入故障
- 默认不启用极端工况
如果把所有参数都塞进构造函数,就会出现大量调用方并不关心的参数。
cpp
VehicleTestCase testCase(
60,
80,
25,
0.8,
DriveMode::Normal,
false,
FaultType::None,
0,
0,
true
);
很多参数只是为了凑构造函数,不是当前业务真正关心的东西。
3. 参数之间可能有约束
复杂对象往往不是字段堆叠。
它们内部有规则。
比如诊断请求:
serviceId必须合法did只有在读数据服务下才需要- 安全等级只有在开启安全访问时才有效
- 超时时间不能小于 0
- 重试次数不能无限大
- 功能寻址下某些服务不允许发送
如果只是构造函数直接赋值,这些规则很容易散落到外部。
最后就会变成:
cpp
if (needSecurity) {
request.SetSecurityLevel(level);
}
if (timeout > 0) {
request.SetTimeout(timeout);
}
if (serviceId == 0x22) {
request.SetDid(did);
}
看起来是在创建对象,实际上调用方已经开始承担对象内部规则了。
这会破坏封装。
4. 对象可能出现"半成品状态"
如果用一堆 setter 创建对象,可能会写成这样:
cpp
DiagnosticRequest request;
request.SetServiceId(0x22);
request.SetTargetEcu("VCU");
request.SetTimeoutMs(500);
request.SetRetryCount(3);
request.SetDid(0xF190);
这比巨大构造函数好读一些。
但它也有问题。
在所有 setter 调完之前,request 其实是一个半成品。
如果中间被拿去使用,就可能出错。
比如:
cpp
DiagnosticRequest request;
request.SetServiceId(0x22);
request.SetTargetEcu("VCU");
sender.Send(request); // DID 还没设置
所以,复杂对象创建要解决的不只是"写起来方便",还要解决:
构建过程中可以逐步配置,但构建完成后的对象必须是有效的。
这就是建造者模式的价值。
三、建造者模式到底是什么?
建造者模式可以这样理解:
把一个复杂对象的构建过程从对象本身拆出来,让调用方可以一步步配置,最后一次性生成一个完整对象。
再说得直白一点:
对象不是靠一个巨大构造函数硬塞出来,而是通过一个 Builder 按步骤构建出来。
它关注的不是"创建哪个类"。
它关注的是:
一个复杂对象应该如何一步步组装。
所以,建造者模式也是创建型模式。
但它和工厂方法、抽象工厂的关注点不一样。
工厂方法关注:
创建哪一种产品。
抽象工厂关注:
创建哪一族产品。
建造者模式关注:
一个复杂产品内部怎么构建。
四、建造者模式解决的核心问题
建造者模式最核心的价值有三个。
1. 让复杂对象创建过程更清晰
原来是这样:
cpp
DiagnosticRequest request(
0x22,
0xF190,
"VCU",
payload,
true,
0x03,
500,
3,
true,
false,
ResponseCheckMode::Strict
);
使用 Builder 后,可以变成:
cpp
auto request = DiagnosticRequestBuilder()
.SetServiceId(0x22)
.SetDid(0xF190)
.SetTargetEcu("VCU")
.SetPayload(payload)
.RequireSecurity(0x03)
.SetTimeoutMs(500)
.SetRetryCount(3)
.EnableTrace()
.UsePhysicalAddressing()
.Build();
这段代码更像是在描述业务:
我要构建一个读 DID 的诊断请求。
目标是 VCU。
需要安全访问。
超时 500ms。
重试 3 次。
开启 trace。
创建过程本身就有可读性。
2. 把校验逻辑集中起来
复杂对象的合法性不应该散落在调用方。
Builder 可以在 Build() 时统一校验:
- 必填字段是否存在
- 参数范围是否合法
- 参数组合是否冲突
- 默认值是否补齐
- 构建结果是否满足不变量
这样调用方不需要到处写校验逻辑。
3. 避免对象暴露半成品状态
Builder 可以是可变的。
但最终生成出来的产品对象可以是不可变的。
也就是说:
text
Builder 阶段:允许一步步配置
Product 阶段:必须完整、合法、稳定
这对工程代码很重要。
因为真正被业务流程使用的对象,最好不要处于半初始化状态。
五、建造者模式的核心角色
建造者模式通常有几个角色。
1. Product:最终产品
也就是最终要创建出来的复杂对象。
比如:
text
DiagnosticRequest
VehicleTestScenario
VehicleConfig
OtaUpdateTask
它应该是构建完成后的稳定对象。
2. Builder:建造者接口或建造者类
负责一步步设置构建参数。
比如:
text
SetServiceId()
SetDid()
SetTargetEcu()
RequireSecurity()
SetTimeoutMs()
Build()
它把"怎么构建"这件事从 Product 里拆出来。
3. ConcreteBuilder:具体建造者
如果同一种产品有不同构建方式,可以有多个具体 Builder。
比如:
text
DiagnosticRequestBuilder
VehicleScenarioBuilder
SimulationScenarioBuilder
不过在很多 C++ 工程里,未必一定要单独抽象出 Builder 接口。
如果只有一种构建方式,一个具体 Builder 就够了。
4. Director:指导者,可选
Director 负责封装一套固定构建流程。
比如:
text
BuildReadVinRequest()
BuildReadDtcRequest()
BuildClearDtcRequest()
它不是必须的。
如果构建流程比较简单,调用方直接用 Builder 就可以。
如果某些构建流程经常复用,就可以加 Director。
六、建造者模式的结构
可以用一个简化结构理解:
text
调用方
↓
Builder
↓ 一步步设置参数
Product
如果有 Director,则变成:
text
调用方
↓
Director
↓ 使用
Builder
↓ 创建
Product
用 UML 简化表示:
creates
uses
DiagnosticRequest
-serviceId
-targetEcu
-payload
-timeoutMs
DiagnosticRequestBuilder
+SetServiceId(id)
+SetDid(did)
+SetTargetEcu(ecu)
+SetPayload(payload)
+RequireSecurity(level)
+SetTimeoutMs(timeout)
+Build()
DiagnosticRequestDirector
+BuildReadVinRequest()
+BuildReadDtcRequest()
这张图里最重要的不是类名,而是:
Product 不直接暴露复杂构建过程,Builder 负责把对象一步步构建完整。
七、一个 C++ 示例:构建诊断请求对象
下面用一个简化的诊断请求来说明。
先定义最终产品。
cpp
#include <cstdint>
#include <iostream>
#include <optional>
#include <string>
#include <vector>
class DiagnosticRequest {
public:
uint8_t GetServiceId() const {
return m_serviceId;
}
uint16_t GetDid() const {
return m_did;
}
const std::string& GetTargetEcu() const {
return m_targetEcu;
}
int GetTimeoutMs() const {
return m_timeoutMs;
}
int GetRetryCount() const {
return m_retryCount;
}
bool NeedSecurity() const {
return m_needSecurity;
}
uint8_t GetSecurityLevel() const {
return m_securityLevel;
}
bool TraceEnabled() const {
return m_traceEnabled;
}
private:
friend class DiagnosticRequestBuilder;
uint8_t m_serviceId = 0;
uint16_t m_did = 0;
std::string m_targetEcu;
std::vector<uint8_t> m_payload;
bool m_needSecurity = false;
uint8_t m_securityLevel = 0;
int m_timeoutMs = 500;
int m_retryCount = 0;
bool m_traceEnabled = false;
};
这里 DiagnosticRequest 本身不提供一堆 setter。
它更像一个构建完成后的稳定对象。
然后定义 Builder。
cpp
class DiagnosticRequestBuilder {
public:
DiagnosticRequestBuilder& SetServiceId(uint8_t serviceId) {
m_request.m_serviceId = serviceId;
m_hasServiceId = true;
return *this;
}
DiagnosticRequestBuilder& SetDid(uint16_t did) {
m_request.m_did = did;
m_hasDid = true;
return *this;
}
DiagnosticRequestBuilder& SetTargetEcu(const std::string& ecu) {
m_request.m_targetEcu = ecu;
return *this;
}
DiagnosticRequestBuilder& SetPayload(const std::vector<uint8_t>& payload) {
m_request.m_payload = payload;
return *this;
}
DiagnosticRequestBuilder& RequireSecurity(uint8_t level) {
m_request.m_needSecurity = true;
m_request.m_securityLevel = level;
return *this;
}
DiagnosticRequestBuilder& SetTimeoutMs(int timeoutMs) {
m_request.m_timeoutMs = timeoutMs;
return *this;
}
DiagnosticRequestBuilder& SetRetryCount(int retryCount) {
m_request.m_retryCount = retryCount;
return *this;
}
DiagnosticRequestBuilder& EnableTrace() {
m_request.m_traceEnabled = true;
return *this;
}
std::optional<DiagnosticRequest> Build() {
if (!Validate()) {
return std::nullopt;
}
return m_request;
}
private:
bool Validate() const {
if (!m_hasServiceId) {
return false;
}
if (m_request.m_targetEcu.empty()) {
return false;
}
if (m_request.m_timeoutMs <= 0) {
return false;
}
if (m_request.m_retryCount < 0 || m_request.m_retryCount > 5) {
return false;
}
if (m_request.m_needSecurity && m_request.m_securityLevel == 0) {
return false;
}
// 0x22 表示 ReadDataByIdentifier,必须有 DID
if (m_request.m_serviceId == 0x22 && !m_hasDid) {
return false;
}
return true;
}
private:
DiagnosticRequest m_request;
bool m_hasServiceId = false;
bool m_hasDid = false;
};
使用时可以这样写:
cpp
int main() {
auto request = DiagnosticRequestBuilder()
.SetServiceId(0x22)
.SetDid(0xF190)
.SetTargetEcu("VCU")
.RequireSecurity(0x03)
.SetTimeoutMs(500)
.SetRetryCount(3)
.EnableTrace()
.Build();
if (!request.has_value()) {
std::cout << "build diagnostic request failed\n";
return 1;
}
std::cout << "service id: "
<< static_cast<int>(request->GetServiceId())
<< "\n";
return 0;
}
这样写的好处是:
- 参数含义清楚
- 构建过程可读
- 校验逻辑集中
- 不容易得到非法对象
- 默认值可以在 Builder 或 Product 内部统一管理
这就是建造者模式的基本价值。
八、如果构建流程经常复用,可以引入 Director
有些诊断请求非常常见。
比如:
- 读取 VIN
- 读取 DTC
- 清除 DTC
- 读取软件版本号
- 进入扩展会话
- 执行安全访问
如果每个地方都手写 Builder 链式调用,也会重复。
这时可以引入一个 Director。
cpp
class DiagnosticRequestDirector {
public:
static std::optional<DiagnosticRequest> BuildReadVinRequest(
const std::string& targetEcu) {
return DiagnosticRequestBuilder()
.SetServiceId(0x22)
.SetDid(0xF190)
.SetTargetEcu(targetEcu)
.SetTimeoutMs(500)
.SetRetryCount(3)
.EnableTrace()
.Build();
}
static std::optional<DiagnosticRequest> BuildReadDtcRequest(
const std::string& targetEcu) {
return DiagnosticRequestBuilder()
.SetServiceId(0x19)
.SetTargetEcu(targetEcu)
.SetTimeoutMs(1000)
.SetRetryCount(2)
.EnableTrace()
.Build();
}
};
调用方就可以写成:
cpp
auto request = DiagnosticRequestDirector::BuildReadVinRequest("VCU");
这里 Director 的作用不是"让模式更完整"。
它的真正价值是:
把常用构建配方沉淀下来。
所以 Director 不是必须的。
只有当构建流程有复用价值时,它才有意义。
九、这个例子到底好在哪里?
1. 创建代码更像业务描述
对比一下。
巨大构造函数:
cpp
DiagnosticRequest request(0x22, 0xF190, "VCU", payload, true, 3, 500);
Builder 写法:
cpp
auto request = DiagnosticRequestBuilder()
.SetServiceId(0x22)
.SetDid(0xF190)
.SetTargetEcu("VCU")
.RequireSecurity(0x03)
.SetTimeoutMs(500)
.Build();
后者更清楚。
因为它把参数含义直接写在方法名里。
这不是语法糖。
这是在降低误用概率。
2. 校验集中在构建阶段
复杂对象最怕的一件事是:
字段虽然都赋值了,但组合起来是不合法的。
Builder 可以在 Build() 阶段统一处理。
比如:
- 读 DID 请求必须有 DID
- 安全访问必须有安全等级
- 超时时间必须大于 0
- 重试次数必须有上限
- 某些服务不能用功能寻址
这些规则如果散落到调用方,后期一定会乱。
集中在 Builder 里,至少可以保证所有对象都走同一套构建校验。
3. 产品对象更稳定
Builder 可以一步步改。
但最终 Product 不一定要暴露 setter。
这就避免了业务流程随意修改内部字段。
比如诊断请求构建完成后,服务 ID、目标 ECU、超时时间通常不应该在发送过程中被随便改掉。
这种设计更符合前面讲类定义时强调的:
类应该维护自己的不变量。
Builder 负责构建过程。
Product 负责稳定状态。
两者职责分开,系统会更清楚。
4. 默认值更容易统一管理
很多复杂对象都有默认值。
比如:
- 默认超时时间 500ms
- 默认重试次数 0
- 默认不开启 trace
- 默认物理寻址
- 默认严格响应校验
如果没有 Builder,这些默认值可能散落在多个构造点。
Builder 可以统一管理默认值。
调用方只需要设置自己关心的部分。
十、车企项目里,哪些地方适合建造者模式?
1. 构建复杂诊断报文
诊断请求往往包含服务、DID、payload、寻址方式、安全等级、超时、重试、响应校验等配置。
这些字段不是简单堆叠,而是有规则。
比如:
- 0x22 服务需要 DID
- 0x2E 写数据需要 payload
- 0x27 安全访问需要 seed/key 流程
- 功能寻址下某些服务受限
这类对象很适合用 Builder。
2. 构建自动化测试工况
自动化测试工况也很典型。
比如一个测试场景可能包含:
- 初始车速
- SOC
- 档位
- 驾驶模式
- 环境温度
- 路面附着系数
- 故障注入
- 持续时间
- 期望结果
- 数据采集信号
如果直接写构造函数,参数会非常多。
用 Builder 可以写成:
cpp
auto scenario = VehicleScenarioBuilder()
.SetInitialSpeedKph(60)
.SetSocPercent(80)
.SetDriveMode(DriveMode::Eco)
.SetRoadFriction(0.7)
.InjectFault(FaultType::WheelSpeedSensorLost)
.ExpectWarning("ABS_FAULT")
.Build();
这段代码本身就像测试描述。
对测试工程来说,可读性非常重要。
因为测试用例经常要被别人 review、维护、复制、调整。
3. 构建整车配置对象
整车配置对象通常也很复杂。
比如:
- 车型平台
- 动力形式
- 电池容量
- 电机类型
- 制动系统类型
- 转向系统类型
- ADAS 功能开关
- 地区法规配置
- 供应商差异配置
- 标定参数版本
如果所有配置都通过一个构造函数传入,调用方会非常痛苦。
Builder 可以把配置过程按业务语义拆开:
cpp
auto config = VehicleConfigBuilder()
.SetPlatform("APlatform")
.SetPowertrain(PowertrainType::BEV)
.SetBatteryCapacityKwh(75)
.EnableFeature("ACC")
.EnableFeature("AEB")
.SetMarketRegion("EU")
.SetCalibrationVersion("2026.05")
.Build();
这种写法更容易读,也更容易加校验。
4. 构建仿真场景
仿真场景往往有大量可选参数。
比如:
- 道路类型
- 交通参与者
- 天气
- 光照
- 传感器噪声
- GPS 漂移
- 故障注入
- 回放数据源
- 触发条件
这些参数组合非常多。
Builder 可以让仿真场景以更自然的方式描述出来。
5. 构建 OTA 升级任务
OTA 升级任务也可能适合。
比如:
- 目标 ECU
- 包版本
- 下载地址
- 校验方式
- 升级窗口
- 失败回滚策略
- 重试策略
- 用户提示策略
- 安全校验策略
这些字段之间有强约束。
用 Builder 可以把这些约束集中到构建阶段,避免业务流程拿到不完整任务。
十一、建造者模式和工厂方法有什么区别?
这是很容易混淆的地方。
可以先用一句话区分:
工厂方法关注"创建哪一种对象"。
建造者模式关注"一个复杂对象怎么构建完整"。
比如工厂方法解决的是:
text
我要创建 CAN Transport,还是 DoIP Transport?
建造者解决的是:
text
我要如何一步步构建一个完整的 DiagnosticRequest?
再对比一下:
| 对比项 | 工厂方法 | 建造者模式 |
|---|---|---|
| 关注点 | 对象类型选择 | 对象构建过程 |
| 典型问题 | 创建哪种具体实现 | 参数多、步骤多、校验多 |
| 返回结果 | 某个产品对象 | 一个完整复杂对象 |
| 扩展点 | 新增产品类型 | 新增构建步骤或构建配方 |
| 典型场景 | CAN / DoIP / Simulation 对象切换 | 复杂报文、测试场景、配置对象 |
所以不要看到"创建对象"就都叫工厂。
如果重点是选择类型,更像工厂。
如果重点是组装过程,更像建造者。
十二、建造者模式和抽象工厂有什么区别?
抽象工厂关注的是产品族。
比如:
text
A 平台一整套对象
B 平台一整套对象
仿真环境一整套对象
真实环境一整套对象
建造者关注的是单个复杂对象。
比如:
text
一个完整诊断请求
一个完整测试工况
一个完整整车配置
可以这样理解:
抽象工厂解决"成套对象从哪里来"。
建造者解决"复杂对象怎么拼起来"。
两者也可以一起用。
比如一个 SimulationEnvironmentFactory 创建一组对象,其中某个复杂的 SimulationScenario 可以由 SimulationScenarioBuilder 构建。
它们不是互斥关系。
只是解决的问题不同。
十三、建造者模式和 setter 有什么区别?
很多人会问:
Builder 不就是一堆 setter 吗?
看起来像,但重点不一样。
普通 setter 是在修改对象本身。
Builder 是在构建一个对象。
普通 setter 写法可能是:
cpp
DiagnosticRequest request;
request.SetServiceId(0x22);
request.SetDid(0xF190);
request.SetTargetEcu("VCU");
这种写法的问题是:
request可能中途处于非法状态- 外部可以随时继续修改
- 校验逻辑容易散
- 产品对象不稳定
Builder 写法是:
cpp
auto request = DiagnosticRequestBuilder()
.SetServiceId(0x22)
.SetDid(0xF190)
.SetTargetEcu("VCU")
.Build();
这里真正被业务使用的是 Build() 之后的对象。
也就是说:
text
setter 修改的是产品对象
builder 配置的是构建过程
如果 Builder 只是把 setter 换个名字,没有校验、没有默认值、没有构建边界,那它就只是形式上的 Builder。
十四、建造者模式最容易被滥用在哪里?
1. 简单对象也强行 Builder
比如一个对象只有两个字段:
cpp
class Point {
public:
Point(int x, int y);
};
这时如果写成:
cpp
auto point = PointBuilder()
.SetX(10)
.SetY(20)
.Build();
就没有必要。
建造者模式适合复杂对象。
如果对象本身很简单,普通构造函数更清楚。
2. Builder 只包装 setter,没有任何价值
有些代码看起来用了 Builder:
cpp
auto config = ConfigBuilder()
.SetA(a)
.SetB(b)
.SetC(c)
.Build();
但 Build() 只是简单返回对象,没有校验,没有默认值,没有构建规则。
这时 Builder 的价值很弱。
它只是把代码写长了。
所以要记住:
Builder 的价值不在链式调用,而在管理复杂构建过程。
3. Builder 变成万能配置中心
还有一种反模式是 Builder 什么都管。
比如:
cpp
class VehicleSystemBuilder {
public:
SetBrakeConfig();
SetBatteryConfig();
SetLogConfig();
SetDatabaseConfig();
SetHmiConfig();
SetCloudConfig();
SetUserConfig();
SetSecurityConfig();
};
如果这些东西没有清晰边界,Builder 就会变成新的万能 Manager。
这和前面单例、工厂里讲过的问题一样。
模式本身不能拯救职责混乱。
如果职责边界不清,换成 Builder 也只是换一种混乱方式。
4. Build 之后对象还能被随便改
如果 Builder 构建完对象后,对象仍然暴露大量 setter:
cpp
auto request = builder.Build();
request.SetServiceId(0x31);
request.SetTimeoutMs(-1);
那 Builder 的校验价值就被削弱了。
因为你在构建阶段保证了合法性,但构建后又允许别人破坏合法性。
所以复杂对象更推荐:
- Builder 阶段可变
- Product 阶段尽量稳定
- 必要修改通过明确业务方法完成,而不是随便 setter
5. 构建失败没有清晰处理
Builder 可能构建失败。
比如:
- 必填字段缺失
- 参数范围错误
- 参数组合冲突
- 依赖资源不可用
如果 Build() 失败时只是返回一个默认对象,风险很大。
更好的方式是明确表达失败:
- 返回
std::optional - 返回
std::expected - 返回错误码和错误信息
- 抛异常,但要统一项目风格
不要悄悄构建一个看似可用、实际错误的对象。
十五、工程中更推荐的用法
1. 先确认复杂度是否真实存在
使用 Builder 前,先问:
这个对象真的复杂吗?
判断方式可以很简单:
- 参数是否很多
- 可选字段是否很多
- 默认值是否很多
- 参数之间是否有约束
- 构建流程是否有顺序
- 构建逻辑是否在多个地方重复
如果都没有,就不要急着上 Builder。
2. 必填参数要明确
Builder 最怕的一件事是:
看起来所有字段都可以不填。
比如:
cpp
auto request = DiagnosticRequestBuilder().Build();
这通常是不合理的。
必填参数要么通过构造 Builder 时传入:
cpp
DiagnosticRequestBuilder builder(0x22, "VCU");
要么在 Build() 时严格校验。
不要让调用方轻易构建出一个空对象。
3. 默认值要集中管理
Builder 很适合管理默认值。
比如:
text
timeout = 500ms
retryCount = 0
traceEnabled = false
addressingMode = Physical
这些默认值应该集中在 Builder 或 Product 内部。
不要让调用方到处重复写默认值。
4. Build 阶段要做完整校验
Builder 不只是拼字段。
真正重要的是 Build()。
它应该负责:
- 补齐默认值
- 检查必填项
- 校验参数范围
- 校验参数组合
- 返回构建成功或失败
如果没有这一步,Builder 的工程价值会大打折扣。
5. Product 尽量保持稳定
复杂对象一旦构建完成,就应该尽量保持稳定。
尤其是:
- 诊断请求
- 测试工况
- OTA 任务
- 整车配置
- 仿真场景
这些对象如果在运行过程中被随意改,很容易造成排查困难。
所以可以考虑:
- 字段私有
- 只提供只读接口
- 通过构造完成初始化
- 少暴露 setter
- 修改时重新构建新对象
6. 常用构建流程可以沉淀成 Director 或静态方法
如果某些构建流程经常重复,就不要让调用方到处写。
比如:
cpp
BuildReadVinRequest()
BuildDefaultCityScenario()
BuildHighTemperatureChargingCase()
BuildMockVehicleConfig()
这些可以沉淀成 Director、工厂函数,或者项目里统一的构建工具。
关键不是类名叫什么。
关键是:
复用构建配方,而不是复制一堆构建步骤。
十六、建造者模式的优缺点
优点
建造者模式的优点很明确:
- 复杂对象创建过程更清晰
- 避免巨大构造函数
- 减少参数顺序错误
- 可读性更好
- 默认值更容易管理
- 构建校验更集中
- 可以避免产品对象半初始化
- 有利于构建不可变对象
- 常用构建流程可以复用
它适合处理"对象本身构建复杂"的问题。
缺点
它的缺点也很明显:
- 会多一个 Builder 类
- 简单对象使用会显得啰嗦
- 如果没有校验和默认值管理,容易变成形式主义
- Builder 设计不好也会膨胀
- 构建失败处理需要统一
- 链式调用过长时也可能影响可读性
所以,建造者模式不是为了让创建代码看起来更"优雅"。
它是为了让复杂对象的构建过程更可控。
十七、使用建造者模式前,先问这 6 个问题
1. 这个对象的参数是否很多?
如果只有两三个参数,构造函数通常更简单。
2. 是否有大量可选参数?
如果很多参数不是每次都要设置,Builder 会更清晰。
3. 参数之间是否有约束?
如果字段之间存在依赖关系,Builder 可以集中校验。
4. 是否需要统一默认值?
如果默认值散落在各处,Builder 可以收拢。
5. 是否存在常用构建流程?
如果很多地方都在构建类似对象,可以沉淀成 Director 或构建模板。
6. Build 之后对象是否应该稳定?
如果对象构建完成后不应该被随便修改,Builder 很适合。
十八、总结
建造者模式解决的核心问题是:
复杂对象不应该靠巨大构造函数或一堆随意 setter 拼出来。
它不是为了让代码看起来更花。
它真正想解决的是:
- 参数太多导致可读性差
- 可选参数和默认值难管理
- 构建规则散落在调用方
- 对象容易处于半成品状态
- 参数组合是否合法无法集中保证
- 常用构建流程无法复用
一句话概括:
建造者模式的重点不是"创建对象",而是"把复杂对象的构建过程表达清楚"。
在车企软件里,它很适合这些场景:
- 构建复杂诊断报文
- 构建自动化测试工况
- 构建整车配置对象
- 构建仿真场景
- 构建 OTA 升级任务
- 构建数据采集任务参数
但它也不适合所有场景。
如果对象很简单、参数很少、没有复杂校验,就不要为了"看起来像设计模式"而强行 Builder。
设计模式真正有价值的地方,不是让代码显得高级,而是让复杂性有地方安放。
如果上一篇抽象工厂模式提醒我们:
不要把"一整套相关对象"拆成一堆互不约束的创建逻辑。
那么这一篇建造者模式提醒我们:
不要把"复杂对象"硬塞进一个巨大构造函数里。
如果这篇对你有帮助,欢迎点赞、转发、关注。
我们下一篇继续拆设计模式。