第12章:规范开发行为(建议154-157)
覆盖避免过度设计与持续重构、测试和生产代码共同版本化、attribute/feature/protocol多版本选择,以及从首个UI开始的分层自动化与发布证据。
学习目标
- 能分析current evidence、reversibility与public migration cost,设计不过度但保留refactoring option的最小实现
- 能设计unit/contract/integration/UI/production test ownership,使测试与生产代码在同一change中版本化和验收
- 能比较attribute metadata、feature flag、protocol version与release gate,判断selector、rollout、compatibility和cleanup责任
机制总览
第12章:规范开发行为(建议154-157):机制路径
- 1
为什么开发行为必须把“以后再改”变成可兑现承诺
YAGNI若没有tests会变成“不敢改”,自动化若只在项目末期补会得到不可测试UI和不可控环境,feature flag若没有owner/expiry则成为永久分支。规范行为不是流程表格,而是让每个change带着证据、rollback和下一次演进option进入仓库。
- 2
Evidence-Driven Design与Refact…
只为当前已知use case实现最简单cohesive design,不为假想十种variants创建plugin framework;当第二个真实variant、量测到的scale或重复change出现时,再提取abstraction。与此同时,public schema/protocol/dat…
- 3
Test Code也是Production Asset(建…
behavior change、test和必要fixture在同一commit/PR,reviewer看到requirement如何被证明;test source与production使用同一version、branch和ownership。不要把tests当一次性脚本或另一个长期不同步仓库,除非c…
章级决策实验
第12章:规范开发行为(建议154-157):机制与证据
切换《第12章:规范开发行为(建议154-157)》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 为什么开发行为必须把“以后再改”变成可兑现承诺
YAGNI若没有tests会变成“不敢改”,自动化若只在项目末期补会得到不可测试UI和不可控环境,feature flag若没有owner/expiry则成为永久分支。规范行为不是流程表格,而是让每个change带着证据、rollback和下一次演进option进入仓库。
可核验证据
固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「为什么开发行为必须把“以后再改”变成可兑现承诺」的收益与反例。
学完《第12章:规范开发行为(建议154-157)》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
第12章:规范开发行为(建议154-157):失效与核验
为什么开发行为必须把“以后再改”变成可兑现承诺
典型失效
若把「为什么开发行为必须把“以后再改”变成可兑现承诺」当作脱离版本与上下文的硬规则,可能用过时的优化或风格替换了更重要的正确性、安全性与可维护性约束。
核验证据
固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「为什么开发行为必须把“以后再改”变成可兑现承诺」的收益与反例。
Evidence-Driven Design与Refact…
典型失效
若把「Evidence-Driven Design与Refact…」当作脱离版本与上下文的硬规则,可能用过时的优化或风格替换了更重要的正确性、安全性与可维护性约束。
核验证据
固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「Evidence-Driven Design与Refact…」的收益与反例。
Test Code也是Production Asset(建…
典型失效
若把「Test Code也是Production Asset(建…」当作脱离版本与上下文的硬规则,可能用过时的优化或风格替换了更重要的正确性、安全性与可维护性约束。
核验证据
固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「Test Code也是Production Asset(建…」的收益与反例。
为什么开发行为必须把“以后再改”变成可兑现承诺
YAGNI若没有tests会变成“不敢改”,自动化若只在项目末期补会得到不可测试UI和不可控环境,feature flag若没有owner/expiry则成为永久分支。规范行为不是流程表格,而是让每个change带着证据、rollback和下一次演进option进入仓库。
先预测:避免过度设计是否意味着不设计schema migration;测试是否可以放在另一个长期滞后的仓库;第一个UI只是静态页面时是否可等功能齐全再自动化。三个答案都是否。
↡当前实现有明确替换边界、characterization/contract tests和migration路径,使未来变化无需big-bang rewrite的能力。Evidence-Driven Design与Refactoring(建议154)
建议154:不要过度设计,在敏捷中体会重构的乐趣
只为当前已知use case实现最简单cohesive design,不为假想十种variants创建plugin framework;当第二个真实variant、量测到的scale或重复change出现时,再提取abstraction。与此同时,public schema/protocol/data migration等难逆决策必须提前设计compatibility,不能拿YAGNI逃避。
public sealed class InvoiceExporter
{
public Task ExportCsvAsync(Invoice invoice, Stream output, CancellationToken token)
=> WriteCsvAsync(invoice, output, token);
}当JSON成为真实需求,再提取IInvoiceFormatter并用contract tests约束两种implementation;不是第一天先建formatter registry、discovery、hot reload。
可逆与不可逆决策分开
private method shape、internal class拆分通常可逆,可快速试验;published API、database schema、wire version、encrypted format和external event不可轻易回滚,需要decision record、compatibility matrix与migration。设计投入跟reversal cost成比例,而非一律“先简单”。
小步演进闭环
每步保持green:补characterization,做一个结构变化,跑tests/observability,删除旧path;不要同时重写behavior、storage和deployment。refactor commit与feature change可逻辑分离,reviewer能验证行为未变。
切换current need、second variant、scale、legacy与irreversible choice
Test Code也是Production Asset(建议155)
建议155:随生产代码一起提交单元测试代码
behavior change、test和必要fixture在同一commit/PR,reviewer看到requirement如何被证明;test source与production使用同一version、branch和ownership。不要把tests当一次性脚本或另一个长期不同步仓库,除非cross-system test有明确独立release contract。
↡负责production behavior的团队同时负责对应test、fixture、failure triage、flakiness和长期维护,不把验证债务转给下游。[Fact]
public void DateRange_rejects_end_before_start()
{
Action construct = () => new DateRange(new DateOnly(2026, 7, 2), new DateOnly(2026, 7, 1));
construct.Should().Throw<ArgumentException>();
}分层而不是只追求unit数量
unit验证policy/value,contract运行所有implementations,integration验证database/queue/filesystem,UI journey覆盖critical path,deployed smoke验证environment/config。每层解决不同risk;只做unit会漏wiring,只做E2E则慢且难定位。
Flaky test是产品缺陷
随机time、shared global state、unowned fixture、eventual consistency和brittle selector会制造flakiness。记录owner,修determinism或明确quarantine deadline;不能反复rerun直到绿,更不能长期关闭gate。
切换unit、contract、integration、UI journey与production check
Version Selection与Automation from First UI(建议156-157)
建议156:利用特性为应用程序提供多个版本
attribute可在type/member上声明version/capability metadata,由composition root集中发现和选择,适合compile-time known implementations;不要让业务各处反射扫描。runtime rollout常用feature flag,wire compatibility用explicit protocol version,它们有不同selector和lifetime。
↡attribute、request version、tenant cohort或feature flag中明确决定使用哪套implementation/contract且可审计的输入。[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class ProtocolVersionAttribute(int version) : Attribute
{
public int Version { get; } = version;
}
[ProtocolVersion(2)]
public sealed class InvoiceProtocolV2 : IInvoiceProtocol { }selection必须处理duplicate/missing/unsupported version并fail closed。feature flag有owner、metric、rollback和expiry;migration完成删除旧implementation和flag,防止组合爆炸。
建议157:从写第一个界面开始,就进行自动化测试
首个UI同时建立testability seam:stable role/test id、可注入clock/network、deterministic data、可观察loading/error/success state。先自动化一条critical journey,而不是等所有screens完成后再改造selector与state management。
↡用大量fast unit/contract、较少integration和最少critical UI journeys组成,并以deployed smoke补环境证据的风险分层。<button type="submit" aria-label="Issue invoice" disabled={isSubmitting}>
Issue
</button>UI test断言用户outcome和accessible role,不依赖CSS class、pixel coordinate或内部component tree;visual regression只覆盖layout-specific risk。viewport、keyboard、loading/failure和retry属于首批关键cases。
Release gate收口版本与测试
CI先跑type/lint/unit/contract,再按change触发integration/UI;deploy后执行smoke并保留artifact/version evidence。gate失败不push/promote,flaky check有owner和修复deadline。版本selector的每个active branch都必须在矩阵中有证据。
切换attribute、flag、protocol、UI seam与release gate
本章回顾:每次Change都带着下一次Change的证据
- 当前需求用最小直接设计,真实第二variant再抽象;不可逆schema/protocol仍需提前migration设计。
- refactoring safety net让small reversible steps可持续,不做big-bang rewrite。
- test与production同commit、同owner、同version;unit/contract/integration/UI/smoke各自覆盖不同risk。
- attribute、feature flag和protocol version是不同selectors,都要处理unsupported、rollout、rollback和deletion。
- 第一个UI就建立stable selectors、injectable dependencies与critical journey automation。
- release gate汇总代码、版本矩阵与部署证据,失败不promote。
练习
问题 1:何时应从直接实现提取interface,何时属于过度设计?
问题 2:一个功能PR应怎样同时提交测试并控制flakiness?
问题 3:attribute version、feature flag和protocol version如何共同存在而不失控?
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- design option
- refactoring safety net
- test ownership
- version selector
- automation pyramid
原版目录概念补充核对
以下条目补齐官方目录中容易被示例主线掩盖的概念。它们不重复罗列目录,而是明确每项概念的机制、适用边界和验收证据。
建议155:随生产代码一起提交单元测试代码:机制、边界与证据
- 规范开发行为(建议154-157)中的建议155:随生产代码一起提交单元测试代码是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议156:利用特性为应用程序提供多个版本:机制、边界与证据
- 规范开发行为(建议154-157)中的建议156:利用特性为应用程序提供多个版本是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议157:从写第一个界面开始,就进行自动化测试:机制、边界与证据
- 规范开发行为(建议154-157)中的建议157:从写第一个界面开始,就进行自动化测试是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。