写在前面
上一篇工厂解决"造哪个"。这篇解决另一个常见痛点:对象很复杂,怎么造得清爽。
想象一个对象有十几个字段,大多可选。你见过那种构造函数吗——new Order(cust, addr, null, null, true, null, "CNY", 0, ...),调用方数参数能数瞎。这就是伸缩构造函数反模式(Telescoping Constructor),建造者模式就是它的解药。
一、问题:伸缩构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
| // 反模式:为了覆盖各种可选组合,构造函数越开越多
public class Mail
{
public Mail(string from, string to) { ... }
public Mail(string from, string to, string subject) { ... }
public Mail(string from, string to, string subject, string body) { ... }
public Mail(string from, string to, string subject, string body, IEnumerable<string> cc) { ... }
public Mail(string from, string to, string subject, string body, IEnumerable<string> cc, bool html) { ... }
// ... 还能继续开
}
// 调用方噩梦:这个 true 是啥?两个 null 又是啥?
var m = new Mail("a@x.com", "b@y.com", "Hi", null, null, true);
|
1
2
3
4
5
6
7
8
9
10
11
| 这坨代码的病:
✗ 参数多,位置易错(传反了编译器不报)
✗ 一堆 null 占位,可读性极差
✗ 新增字段 → 又要开构造函数,或改所有现有构造函数
✗ 难以表达"必填 vs 可选"
另一种"病":用无参构造 + 一堆 setter
var m = new Mail();
m.From = ...; m.To = ...; m.Subject = ...;
✗ 对象在"半成品"状态就被别人看到(多线程下尤其危险)
✗ 字段可变,无法做不可变对象
|
二、建造者:分步构造 + 链式
1
2
3
4
5
6
7
8
9
10
11
12
| 意图:把复杂对象的构造与它的表示分离。
分步设置,最后一步统一"产出",保证产出的对象是完整、一致的。
结构:
Director/客户端 → 调 Builder 的一系列设置方法(链式)→ 最后 Build() 得对象
产出过程对象不可见(半成品状态不外泄)
好处:
✓ 参数有名字(.Subject("Hi") 比 null 位置清晰)
✓ 必填/可选分明,可在 Build() 时校验
✓ 产出的对象可以是不可变的
✓ 加字段 = 加一个方法,老调用不受影响(开闭)
|
三、手写一个 fluent builder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| // 目标对象:不可变(只读字段)
public sealed class Mail
{
public string From { get; }
public string To { get; }
public string Subject { get; }
public string Body { get; }
public IReadOnlyList<string> Cc { get; }
public bool IsHtml { get; }
private Mail(string from, string to, string subject, string body,
IReadOnlyList<string> cc, bool isHtml)
{
From = from; To = to; Subject = subject; Body = body; Cc = cc; IsHtml = isHtml;
}
// 建造者(嵌套类,唯一能造 Mail 的途径)
public class Builder
{
private string _from = "", _to = "", _subject = "", _body = "";
private List<string> _cc = new();
private bool _isHtml;
public Builder From(string v) { _from = v; return this; }
public Builder To(string v) { _to = v; return this; }
public Builder Subject(string v){ _subject = v; return this; }
public Builder Body(string v) { _body = v; return this; }
public Builder Cc(string v) { _cc.Add(v); return this; }
public Builder AsHtml() { _isHtml = true; return this; }
public Mail Build()
{
if (string.IsNullOrEmpty(_from) || string.IsNullOrEmpty(_to))
throw new InvalidOperationException("From/To 必填");
return new Mail(_from, _to, _subject, _body, _cc, _isHtml);
}
}
}
// 用:链式调用,一目了然
var mail = new Mail.Builder()
.From("a@x.com")
.To("b@y.com")
.Subject("Hi")
.Body("正文...")
.Cc("boss@z.com")
.AsHtml()
.Build(); // 校验通过才产出完整对象
|
1
2
3
4
5
| 对比开头的噩梦:
- 每个参数有名字,传不错位置
- 必填校验在 Build() 里,半成品不会泄漏
- 产出的 Mail 不可变,天然线程安全
- 加字段 = Builder 加个方法,老代码不动
|
四、record 让建造者更简洁
如果对象不需要复杂校验,C# record + with 能省掉一大半样板:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // record 自带"with 表达式",本质就是个轻量建造者
public record MailSettings(string From, string To, string Subject = "",
string Body = "", bool IsHtml = false);
// 用:必填位参 + 可选命名参数 + with 覆盖
var m = new MailSettings("a@x.com", "b@y.com") with { Subject = "Hi", IsHtml = true };
// 或要链式体验,用 record 写个轻量 builder
public record MailBuilder(string From, string To)
{
public string Subject { get; init; } = "";
public string Body { get; init; } = "";
public bool IsHtml { get; init; }
}
var b = new MailBuilder("a@x.com", "b@y.com") { Subject = "Hi", IsHtml = true };
|
1
2
3
4
5
6
7
| record + with / init:
✓ 极少样板代码
✓ 不可变
✓ 命名清晰
适合:对象不复杂、校验少。
对象复杂、需要严格分步和校验时,还是用上面的经典 builder。
|
五、.NET 里建造者无处不在
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| 你天天见 builder,认出来了吗:
EF Core:
modelBuilder.Entity<Order>()
.HasKey(o => o.Id)
.HasOne(o => o.Customer)
.WithMany(c => c.Orders); // ModelBuilder 是 builder
ASP.NET Core:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddSwagger();
var app = builder.Build(); // 经典分步构造 + Build()
配置:
configurationBuilder.AddJsonFile("app.json")
.AddEnvironmentVariables()
.Build();
字符串:
new StringBuilder().Append("a").Append("b").ToString(); // 最朴素的 builder
它们共同点:链式设置 + 最后一步产出。
这就是建造者模式的"语言级习惯"。
|
六、建造者 vs 工厂
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| 常被搞混,区别其实清楚:
工厂:一步产出对象
var order = OrderFactory.Create(type);
→ 关心"造哪个",一次到位
建造者:分步产出对象
var order = new OrderBuilder().For(cust).With(items).Express().Build();
→ 关心"怎么配置这一个",逐步装配
选择:
对象创建简单、有几种类型 → 工厂
对象字段多、可选参数多、要分步配置 → 建造者
两者可配合:工厂返回一个 builder,让你再细调
|
七、何时用 / 何时别用
1
2
3
4
5
6
7
8
9
10
11
12
13
| 该用建造者:
✓ 字段多(5+),尤其大多是可选
✓ 构造需要分步、有顺序、要校验
✓ 想要产出的对象不可变
✓ 配置类、DSL 式 API(Fluent 接口)
别用:
✗ 字段少、必填为主 → 普通构造函数 + record 就够
✗ 对象本身就该可变、简单 → 别硬套 builder 增加样板
✗ 只有一两种组合 → 工厂更直接
信号:当你写构造函数写到参数列表比方法体还长、
调用方需要数着位置传 null —— 就该上建造者了。
|
八、小结
- 问题:伸缩构造函数(参数越开越多)和 setter 半成品泄漏
- 建造者模式:分步链式设置 + 最后 Build() 产出,保证完整、可校验、可不可变
- 手写:嵌套 Builder 类,每个字段一个返回 this 的方法,Build() 时校验
- 现代 C#:
record + with/init 能省掉大部分样板(简单场景) - .NET 里随处可见:EF Core ModelBuilder、WebApplication、配置、StringBuilder
- vs 工厂:工厂一步造"哪个",建造者分步配"这一个"
- 信号:构造参数列表长到要数 null,就上建造者
下一篇讲装饰器模式——怎么不改原类、透明地给对象叠加新功能(日志/缓存/重试)。