建造者模式在 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<t></t>)暴露内部数据,切断外部修改通路
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() 入口统一处理,而不是散落在十几个构造函数或工厂方法里。