第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. 1

    为什么开发行为必须把“以后再改”变成可兑现承诺

    YAGNI若没有tests会变成“不敢改”,自动化若只在项目末期补会得到不可测试UI和不可控环境,feature flag若没有owner/expiry则成为永久分支。规范行为不是流程表格,而是让每个change带着证据、rollback和下一次演进option进入仓库。

  2. 2

    Evidence-Driven Design与Refact…

    只为当前已知use case实现最简单cohesive design,不为假想十种variants创建plugin framework;当第二个真实variant、量测到的scale或重复change出现时,再提取abstraction。与此同时,public schema/protocol/dat…

  3. 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只是静态页面时是否可等功能齐全再自动化。三个答案都是否。

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能验证行为未变。

分步1 / 3

切换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。

[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。

分步1 / 3

切换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。

[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。

<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都必须在矩阵中有证据。

分步1 / 3

切换attribute、flag、protocol、UI seam与release gate

本章回顾:每次Change都带着下一次Change的证据

  1. 当前需求用最小直接设计,真实第二variant再抽象;不可逆schema/protocol仍需提前migration设计。
  2. refactoring safety net让small reversible steps可持续,不做big-bang rewrite。
  3. test与production同commit、同owner、同version;unit/contract/integration/UI/smoke各自覆盖不同risk。
  4. attribute、feature flag和protocol version是不同selectors,都要处理unsupported、rollout、rollback和deletion。
  5. 第一个UI就建立stable selectors、injectable dependencies与critical journey automation。
  6. 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:随生产代码一起提交单元测试代码:机制、边界与证据

  1. 规范开发行为(建议154-157)中的建议155:随生产代码一起提交单元测试代码是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议156:利用特性为应用程序提供多个版本:机制、边界与证据

  1. 规范开发行为(建议154-157)中的建议156:利用特性为应用程序提供多个版本是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议157:从写第一个界面开始,就进行自动化测试:机制、边界与证据

  1. 规范开发行为(建议154-157)中的建议157:从写第一个界面开始,就进行自动化测试是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

讨论

评论区加载中…