C#中可用的Builder核心是守住三道线:Product必须不可变、Builder不能提前持有Product、每步SetXxx()须校验参数并拒非法状态;Product应私有构造+只读属性,避免中途暴露无效状态。

直接说结论:C#里写一个能用的 Builder,核心不是堆语法糖,而是守住三道线——Product 必须不可变、Builder 不能提前持有或暴露 Product 实例、每一步 SetXxx() 都得校验参数并拒绝非法状态。
Product 类为什么必须用私有构造 + 只读属性
常见错误是把 Product 写成带 public setter 的普通类,比如:
public class HttpClientConfig
{
public string Url { get; set; }
public int Timeout { get; set; }
public int RetryCount { get; set; }
}这会导致两个问题:一是构建中途对象已可被外部读取(但字段可能为空或无效),二是无法保证一致性(比如 Url 为 null 时 Timeout 却被设了值)。正确做法是:
- 所有字段通过私有构造函数一次性传入
- 只暴露只读属性(
getonly) - 在构造函数里做基础校验,比如
if (string.IsNullOrEmpty(url)) throw new ArgumentException(...)
Fluent Builder 的 NullReferenceException 从哪来
链式调用如 builder.SetUrl("https://api.com").SetTimeout(30).Build() 看着干净,崩得也悄无声息。典型原因:
-
SetTimeout(null)没校验,默默存了个null,后续Build()里用它算超时就炸 - JSON 配置缺失字段,
SetUrl()传入空字符串,但没拦住,Build()时才发现_url是空 - 多个
SetXxx()方法返回this,但没统一状态标记,导致未填必填项就允许调Build()
防崩建议:
- 每个
SetXxx()开头加参数检查,非法值立刻throw new ArgumentException() -
Build()方法第一行检查关键字段,例如if (string.IsNullOrEmpty(_url)) throw new InvalidOperationException("URL is required") - 加个
_isConfigured标志位,只在所有必填步骤完成后才设为true,否则Build()直接抛异常
Director 类是不是非写不可
绝大多数 C# 场景下,Director 是多余的一层抽象。硬加它反而让调用变啰嗦:
- 原来一行
new HttpClientBuilder().SetUrl(...).Build() - 加了
Director后变成三步:var builder = new HttpClientBuilder(); var director = new Director(builder); director.Construct();
只有当出现以下情况时才值得引入 Director:
- 同一组构建逻辑被多处复用,且不同环境有分支(比如测试环境跳过认证、生产环境强制开启)
-
Director只依赖抽象IBuilder接口,不判断具体类型(如避免if (builder is GamingBuilder))
否则,把构建流程直接写在业务代码里更直白、更易调试。
Builder 和 Product 耦合的典型表现
耦合不是指“Builder 用了 Product 类名”,而是指构建逻辑泄漏、修改 Product 就必须改 Builder。常见症状:
-
ConcreteBuilder里直接new Product(),然后一个个product.Url = ...赋值 -
Builder持有Product实例引用(比如protected Product _product) - 嵌套
Builder(如Computer.Builder)访问了Computer的内部字段或 protected 成员
解耦要点:
-
Builder抽象类/接口只声明BuildXxx()方法,不定义任何字段,也不持有Product - 具体
Builder在Build()方法里才new Product(...),且所有参数都来自 Builder 自己的私有字段 - 如果要用嵌套
Builder,确保它和外层Product之间只通过构造参数通信,不穿透封装
最易被忽略的一点:很多开发者以为只要写了 Builder 类就完成了模式落地,其实真正难的是让 Build() 方法成为唯一出口、且这个出口只在完全合法的状态下才打开。


















