第8章:类型设计(建议102-112)

覆盖接口/抽象类、组合/继承、多态,单例/静态构造/静态类、sealed,以及嵌套类型、enum与rich class、双向耦合和类/命名空间建模。

学习目标

  • 能比较interface、abstract class、composition和inheritance的state、variation与substitutability contract,并设计最小扩展面
  • 能分析private singleton、static constructor/class、DI lifetime和sealed type的construction、failure与test isolation
  • 能判断nested type、enum/rich class、one-way dependency和namespace是否形成清晰domain与module boundary

机制总览

第8章:类型设计(建议102-112):机制路径

  1. 1

    为什么类型图首先是一张Ownership和Dependen…

    继承箭头、namespace和singleton写法很容易画,困难的是证明caller能安全替换subtype、shared instance由谁创建/释放、两个objects谁依赖谁,以及一组values是closed set还是会携带持续演化的behavior。类型设计的质量来自这些约束,而不是使用了多少pattern。

  2. 2

    Contract、Reuse与Polymorphic Br…

    interface表达role/capability,让unrelated types实现多个contracts,适合dependency boundary;abstract class适合一条有共同state、protected invariant和设计过virtual hooks的hierarc…

  3. 3

    Global Lifetime、Initializatio…

    private constructor阻止外部new,配合static holder或 Lazy 可保证一个process/type-context instance。但singleton隐藏global dependency、共享mutable state与test order;现代applica…

先按顺序建立机制,再进入实验切换阶段并检查失效证据。

章级决策实验

第8章:类型设计(建议102-112):机制与证据

切换《第8章:类型设计(建议102-112)》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。

选择推理阶段

当前阶段 · 为什么类型图首先是一张Ownership和Dependen…

继承箭头、namespace和singleton写法很容易画,困难的是证明caller能安全替换subtype、shared instance由谁创建/释放、两个objects谁依赖谁,以及一组values是closed set还是会携带持续演化的behavior。类型设计的质量来自这些约束,而不是使用了多少pattern。

可核验证据

固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「为什么类型图首先是一张Ownership和Dependen…」的收益与反例。

学完《第8章:类型设计(建议102-112)》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。

失效—证据矩阵

第8章:类型设计(建议102-112):失效与核验

为什么类型图首先是一张Ownership和Dependen…

典型失效

若把「为什么类型图首先是一张Ownership和Dependen…」当作脱离版本与上下文的硬规则,可能用过时的优化或风格替换了更重要的正确性、安全性与可维护性约束。

核验证据

固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「为什么类型图首先是一张Ownership和Dependen…」的收益与反例。

Contract、Reuse与Polymorphic Br…

典型失效

若把「Contract、Reuse与Polymorphic Br…」当作脱离版本与上下文的硬规则,可能用过时的优化或风格替换了更重要的正确性、安全性与可维护性约束。

核验证据

固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「Contract、Reuse与Polymorphic Br…」的收益与反例。

Global Lifetime、Initializatio…

典型失效

若把「Global Lifetime、Initializatio…」当作脱离版本与上下文的硬规则,可能用过时的优化或风格替换了更重要的正确性、安全性与可维护性约束。

核验证据

固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「Global Lifetime、Initializatio…」的收益与反例。

每个判断都必须能落到观测、测试或产物,不能只凭代码表面推测。

为什么类型图首先是一张Ownership和Dependency图

继承箭头、namespace和singleton写法很容易画,困难的是证明caller能安全替换subtype、shared instance由谁创建/释放、两个objects谁依赖谁,以及一组values是closed set还是会携带持续演化的behavior。类型设计的质量来自这些约束,而不是使用了多少pattern。

先预测:只为复用三行代码是否足以继承;private constructor的singleton是否天然易测试;把enum改成class是否永远更面向对象。答案都是否。先定义variation、lifetime、dependency direction和compatibility,再选择language shape。

Contract、Reuse与Polymorphic Branch(建议102-104)

建议102:区分接口和抽象类的应用场合

interface表达role/capability,让unrelated types实现多个contracts,适合dependency boundary;abstract class适合一条有共同state、protected invariant和设计过virtual hooks的hierarchy。现代default interface members能帮助部分API演进,但不让interface变成shared instance-state owner。

public interface IClock
{
    DateTimeOffset UtcNow { get; }
}
 
public abstract class AuditedEntity
{
    protected AuditedEntity(EntityId id, DateTimeOffset createdAt)
        => (Id, CreatedAt) = (id, createdAt);
 
    public EntityId Id { get; }
    public DateTimeOffset CreatedAt { get; }
}

若caller只需要clock capability,不应被迫继承base class。若derived types必须共享construction invariant,abstract base比复制interface implementation更诚实。

建议103:区分组合和继承的应用场合

composition显式持有collaborators,variation可独立替换,默认更适合代码复用;inheritance要求真实is-a和完整substitutability proof,并把derived生命周期绑在base evolution上。不要为复用implementation继承一个语义不匹配的type。

public sealed class InvoiceService
{
    private readonly ITaxPolicy _taxPolicy;
    private readonly IInvoiceStore _store;
 
    public InvoiceService(ITaxPolicy taxPolicy, IInvoiceStore store)
        => (_taxPolicy, _store) = (taxPolicy, store);
}

composition也可能变成层层转发。只有存在真实variation/ownership或隔离外部依赖时引入collaborator,不为每个method制造interface。

建议104:用多态代替条件语句

switch反复按同一type/kind分支且每次添加variant都修改多处时,strategy/polymorphism把behavior放回variant;但小型closed enum、exhaustive pattern matching和一次性branch可能更清晰。目标是减少shotgun modification,不是消灭所有条件语句。

public interface IShippingPolicy
{
    Money Quote(Shipment shipment);
}
 
public sealed class ExpressShippingPolicy : IShippingPolicy
{
    public Money Quote(Shipment shipment) => shipment.Weight * Rates.Express;
}

验收要新增一个variant:是否只增加implementation和registration,还是修改多个unrelated consumers;同时确认strategy没有把data access、logging等全部塞进一个class。

分步1 / 3

切换interface、abstract、composition、inheritance与polymorphism

Global Lifetime、Initialization与Extensibility(建议105-108)

建议105:使用私有构造函数强化单例

private constructor阻止外部new,配合static holder或Lazy<T>可保证一个process/type-context instance。但singleton隐藏global dependency、共享mutable state与test order;现代application service通常由DI container以singleton lifetime创建,并让consumer依赖interface。

public sealed class LegacyRegistry
{
    private static readonly Lazy<LegacyRegistry> Current = new(() => new LegacyRegistry());
    private LegacyRegistry() { }
    public static LegacyRegistry Instance => Current.Value;
}

只有确实global identity且无request/user scope时才用。clock、random、cache、configuration等更适合inject,测试可替换且disposal由host拥有。

建议106:为静态类添加静态构造函数

explicit static constructor可执行一次type initialization,并影响runtime的beforefieldinit优化时机;适合必须原子建立的in-memory deterministic state。不要在其中做network/file I/O、依赖ambient configuration或长时间阻塞,失败会形成TypeInitializationException,使后续type使用持续失败且难恢复。

public static class UnitTable
{
    public static readonly ImmutableDictionary<string, decimal> Factors;
 
    static UnitTable()
    {
        Factors = new Dictionary<string, decimal>(StringComparer.Ordinal)
        {
            ["m"] = 1m,
            ["cm"] = 0.01m,
        }.ToImmutableDictionary(StringComparer.Ordinal);
    }
}

能用inline field initializer清楚表达时无需额外static ctor。外部failure-prone initialization放到host startup service并显式报告/retry。

建议107:区分静态类和单例

static class没有instance、不能实现instance interface或参与普通polymorphism,适合stateless pure helpers/constants;singleton仍是object,可实现interface、持有disposable dependencies并由lifetime owner管理。static mutable state和singleton mutable state都可能global coupling,区别不在关键词而在ownership/substitution。

public static class MoneyMath
{
    public static decimal Round(decimal amount) => decimal.Round(amount, 2);
}
 
public sealed class ExchangeRateService : IExchangeRateService, IAsyncDisposable
{
    // Instance dependencies and lifecycle belong to the host.
}

建议108:将类型标识为sealed

未明确支持inheritance的concrete type可sealed,关闭未知override、保护constructor/dispose invariant并保留未来内部变化自由。unsealed public class事实上承诺了extension surface;一旦外部subclass依赖protected/virtual behavior,compatibility成本上升。

sealed不是单纯performance micro-optimization,也不妨碍type实现interfaces供substitution。只有为继承设计、记录virtual contracts并提供contract tests时才开放。

分步1 / 3

切换private singleton、static ctor/class、DI singleton与sealed

Modeling、Coupling与Namespace(建议109-112)

建议109:谨慎使用嵌套类

private nested type适合只服务owner implementation、共享concept/lifetime且外部无独立意义的helper;public nested type把consumer命名与outer type绑定,容易形成过强coupling和发现困难。C# nested type即使private access规则便利,也不意味着自动共享outer instance。

public sealed class LruCache<TKey, TValue> where TKey : notnull
{
    private sealed class Entry
    {
        public required TKey Key { get; init; }
        public required TValue Value { get; set; }
    }
}

若nested type开始被多个owners使用、拥有独立tests或public identity,应提升到namespace/module级type。

建议110:用类来代替enum

当每个named value需要behavior、metadata、validation、外部扩展或版本映射时,用sealed rich value class/record或strategy比不断扩大的switch更合适。若集合确实small、closed、无额外state且用于flags/state,enum更简单、可exhaustive分析。

public sealed record PaymentMethod(string Code, bool RequiresRedirect)
{
    public static readonly PaymentMethod Card = new("card", false);
    public static readonly PaymentMethod BankRedirect = new("bank_redirect", true);
}

“smart enum”仍要处理equality、serialization、unknown value和registry extension;不要只把enum成员搬成static fields。

建议111:避免双向耦合

A直接持有B、B又调用A会形成construction、lifetime、testing和change cycle。确定dependency direction:stable domain定义port,orchestrator注入implementation;反向通知通过return value、domain event或narrow callback完成。event subscription若双方互持也可能形成lifetime cycle。

public sealed class CheckoutService
{
    private readonly IPaymentGateway _gateway;
    private readonly IOrderRepository _orders;
 
    public CheckoutService(IPaymentGateway gateway, IOrderRepository orders)
        => (_gateway, _orders) = (gateway, orders);
}

domain aggregate内部的parent-child back reference有时合理,但要由aggregate owner控制、不可跨module任意调用,并测试serialization/lifetime。

建议112:将现实世界中的对象抽象为类,将可复用对象圈起来就是命名空间

不要逐字把每个现实名词变class;model只包含系统需要维护的identity、value、behavior和rules。namespace用于组织cohesive public vocabulary与discoverability,但不提供runtime isolation、deployment或dependency enforcement;真正module boundary还需要assembly/project/package、visibility和dependency rules。

namespace Billing.Invoicing;
 
public sealed class Invoice { }
public readonly record struct InvoiceId(Guid Value);
internal sealed class InvoiceNumberSequence { }

以bounded context/use case命名,不建HelpersCommon等无限收纳空间。可复用应由稳定contract和真实多个consumer证明,而非先圈一个namespace。

分步1 / 3

切换nested、enum/rich class、one-way dependency与namespace

本章回顾:Type Shape要匹配Variation、Lifetime与Dependency

  1. interface表达role,abstract class拥有shared hierarchy invariant;composition默认隔离variation,inheritance必须证明LSP。
  2. polymorphism减少重复type switch,但small closed branch可保留enum/pattern matching。
  3. private singleton和static state都是global lifetime风险;DI singleton让creation、replacement与disposal显式。
  4. static constructor保持快速确定,sealed关闭未承诺的继承扩展面。
  5. nested type只服务owner时有价值,rich class只在enum values需要behavior/open extension时成立。
  6. dependency保持单向,namespace组织词汇但不代替assembly/package边界。

练习

问题 1:何时选abstract class而不是interface+composition?

问题 2:一个共享cache为何更适合DI singleton而不是static Instance?

问题 3:什么时候enum应保留,什么时候升级为rich class或strategy?

术语表

名词解释

本章出现的专业名词,用大白话再讲一遍。

substitutability proof
composition boundary
global lifetime
closed value set
dependency direction

原版目录概念补充核对

以下条目补齐官方目录中容易被示例主线掩盖的概念。它们不重复罗列目录,而是明确每项概念的机制、适用边界和验收证据。

建议103:区分组合和继承的应用场合:机制、边界与证据

  1. 类型设计(建议102-112)中的建议103:区分组合和继承的应用场合是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议104:用多态代替条件语句:机制、边界与证据

  1. 类型设计(建议102-112)中的建议104:用多态代替条件语句是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议105:使用私有构造函数强化单例:机制、边界与证据

  1. 类型设计(建议102-112)中的建议105:使用私有构造函数强化单例是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议106:为静态类添加静态构造函数:机制、边界与证据

  1. 类型设计(建议102-112)中的建议106:为静态类添加静态构造函数是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议107:区分静态类和单例:机制、边界与证据

  1. 类型设计(建议102-112)中的建议107:区分静态类和单例是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议108:将类型标识为sealed:机制、边界与证据

  1. 类型设计(建议102-112)中的建议108:将类型标识为sealed是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议109:谨慎使用嵌套类:机制、边界与证据

  1. 类型设计(建议102-112)中的建议109:谨慎使用嵌套类是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议110:用类来代替enum:机制、边界与证据

  1. 类型设计(建议102-112)中的建议110:用类来代替enum是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议111:避免双向耦合:机制、边界与证据

  1. 类型设计(建议102-112)中的建议111:避免双向耦合是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议112:将现实世界中的对象抽象为类,将可复用对象圈起来就是命名空间:机制、边界与证据

  1. 类型设计(建议102-112)中的建议112:将现实世界中的对象抽象为类,将可复用对象圈起来就是命名空间是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

讨论

评论区加载中…