C#建造者模式:从重叠构造器到Build校验与不可变收口

发布时间:2026/11/2 13:44:03
C#建造者模式:从重叠构造器到Build校验与不可变收口 构造函数参数一路加到十一个、其中六个还是bool这种代码在 C# 项目里一点都不罕见。多数人第一次接触 C# 建造者模式是在设计模式教材里看完示例觉得不就是把 setter 换了个名字然后在真实项目里再也没用过。我对这个判断曾经深信不疑直到某次接手一个订单创建模块构造函数签名要换行三次调用方每次 new 都得对着 IntelliSense 数参数位置改一个字段顺序就全线编译报错。那次重构之后我才明白建造者模式解决的从来不是怎么写得更花哨而是把一个可能非法的中间状态彻底挡在对象生命周期之外。这篇就聊聊我在 C# 里落地建造者模式的完整思路它到底该长什么样、哪些角色可以砍掉、和 record、对象初始化器、依赖注入容器怎么划清边界以及那些让我返工过两次的坑。1. 构造函数参数一多就失控C# 建造者模式真正要解决的痛点先把这个模式的价值说透否则后面的代码都是无根之木。建造者模式Builder Pattern在 GoF 的原始定义里核心是把复杂对象的构建过程和表示分离使得同样的构建过程可以创建不同的表示。这句话在 C# 语境下需要翻译一下构造过程 那些顺序敏感、依赖上下文、需要校验的赋值动作表示 最终那个不可变的产品对象。真正触发我使用它的场景通常不是对象字段多而是下面这几条同时成立字段之间存在约束关系比如DiscountAmount必须小于TotalAmount、有必填项和可选项混杂、对象一旦构造完成就不该再被修改、构造过程可能需要分步骤决策先决定用哪种计价策略再决定怎么算总价。只要命中其中两条以上手写构造函数就开始变得难受。1.1 重叠构造器在 C# 里的典型症状Java 圈管这个叫 telescoping constructor翻译过来是重叠构造器或者参数蠕变。C# 因为有可选参数和命名实参症状比 Java 轻微一些但依然存在。看一段我实际遇到过的代码public class Order { public Order(string orderNo, string customerId, decimal totalAmount) : this(orderNo, customerId, totalAmount, null) { } public Order(string orderNo, string customerId, decimal totalAmount, string remark) : this(orderNo, customerId, totalAmount, remark, false) { } public Order(string orderNo, string customerId, decimal totalAmount, string remark, bool isUrgent) : this(orderNo, customerId, totalAmount, remark, isUrgent, DateTime.Now) { } public Order(string orderNo, string customerId, decimal totalAmount, string remark, bool isUrgent, DateTime createdAt) { // 真正的赋值逻辑 } }这种写法的问题不在于长而在于语义会悄悄错位。调用方写new Order(no, cid, 100m, 加急)编译器不会拦你运行起来也没事但你传的其实是remark。等你后面再加一个string channel参数所有旧的调用点都会因为位置错位而改变含义而且编译器一声不吭。字符串类型的参数尤其危险customerId和remark都是string调换位置照样编译通过。我后来统计过这个模块光这一个类就有 14 个调用点其中 3 处存在参数错位只是因为恰好业务上没触发才没暴露。这就是重叠构造器最阴的地方它不报错它只是让你在某个深夜的生产事故里慢慢发现。1.2 建造者和工厂、可选参数、对象初始化器的边界在哪选型之前先把几个容易混淆的方案摆清楚我按是否保证最终对象合法和是否保证不可变两个维度做了对照方案能否保证构造完成即合法能否保证不可变适合的典型场景重叠构造器弱靠约定可以参数 ≤ 3 且位置不易混淆可选参数 命名实参弱默认值可能本身就不合法可以参数 ≤ 6无字段间约束对象初始化器new X { ... }不能中间态裸露不能需可写属性DTO、配置对象、内部使用工厂方法强可在方法内校验可以构建逻辑简单但需集中管控建造者模式强Build()统一收口可以多步骤、有约束、可选参数多record with强主构造函数校验可以变体衍生多、字段少改动几条我自己的判断标准可以直接拿去用参数在 3 个以内且类型互不相同 —— 老老实实用构造函数别折腾。参数多但没有字段间约束且调用方明确知道每个参数含义 —— 可选参数配合命名实参够用。对象需要多次复制并少量修改 ——record的with表达式是性价比最高的选择我后面会单独讲。需要把非法状态挡在外面同时构造过程本身有多个决策点—— 这才是建造者模式该出场的地方其他方案都会在某个角落漏出破绽。对象初始化器的坑值得多说一句。很多人觉得new Order { OrderNo x, Total 100m }已经很好看了问题是这个表达式的中间状态是可见的OrderNo赋完了但Total还没赋如果这段时间里有人把这个半成品传出去或者属性 setter 里做了副作用就会出现非常难查的问题。更根本的是为了让对象初始化器可用你必须给属性开set不可变性直接丢了。我在一个并发量比较高的计价模块里踩过这个坑两个线程同时操作一个看起来已经初始化完的对象结果读到了半截数据。2. 手写一个能上生产的 C# 建造者接口、链式调用与 Build 收口理论说完了动手写。我先给一个不太教科书但能直接用在项目里的版本然后逐块解释为什么这么设计。这个例子的业务背景是促销订单的创建字段里有必填项、可选项还有一组互斥的优惠规则需要校验。2.1 产品类该暴露什么不该暴露什么产品类的第一原则是所有属性只读构造逻辑只对建造者开放。C# 里实现这一点最干净的做法是把构造函数设为private或internal把建造者声明为嵌套类。嵌套类可以访问外部类的私有成员这是 C# 语言层面给的特权用它比自己写一堆[InternalsVisibleTo]靠谱得多。public sealed class PromotionOrder { public string OrderNo { get; } public string CustomerId { get; } public decimal OriginalAmount { get; } public decimal DiscountAmount { get; } public decimal PayableAmount { get; } public IReadOnlyListstring AppliedRules { get; } public string Remark { get; } public DateTime CreatedAt { get; } private PromotionOrder(string orderNo, string customerId, decimal originalAmount, decimal discountAmount, decimal payableAmount, IReadOnlyListstring appliedRules, string remark, DateTime createdAt) { OrderNo orderNo; CustomerId customerId; OriginalAmount originalAmount; DiscountAmount discountAmount; PayableAmount payableAmount; AppliedRules appliedRules; Remark remark; CreatedAt createdAt; } public sealed class Builder { // 见下一节 } }注意几个细节。第一集合类型用IReadOnlyListstring而不是Liststring并且在Build()里做一次真正的复制防止调用方拿着原始List引用继续改。第二PayableAmount是派生值不在外部传入而是在Build()里算出来的 —— 这类由其他字段推导出来的字段千万不要交给调用方填否则迟早出现原价 100、优惠 20、应付 90这种数据。第三加sealed不是为了性能是因为允许继承的可变构建对象是所有 bug 的温床这条我后面会展开。2.2 流式返回值设计返回 this 还是返回新实例链式调用有两种流派一种返回this可变建造者一种每次返回新实例不可变建造者。C# 生态里 99% 的代码用的是第一种包括各类 ORM 的查询构建器。我建议生产代码里也用第一种理由是简单、分配少、调试友好不可变建造者在 C# 里收益不大反而会让调用链变长、异常栈变难看。public sealed class Builder { private string _orderNo string.Empty; private string _customerId string.Empty; private decimal _originalAmount; private readonly Liststring _appliedRules new(); private string _remark string.Empty; private decimal _manualDiscount; public Builder WithOrderNo(string orderNo) { _orderNo orderNo; return this; } public Builder WithCustomer(string customerId) { _customerId customerId; return this; } public Builder WithAmount(decimal originalAmount) { _originalAmount originalAmount; return this; } public Builder ApplyRule(string ruleCode) { if (!string.IsNullOrWhiteSpace(ruleCode)) _appliedRules.Add(ruleCode); return this; } public Builder WithRemark(string remark) { _remark remark; return this; } public Builder WithManualDiscount(decimal discount) { _manualDiscount discount; return this; } }方法命名上我习惯用With前缀而不是Set。原因很实际With听起来像追加一个特征Set听起来像覆盖一个状态前者鼓励调用方把它当成一次性的配置动作后者容易让人产生可以反复 set 然后复用的错觉。这只是团队约定但确实减少过几次误用。另一个值得说的小技巧给ApplyRule加上params string[]重载多规则场景能少写很多行public Builder ApplyRules(params string[] ruleCodes) { foreach (var code in ruleCodes) ApplyRule(code); return this; }不过要小心params配合null的经典陷阱。调用ApplyRules(null)时编译器会把它当成一个null数组foreach直接抛NullReferenceException。加一行卫语句就能避开但很多人不写等到线上报错了才想起这个语言细节。2.3 Build 方法里的校验与不可变收口Build()是整个模式的重心。我给自己定的规矩是所有校验都必须在Build()里做且校验失败必须抛异常不能返回 null 也不能返回一个半成品。原因在于建造者的用户已经默认调用Build()就是拿到一个可用对象返回 null 等于把校验责任转移回调用方前面所有工作都白做了。public PromotionOrder Build() { if (string.IsNullOrWhiteSpace(_orderNo)) throw new InvalidOperationException(订单号不能为空); if (string.IsNullOrWhiteSpace(_customerId)) throw new InvalidOperationException(客户标识不能为空); if (_originalAmount 0) throw new InvalidOperationException($订单金额必须大于 0当前值 {_originalAmount}); if (_manualDiscount 0) throw new InvalidOperationException(手工优惠金额不能为负数); if (_manualDiscount _originalAmount) throw new InvalidOperationException( $手工优惠 {_manualDiscount} 超过订单金额 {_originalAmount}); if (_appliedRules.Distinct().Count() ! _appliedRules.Count) throw new InvalidOperationException(优惠规则存在重复应用); var discount _manualDiscount; var payable _originalAmount - discount; return new PromotionOrder( _orderNo, _customerId, _originalAmount, discount, payable, _appliedRules.ToArray(), _remark, DateTime.UtcNow); }这里有两个我特意处理的点。_appliedRules.ToArray()而不是直接传_appliedRules是为了切断建造者和产品之间的引用共享 —— 否则调用方在Build()之后继续ApplyRule会直接改掉已经完成的订单。这个 bug 我确实见过一次排查花了整整半天因为现象是订单的规则列表莫名其妙变长了。第二个点是DateTime.UtcNow在Build()里取而不是在建造者构造函数里取。有人喜欢在new Builder()时把时间戳固定下来逻辑上更纯粹但实际业务里一个建造者从创建到Build()可能跨越几秒用哪个时间点都说得通直接在Build()里取最贴近对象诞生的瞬间这个语义。真要测试就把时钟抽象成接口注入别为了可测性牺牲语义正确性。调用方的代码长这样可读性比一堆位置参数强太多var order new PromotionOrder.Builder() .WithOrderNo(PO20240512001) .WithCustomer(C-88213) .WithAmount(1299m) .ApplyRules(FULL_200_MINUS_30, NEW_USER_50) .WithManualDiscount(50m) .WithRemark(客服手动调整) .Build();每个参数都是自解释的参数顺序错位的问题彻底消失新增字段也不会影响已有调用点。这就是我前面说的解决的不是花哨问题。3. 指挥者Director角色在真实项目里的存废教科书上的建造者模式一定有四个角色Product、Builder、ConcreteBuilder、Director。前三个是刚需Director 是最容易被滥用的那一个。我做了几年下来实际项目里用 Director 的次数一只手数得过来但每次面试或者代码评审总有人问你的 Director 呢。3.1 教材为什么一定要塞一个 DirectorDirector 的存在意义是封装一个固定的构建流程。教材里的经典例子是组装套餐Director.Construct()内部依次调builder.BuildBurger()、builder.BuildDrink()、builder.BuildToy()客户端只需要换一个具体的 builder 就能得到不同套餐。这个例子的前提是流程本身是稳定的、有多种表示需要切换。这个前提在业务系统里成立的概率非常低。绝大多数时候客户端的调用顺序本身就是业务逻辑的一部分不同场景需要的字段组合完全不同硬套一个 Director 只会让代码多一层转发。更糟的是Director 一旦引入建造者就必须暴露成公共 API客户端可以绕过 Director 直接调用所谓流程约束形同虚设 —— 你想限制的东西根本没限制住。我见过一个反面案例某模块为了符合设计模式给每个建造者都配了一个 DirectorDirector 内部只有一行return builder.Build()。这层包装除了让调用栈多一帧、让新人多困惑十分钟没有产生任何价值。后来重构时全部删掉代码行数少了三百多行行为完全不变。3.2 Director 变成负担的三种信号我总结了三类信号出现任意一条就该考虑砍掉 DirectorDirector 只有一种实现。如果ConcreteDirector只有一个且里面没有任何分支判断那它就是纯粹的转发层。客户端需要绕过 Director 直接设置字段。这说明固定流程覆盖不了实际需求强行封装只会让客户端写director.Construct(); builder.WithXxx(...)这种四不像代码。Director 里开始出现if/else。一旦流程有分支它就不是稳定流程了应该退化成普通的服务类或者静态工厂方法。真有固定流程的场景我更倾向于用一个静态方法表达比 Director 类直白得多public static class PromotionOrderPresets { public static PromotionOrder CreateStandard(string orderNo, string customerId, decimal amount) { return new PromotionOrder.Builder() .WithOrderNo(orderNo) .WithCustomer(customerId) .WithAmount(amount) .Build(); } public static PromotionOrder CreateNewUser(string orderNo, string customerId, decimal amount) { return new PromotionOrder.Builder() .WithOrderNo(orderNo) .WithCustomer(customerId) .WithAmount(amount) .ApplyRule(NEW_USER_50) .Build(); } }预设方法放在这里客户端一眼就知道有哪些标准套餐IDE 补全直接列出所有可选动作比实例化一个 Director 再调用顺手得多。而且它是无状态的静态方法不存在线程安全问题也方便单元测试。4. 和 C# 语言特性碰撞record、with 表达式与泛型自引用建造者C# 发展到现在很多当年必须靠建造者解决的问题语言层面已经给出了一半答案。搞清楚哪些能偷懒、哪些不能比死守经典实现重要得多。4.1 record with 能替代多少建造者工作量record类型配合with表达式本质上提供了一种轻量不可变 局部复制的能力。如果你的场景是对象构建完成后偶尔需要基于它派生一个只改一两个字段的新版本record的性价比远高于手写建造者。public sealed record ShippingAddress( string Province, string City, string Street, string ContactName, string ContactPhone, string PostalCode); var baseAddr new ShippingAddress(浙江省, 杭州市, 某街道 1 号, 张工, 13800000000, 310000); var changed baseAddr with { Street 某街道 88 号 };with表达式会做一次浅拷贝再覆盖指定字段写起来非常干净。但它替代不了建造者的地方也很明确它不做校验。record的主构造函数可以写校验逻辑但with表达式走的是拷贝构造函数那条校验路径不一定被触发。C# 里如果主构造函数有校验with会绕过它 —— 这是个很隐蔽的坑我见过有人以为with会重新校验结果造出了负数金额的对象。它不支持分步骤决策。建造者的优势是构建过程中可以根据中间结果决定下一步record是一次性把所有参数给齐没有中间态。它处理不了集合的防御性复制。with是浅拷贝集合字段仍然是共享引用。所以我的判断是纯数据载体、字段少、校验简单 —— 用record构建过程有分支、有跨字段约束、有集合要防共享 —— 用建造者。两者不是替代关系同一个项目里混着用完全正常。4.2 泛型自引用建造者骨架CRTP 风格一个业务系统里往往有十几个产品类如果每个都从零手写Builder重复代码会非常可观。C# 里可以用泛型自引用Curiously Recurring Template Pattern 的变体做一个基类把重复的样板代码抽走。public abstract class BuilderBaseTSelf, TProduct where TSelf : BuilderBaseTSelf, TProduct { protected abstract TProduct Create(); public TProduct Build() { Validate(); return Create(); } protected virtual void Validate() { } protected TSelf Self (TSelf)this; }用起来是这样public sealed class PromotionOrderBuilder : BuilderBasePromotionOrderBuilder, PromotionOrder { private string _orderNo string.Empty; private decimal _originalAmount; public PromotionOrderBuilder WithOrderNo(string orderNo) { _orderNo orderNo; return this; } public PromotionOrderBuilder WithAmount(decimal amount) { _originalAmount amount; return this; } protected override void Validate() { if (string.IsNullOrWhiteSpace(_orderNo)) throw new InvalidOperationException(订单号不能为空); if (_originalAmount 0) throw new InvalidOperationException(订单金额必须大于 0); } protected override PromotionOrder Create() new(_orderNo, _originalAmount); }这个骨架的好处是Build()一定是先校验、后创建的顺序不会有人忘记调Validate()Validate()是虚方法子类按需覆写不覆写就走空实现不会强制写一堆空方法。缺点也有Self属性要做一次强制转换如果泛型参数填错比如BuilderBaseOtherBuilder, PromotionOrder编译期不一定拦得住。这个问题可以用where TSelf : BuilderBaseTSelf, TProduct这个约束缓解它保证了TSelf确实是自己的子类但不能保证TProduct和TSelf的配对正确。我在一个含 8 个产品类的模块里用过这套骨架重复代码大概减少了 60%代价是新人第一次看BuilderBaseTSelf, TProduct会懵几分钟。所以我的建议是产品类少于 3 个别抽象手写更清楚超过 5 个再考虑抽基类。4.3 与依赖注入容器配合时的坑建造者如果需要在构建过程中调用外部服务比如根据客户等级查优惠规则就得注入依赖这时候和 DI 容器的配合就有讲究了。第一个坑是生命周期错配。如果建造者注册为Singleton但它内部持有的某个服务是Scoped这个Scoped实例会被提升为单例生命周期永远不过期。这在 ASP.NET Core 里是经典问题容器启动时可能直接报cannot consume scoped service from singleton的异常也可能因为校验被关掉而悄悄埋雷。第二个坑是建造者实例被复用。有人把建造者注册成Singleton后在请求里反复new出产品结果上一个请求的字段残留在建造者里下一个请求凭空多出一堆优惠规则。这个 bug 的表现极其诡异单次请求测试永远是好的只有并发或者连续请求才复现。我的做法是建造者一律Transient或者干脆不注册进容器由工厂方法按需创建反正它本身没有状态需要共享。第三个坑更隐蔽一些建造者持有服务引用产品却被长期缓存。如果产品对象被放进内存缓存而建造者通过注入的服务间接引用了请求上下文这个引用链会把整个上下文钉在内存里。这种内存泄漏用 dump 分析能看出来但定位过程相当痛苦。规避方式很简单 —— 建造者在Build()返回之前把所有需要的数据都拍平成值类型或纯数据不把任何服务引用带进产品。5. 踩坑实录建造者模式写歪的几种典型症状前面讲的都是怎么写对这一节讲怎么写歪。这些问题我在代码评审里反复见到有些我自己也犯过。5.1 建造者退化成万能 setter 集合最典型的走歪方式是建造者变成了一个拥有二十个WithXxx方法的巨无霸每个方法对应产品类的一个属性一一映射毫无业务语义。这种建造者和给每个属性开set没有本质区别只是把赋值挪了个地方中间态一样会暴露校验一样没做。判断标准很直接如果你的建造者方法和产品属性是 1:1 对应的那这个模式基本白用了。有价值的建造者方法通常是一个方法影响多个字段或者一个方法带一段业务判断。比如ApplyRule不只是往列表里塞一个字符串它可能还要判断互斥规则、计算叠加后的折扣。这种带语义的动作才是建造者的价值所在。我重构过一个很夸张的例子某个查询条件的建造者写了 34 个With方法其中 12 个是DateTime相关分别叫WithStartTime、WithEndTime、WithCreateStartTime、WithCreateEndTime……光看清楚哪个是哪个就要花五分钟。后来我把它们收敛成三个方法InTimeRange(from, to)、CreatedInRange(from, to)、OrderBy(...)方法数少了语义反而更清楚。5.2 复用同一建造者实例导致的字段串味这个坑前面提过值得单独展开因为它的复现条件很迷惑。我先还原一下现场// 错误示范 var builder new PromotionOrder.Builder() .WithOrderNo(PO-001) .WithCustomer(C-001) .WithAmount(100m); var order1 builder.Build(); // 后续代码又拿同一个 builder 改了几个字段 builder.WithOrderNo(PO-002).ApplyRule(FULL_100_MINUS_10); var order2 builder.Build();如果_appliedRules没做防御性复制order1的规则列表会跟着变长如果Build()里没做复制order1和order2会共享同一个List实例。更糟的是如果字段没被重置order2会继承order1的customerId、amount等一堆值 —— 因为建造者本来就是累积式的。我的应对办法有三个层次一是Build()里一定做深拷贝尤其是集合二是文档或注释里明确写建造者不是可复用对象一个实例只Build()一次三是在Build()末尾加一个_built标志重复调用直接抛异常private bool _built; public PromotionOrder Build() { if (_built) throw new InvalidOperationException(建造者实例已使用过请重新创建); // ... 校验与创建 _built true; return order; }这个检查很廉价但能把一类隐蔽的复用问题提前暴露。代价是如果业务确实需要复用你得改代码不过据我观察需要复用建造者的场景本身就很可疑。5.3 Build 不校验把错误推迟到运行时第三种走歪是Build()里什么都不做直接new出来返回校验交给下层去处理。这种写法看起来职责清晰实际上把错误发现的时间点从对象创建时推迟到了业务逻辑执行到一半时排查成本差了一个数量级。我遇到过一个真实场景订单构建时没校验金额等到调用支付接口时才报错这时候已经写了三条日志、扣了一次库存预占、发了一条站内消息回滚逻辑要写一大堆。如果在Build()里拦一下一行异常就解决了。给参数校验写异常信息的时候有个小习惯我坚持了很多年异常消息里一定要带上违规的实际值。订单金额必须大于 0不如订单金额必须大于 0当前值 -5有用。生产环境出问题时日志里那一句带值的信息能省掉你去翻调试器的时间。当然敏感字段比如证件号、手机号不要直接打出来脱敏处理后再拼进消息里。6. 一个可复用的建造者骨架与落地建议最后把前面散落的东西收拢成一个可以直接抄的骨架再补几条落地经验。6.1 骨架代码这是一个我目前在用的最小可用版本包含校验、防复用、集合拷贝三个要点public sealed class PromotionOrder { public string OrderNo { get; } public decimal Amount { get; } public IReadOnlyListstring Rules { get; } private PromotionOrder(string orderNo, decimal amount, IReadOnlyListstring rules) { OrderNo orderNo; Amount amount; Rules rules; } public static Builder CreateBuilder() new(); public sealed class Builder { private string? _orderNo; private decimal _amount; private readonly Liststring _rules new(); private bool _built; public Builder WithOrderNo(string orderNo) { _orderNo orderNo; return this; } public Builder WithAmount(decimal amount) { _amount amount; return this; } public Builder ApplyRule(string rule) { if (!string.IsNullOrWhiteSpace(rule)) _rules.Add(rule); return this; } public PromotionOrder Build() { if (_built) throw new InvalidOperationException(该建造者实例已使用请重新创建); if (string.IsNullOrWhiteSpace(_orderNo)) throw new InvalidOperationException(订单号不能为空); if (_amount 0) throw new InvalidOperationException($订单金额必须大于 0当前值 {_amount}); if (_rules.Count ! _rules.Distinct().Count()) throw new InvalidOperationException(存在重复的优惠规则); _built true; return new PromotionOrder(_orderNo!, _amount, _rules.ToArray()); } } }CreateBuilder()这个静态工厂方法看着多余实际很有用它让调用方不需要知道Builder是嵌套类这一点将来如果建造者要加依赖注入参数改这一个方法就行调用方不用动。6.2 落地时的几条经验第一优先考虑产品类是否需要部分构建能力。有些业务确实需要先建一半、过一会儿再补字段这种场景建造者反而不合适因为它的价值就在于一次性收口。这时候用可变的中间对象 显式Seal()方法更贴合业务。第二建造者方法的粒度按业务动作划分不按字段划分。ApplyDiscount、MarkUrgent、AssignWarehouse这类名字比SetDiscount、SetUrgent、SetWarehouse更能表达意图也更容易在方法内部塞校验逻辑。第三注意decimal和double的选择。涉及金额的字段一律用decimal这不是建造者模式的问题但在建造者的校验代码里特别容易写错。_amount 0对double来说是不可靠的比较对decimal则是精确的。我见过有人在建造者里用double存金额Build()时判断 0通过了结果下游算出来是-0.0000001。第四别为了模式而模式。我见过有人给一个只有两个字段的配置类写建造者最后代码量翻了三倍收益是零。判断标准回到开头那句字段之间有约束、有必填可选的混合、构造过程需要决策 —— 满足才上不满足就是给自己找活干。第五写完之后让同事看一眼调用链。好的建造者调用链读起来应该像一句自然语言描述比如创建订单 → 指定客户 → 加金额 → 应用两个规则 → 构建。如果读起来磕磕绊绊或者需要跳进方法里才能知道做了什么那说明方法命名或者职责划分出了问题这时候改代码比改注释管用。我自己现在的习惯是新写一个建造者之前先问自己一句这个对象的字段将来会不会有人绕过建造者直接 new如果产品类的构造函数是internal且程序集边界清晰答案是不会那这个建造者就能长期保持整洁如果答案模糊那再好的建造者设计也会被绕过它的人慢慢腐蚀这时候可能更该做的是收紧可见性而不是加更多方法。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询