第11章:代码整洁(建议140-153)
覆盖访问修饰符、大括号与命名、抽象层级/单一职责/长度、最小公开面、参数对象与DRY、表驱动/lambda、event accessors及注释和异常文档。
学习目标
- 能分析access、braces、names、abstraction level与public surface对阅读和兼容性的影响,并设计最小稳定API
- 能判断method/class拆分、parameter object、knowledge DRY、table-driven与lambda重构是否有真实change evidence
- 能比较event accessor、why/hazard comment、public docs和exception docs,设计可测试且不过期的contract说明
机制总览
第11章:代码整洁(建议140-153):机制路径
- 1
为什么整洁代码不是行数、注释数或Method数比赛
短method可能只是把一次阅读变成十次跳转,DRY可能把两个偶然相似的rules绑死,零注释也可能隐藏安全协议。真正目标是让一次change只触及一个cohesive owner、让public caller只依赖稳定capability、让failure和non-obvious constraints可被验证。
- 2
Readability与Minimum Surface(建…
C 默认使top-level type internal、class members private,这有助于least exposure;但“依赖默认”可能让reader在不同context猜visibility。现代做法是保持最小访问级别,并按项目style显式写出关键contract,由ana…
- 3
Cohesion、Knowledge与Data-Drive…
一组fields在多个signature中共同出现、具有cross-field invariant或共同lifetime时,提取parameter/value object,使illegal combination在construction被拒绝。不要只造无behavior data bag;新ty…
章级决策实验
第11章:代码整洁(建议140-153):机制与证据
切换《第11章:代码整洁(建议140-153)》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 为什么整洁代码不是行数、注释数或Method数比赛
短method可能只是把一次阅读变成十次跳转,DRY可能把两个偶然相似的rules绑死,零注释也可能隐藏安全协议。真正目标是让一次change只触及一个cohesive owner、让public caller只依赖稳定capability、让failure和non-obvious constraints可被验证。
可核验证据
固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「为什么整洁代码不是行数、注释数或Method数比赛」的收益与反例。
学完《第11章:代码整洁(建议140-153)》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
第11章:代码整洁(建议140-153):失效与核验
为什么整洁代码不是行数、注释数或Method数比赛
典型失效
若把「为什么整洁代码不是行数、注释数或Method数比赛」当作脱离版本与上下文的硬规则,可能用过时的优化或风格替换了更重要的正确性、安全性与可维护性约束。
核验证据
固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「为什么整洁代码不是行数、注释数或Method数比赛」的收益与反例。
Readability与Minimum Surface(建…
典型失效
若把「Readability与Minimum Surface(建…」当作脱离版本与上下文的硬规则,可能用过时的优化或风格替换了更重要的正确性、安全性与可维护性约束。
核验证据
固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「Readability与Minimum Surface(建…」的收益与反例。
Cohesion、Knowledge与Data-Drive…
典型失效
若把「Cohesion、Knowledge与Data-Drive…」当作脱离版本与上下文的硬规则,可能用过时的优化或风格替换了更重要的正确性、安全性与可维护性约束。
核验证据
固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「Cohesion、Knowledge与Data-Drive…」的收益与反例。
为什么整洁代码不是行数、注释数或Method数比赛
短method可能只是把一次阅读变成十次跳转,DRY可能把两个偶然相似的rules绑死,零注释也可能隐藏安全协议。真正目标是让一次change只触及一个cohesive owner、让public caller只依赖稳定capability、让failure和non-obvious constraints可被验证。
先预测:两个相同if是否必然抽取;private method只有一行是否必然更整洁;每个throw都写普通行注释是否足够。答案都是否。需要看knowledge、abstraction与caller contract。
↡外部consumer可调用、读取、修改、继承、订阅或实现的所有public/protected行为及其长期兼容承诺。Readability与Minimum Surface(建议140-146)
建议140:使用默认的访问修饰符
C#默认使top-level type internal、class members private,这有助于least exposure;但“依赖默认”可能让reader在不同context猜visibility。现代做法是保持最小访问级别,并按项目style显式写出关键contract,由analyzer阻止不必要public。重点是capability,不是省一个keyword。
internal sealed class InvoiceNumberSequence
{
private long _current;
public long Next() => Interlocked.Increment(ref _current);
}建议141:不知道该不该用大括号时,就用
if/for/while即使单statement也使用braces,降低后续添加logging/return时脱离control body的风险,并让formatter产生稳定diff。expression-bodied member、switch expression不是“省大括号”的同一问题,应按可读性选择。
if (invoice.IsOverdue)
{
await NotifyAsync(invoice, cancellationToken);
}建议142:总是提供有意义的命名
名字说明domain role、unit、lifetime或policy distinction,不重复type/scope已知信息。elapsedMilliseconds优于time,pendingInvoices优于data;极短local scope的index足够,不必写currentLoopIterationIndex。
建议143:方法抽象级别应在同一层次
orchestrator method每行应处于相同“why/how”层:load、validate、charge、persist;不要夹入SQL column、JSON token和byte offset。low-level mechanism下沉到named operation,但避免只转发一行且没有提高vocabulary的wrapper。
↡一段代码回答同一层why/what/how问题的粒度;同层statements共同形成可顺序阅读的叙事。public async Task CompleteCheckoutAsync(OrderId id, CancellationToken token)
{
Order order = await LoadOrderAsync(id, token);
Payment payment = await ChargeAsync(order, token);
await CommitAsync(order, payment, token);
}建议144:一个方法只做一件事
“一件事”是一个可命名goal和一个主要failure/transaction boundary,而非只调用一个function。判断方法:能否提取一段并用同一抽象层命名;局部变量是否分成不相交groups;测试是否因多个无关原因改变。
建议145:避免过长的方法和过长的类
length是smell,不是阈值。结合cyclomatic complexity、dependency count、state clusters、commit history和reasons to change决定拆分。一个长但线性的data mapping可能比十个跳转method清楚;一个短class若同时负责cache、I/O和policy仍不cohesive。
↡会独立推动module修改、测试和发布的business rule、dependency lifecycle或external contract变化来源。建议146:只对外公布必要的操作
public/protected member一旦被consumer使用就成为compatibility burden。默认private/internal,只公开use-case需要的commands/queries;test不要迫使implementation helper public,可通过public behavior或internal test visibility谨慎验证。
public interface IInvoiceApplication
{
Task<InvoiceView> GetAsync(InvoiceId id, CancellationToken token);
Task IssueAsync(IssueInvoice command, CancellationToken token);
}切换access、braces、name、abstraction与public surface
Cohesion、Knowledge与Data-Driven Refactoring(建议147-150)
建议147:重构多个相关属性为一个类
一组fields在多个signature中共同出现、具有cross-field invariant或共同lifetime时,提取parameter/value object,使illegal combination在construction被拒绝。不要只造无behavior data bag;新type要有domain name、validation、equality/serialization contract。
public readonly record struct DateRange
{
public DateRange(DateOnly start, DateOnly end)
{
if (end < start) throw new ArgumentException("End precedes start.");
(Start, End) = (start, end);
}
public DateOnly Start { get; }
public DateOnly End { get; }
}建议148:不重复代码
DRY针对knowledge duplication:同一business rule、schema mapping或calculation散落多处,变化必须同步。syntax相似但domain meaning不同不应抽到common helper,否则未来variation互相牵制。等到共同rule和variation axis清楚再抽象。
↡同一business rule或contract被编码在多处,导致一次规则变化需要同步修改并可能产生不一致。建议149:使用表驱动法避免过长的if和switch分支
branches只在key、constant或handler上不同且set稳定时,用immutable dictionary/table把data与selection分离;必须定义duplicate/unknown/default和initialization failure。复杂workflow、不同dependencies或开放extension更适合strategy polymorphism。
private static readonly IReadOnlyDictionary<OrderStatus, Func<Order, Transition>> Rules =
new Dictionary<OrderStatus, Func<Order, Transition>>
{
[OrderStatus.Pending] = order => Transition.Pay(order),
[OrderStatus.Paid] = order => Transition.Ship(order),
};table不是把logic藏进untyped configuration;entries保持typed,测试覆盖所有keys与unknown。
建议150:使用匿名方法、Lambda表达式代替方法
短、一次使用、纯且只在local query/callback有意义的behavior用lambda,减少远距离跳转;复杂、复用、递归、需要独立name/test或需要unsubscribe的handler保留named method。closure lifetime和allocation也要考虑。
Invoice[] overdue = invoices
.Where(invoice => invoice.DueDate < today && !invoice.IsPaid)
.OrderBy(invoice => invoice.DueDate)
.ToArray();切换parameter object、DRY、table、lambda与class split
Event Protection、Comments与Failure Contract(建议151-153)
建议151:使用事件访问器替换公开的事件成员变量
公开delegate field允许外部覆盖或raise invocation list;使用event关键字限制外部只能subscribe/unsubscribe,publisher保留raise权。custom add/remove accessor仅在转接、weak subscription或同步policy有明确需求时使用,并保持handler identity/lifetime。
public event EventHandler<InvoiceIssuedEventArgs>? InvoiceIssued;
protected virtual void OnInvoiceIssued(InvoiceIssuedEventArgs args)
=> InvoiceIssued?.Invoke(this, args);建议152:最少,甚至是不要注释
删除重复代码字面含义、已过期history和被better naming/extraction取代的注释;保留无法从syntax推导的why:protocol constraint、security/concurrency invariant、workaround原因与移除条件、public contract。注释必须随代码review/test维护。
建议153:若抛出异常,则必须要注释
对public/protected API,把caller可预防或处理的稳定exceptions写入XML <exception> documentation,说明type与condition;无需列举OutOfMemory等所有runtime failures。implementation附近只在throw原因/协议不显然时写why comment,且用tests证明condition。
/// <exception cref="InvoiceConflictException">
/// The persisted invoice version differs from the command's expected version.
/// </exception>
public Task IssueAsync(IssueInvoice command, CancellationToken token) => ExecuteAsync(command, token);切换event、why comment、API docs、exception docs与hazard comment
本章回顾:Clean意味着Change可以局部证明
- access最小化、braces稳定编辑、names表达domain;public surface只包含支持的capabilities。
- methods保持同一abstraction level,“一件事”由goal/failure boundary定义,length只提供smell。
- related properties在共享invariant时提取value object,DRY只消除knowledge duplication。
- table适合data-driven closed branches,lambda适合短local behavior,不机械替代strategy/named method。
- event保护raise ownership;comments解释why/hazard,不复述syntax。
- public exceptions通过XML docs和tests形成failure contract,不靠散落行注释。
练习
问题 1:一个150行method应该怎样决定拆不拆、怎么拆?
问题 2:两套折扣if/switch很相似,何时DRY、何时table、何时strategy?
问题 3:一个会抛ConflictException并发布事件的public API需要哪些说明和测试?
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- public capability
- abstraction level
- reason to change
- knowledge duplication
- failure documentation
原版目录概念补充核对
以下条目补齐官方目录中容易被示例主线掩盖的概念。它们不重复罗列目录,而是明确每项概念的机制、适用边界和验收证据。
建议141:不知道该不该用大括号时,就用:机制、边界与证据
- 代码整洁(建议140-153)中的建议141:不知道该不该用大括号时,就用是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议142:总是提供有意义的命名:机制、边界与证据
- 代码整洁(建议140-153)中的建议142:总是提供有意义的命名是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议143:方法抽象级别应在同一层次:机制、边界与证据
- 代码整洁(建议140-153)中的建议143:方法抽象级别应在同一层次是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议144:一个方法只做一件事:机制、边界与证据
- 代码整洁(建议140-153)中的建议144:一个方法只做一件事是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议145:避免过长的方法和过长的类:机制、边界与证据
- 代码整洁(建议140-153)中的建议145:避免过长的方法和过长的类是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议146:只对外公布必要的操作:机制、边界与证据
- 代码整洁(建议140-153)中的建议146:只对外公布必要的操作是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议147:重构多个相关属性为一个类:机制、边界与证据
- 代码整洁(建议140-153)中的建议147:重构多个相关属性为一个类是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议148:不重复代码:机制、边界与证据
- 代码整洁(建议140-153)中的建议148:不重复代码是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议149:使用表驱动法避免过长的if和switch分支:机制、边界与证据
- 代码整洁(建议140-153)中的建议149:使用表驱动法避免过长的if和switch分支是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议150:使用匿名方法、Lambda表达式代替方法:机制、边界与证据
- 代码整洁(建议140-153)中的建议150:使用匿名方法、Lambda表达式代替方法是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议151:使用事件访问器替换公开的事件成员变量:机制、边界与证据
- 代码整洁(建议140-153)中的建议151:使用事件访问器替换公开的事件成员变量是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议152:最少,甚至是不要注释:机制、边界与证据
- 代码整洁(建议140-153)中的建议152:最少,甚至是不要注释是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议153:若抛出异常,则必须要注释:机制、边界与证据
- 代码整洁(建议140-153)中的建议153:若抛出异常,则必须要注释跨越输入信任或资源生命周期边界,建议只有在明确威胁模型、所有权和失败路径后才成立。用恶意/畸形输入、失败注入和资源计数检查拒绝行为、敏感数据暴露与最终释放,而不是只验证顺利路径。