c#建造者模式核心是解耦构建逻辑与产品结构、确保对象一致性、避免强制使用director;product应不可变,builder需严格校验参数并一次性构造实例。

直接上结论:C#里实现建造者模式,关键不是“写个 Builder 类”,而是让构建逻辑与产品结构解耦、避免 Build() 返回前对象处于不一致状态、并且别把 Director 当必需品硬塞进去。
Builder 类怎么写才不和 Product 耦合
常见错误是让 ConcreteBuilder 直接 new Product() 或暴露 Product 的 public set 属性——这等于把构造细节全摊在 Builder 里,改一个字段就得同步改 Builder,失去封装意义。
正确做法是:
-
Product使用私有构造函数 + 公共只读属性,所有字段必须在构造时一次性传入 -
Builder(抽象类或接口)只声明BuildXxx()抽象方法,不持有Product实例引用 - 具体 Builder 在
Build()方法里才 newProduct,且只通过构造参数传值,不调用任何 setter - 如果用嵌套
Builder类(如Computer.Builder),也应遵循同样原则,避免访问Computer的内部字段
Fluent Builder 为什么一调用就 NullReferenceException
链式调用看着干净,但 SetUrl().SetTimeout().Build() 这种写法默认假设每步都成功。一旦某步解析失败(比如 JSON 配置缺失 Timeout 字段),SetTimeout(null) 可能默默存了个 null,后续 Build() 里用它算超时就会崩,堆栈还看不出哪步出问题。
防崩要点:
- 每个
SetXxx()必须做参数校验,非法输入立刻throw new ArgumentException(...),不吞异常 -
Build()开头加必填字段检查,例如if (string.IsNullOrEmpty(_url)) throw new InvalidOperationException("URL is required"); - 不要在字段未初始化完时允许调用
Build();可加一个_isConfigured标志位,仅当所有必要步骤完成才设为 true
Director 类到底要不要写
绝大多数 C# 项目里,Director 是冗余的。它适合复用固定流程且存在多条件分支的场景(比如“测试环境跳过认证,生产环境强制开启”),但如果你只是配置一个 HTTP 客户端:builder.SetUrl().SetTimeout().SetRetry().Build(),那直接链式调用更直白。
硬加 Director 的代价:
- 调用方要先 new
Director,再传Builder,再调Construct(),三步变五步 -
Director内部若依赖具体Builder类型(比如if (builder is GamingBuilder)),就破坏了依赖倒置原则 - 如果构建步骤只有 2–4 步、无分支、无复用,
Director只是给简单逻辑套了一层抽象壳
Product 不可变性为什么不能妥协
很多人图省事让 Product 的属性可写,觉得“反正 Builder 控制了赋值顺序”。但只要开放了 public set,就无法阻止外部代码在 Build() 后篡改状态,导致对象在生命周期中处于不一致、不可预测的状态。
真正安全的做法:
-
Product所有属性只读(get;),构造函数接收全部必要参数 - 如果某些字段确实可选,用
Nullable<t></t>或Option<t></t>(如 LanguageExt)显式表达“未设置”语义,而不是用 null 占位 - 避免在
Builder中缓存Product实例——每次Build()都应返回一个全新对象,确保不可变语义不被破坏
最易被忽略的一点:Builder 的字段校验和 Product 的构造校验要对齐。比如 SetUrl(string) 允许空字符串,但 Product 构造函数却要求非空,这种错位会在运行时才暴露,且难以定位。










