建造者模式在C#中通过类设计约定实现,核心是将构造逻辑移至Builder类并由Build()唯一出口完成终态校验与对象创建;Builder需可变、链式调用(各方法返回this),字段私有且仅Builder管理,校验和副作用统一在Build()中处理,Director为可选流程封装组件。

建造者模式在 C# 中不是靠语言特性强制实现的,而是通过类设计约定来达成——核心是把对象构造逻辑从客户端剥离,由 Builder 类逐步组装,再由 Director(可选)或直接由客户端触发 Build() 得到最终对象。
Builder 类必须是可变的、链式调用友好的
多数人一开始写错的地方,是让 Builder 方法返回 void 或新实例但不复用当前状态。正确做法是每个设置方法都返回 this,支持链式调用:
public class PizzaBuilder
{
private string _size = "medium";
private bool _hasCheese = true;
private List<string> _toppings = new();
<pre class="brush:php;toolbar:false;">public PizzaBuilder WithSize(string size) { _size = size; return this; }
public PizzaBuilder WithCheese(bool has) { _hasCheese = has; return this; }
public PizzaBuilder AddTopping(string topping) { _toppings.Add(topping); return this; }
public Pizza Build() => new(_size, _hasCheese, _toppings);
}
- 所有字段必须是
private且由Builder独占管理,避免外部绕过构造逻辑直接改状态 - 不要在
WithXxx()中做校验或副作用(如网络请求),那属于Build()阶段职责 - 如果需要不可变性保障,
Build()后可将内部字段设为只读,或返回readonly struct封装
Build() 方法才是对象创建的唯一出口
真正关键的不是怎么“设参数”,而是谁、何时、以什么顺序“合成对象”。Build() 必须承担终态校验和对象实例化双重责任:
public Pizza Build()
{
if (_toppings.Count == 0)
throw new InvalidOperationException("At least one topping is required.");
<pre class="brush:php;toolbar:false;">// 防止重复构建
if (_isBuilt) throw new InvalidOperationException("Builder already used.");
_isBuilt = true;
return new Pizza(_size, _hasCheese, _toppings.AsReadOnly());
}
- 校验逻辑放在这里,而不是分散在每个
WithXxx()中——否则易漏、难维护 - 加
_isBuilt标志防止重复调用Build(),避免状态污染或意外共享 - 返回新对象时,尽量用只读集合(如
IReadOnlyList)暴露内部数据,切断外部修改通路
Director 不是必需的,但适合固定流程复用
当多个地方都要按相同步骤构造对象(比如“标准素食披萨”“儿童套餐披萨”),可以抽一个 Director 封装流程,避免客户端重复写链式调用:
public class PizzaDirector
{
public Pizza MakeVegetarianPizza(PizzaBuilder builder) =>
builder.WithSize("large")
.WithCheese(true)
.AddTopping("mushrooms")
.AddTopping("bell peppers")
.AddTopping("olives")
.Build();
}
-
Director只依赖抽象Builder(接口或基类),不绑定具体实现,便于扩展不同风格的构建器 - 如果业务流程简单或变化频繁,跳过
Director直接链式调用更轻量、更直观 - 别为了模式而加
Director——它只是流程模板,不是建造者模式的必要组成部分
最常被忽略的一点:建造者模式的价值不在语法糖,而在把“构造契约”显式化。一旦 Build() 成为唯一合法出口,你就天然获得了构造过程的可观测性、可测试性和可拦截性——比如加日志、埋点、权限检查,全都可以在 Build() 入口统一处理,而不是散落在十几个构造函数或工厂方法里。










