第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
为什么类型图首先是一张Ownership和Dependen…
继承箭头、namespace和singleton写法很容易画,困难的是证明caller能安全替换subtype、shared instance由谁创建/释放、两个objects谁依赖谁,以及一组values是closed set还是会携带持续演化的behavior。类型设计的质量来自这些约束,而不是使用了多少pattern。
- 2
Contract、Reuse与Polymorphic Br…
interface表达role/capability,让unrelated types实现多个contracts,适合dependency boundary;abstract class适合一条有共同state、protected invariant和设计过virtual hooks的hierarc…
- 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。
↡每个subtype对base contract的precondition、postcondition、invariant、exception和lifetime逐项保持,使base caller无需识别具体type。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。
↡owner只通过明确interface持有collaborator,并定义其creation、lifetime与调用能力,使behavior可独立替换的边界。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。
切换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拥有。
↡一个instance从application/type context启动持续到结束、被所有consumer共享并可能造成跨request与跨test state传播的生命周期。建议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时才开放。
切换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分析。
↡成员由协议或domain固定、consumer可以合理exhaustive处理且新增值被视为compatibility事件的值集合。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。
↡compile-time与runtime依赖从policy/owner指向明确abstraction或从outer orchestrator连接两端,使变化和lifetime不形成闭环。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命名,不建Helpers、Common等无限收纳空间。可复用应由稳定contract和真实多个consumer证明,而非先圈一个namespace。
切换nested、enum/rich class、one-way dependency与namespace
本章回顾:Type Shape要匹配Variation、Lifetime与Dependency
- interface表达role,abstract class拥有shared hierarchy invariant;composition默认隔离variation,inheritance必须证明LSP。
- polymorphism减少重复type switch,但small closed branch可保留enum/pattern matching。
- private singleton和static state都是global lifetime风险;DI singleton让creation、replacement与disposal显式。
- static constructor保持快速确定,sealed关闭未承诺的继承扩展面。
- nested type只服务owner时有价值,rich class只在enum values需要behavior/open extension时成立。
- 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:区分组合和继承的应用场合:机制、边界与证据
- 类型设计(建议102-112)中的建议103:区分组合和继承的应用场合是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议104:用多态代替条件语句:机制、边界与证据
- 类型设计(建议102-112)中的建议104:用多态代替条件语句是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议105:使用私有构造函数强化单例:机制、边界与证据
- 类型设计(建议102-112)中的建议105:使用私有构造函数强化单例是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议106:为静态类添加静态构造函数:机制、边界与证据
- 类型设计(建议102-112)中的建议106:为静态类添加静态构造函数是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议107:区分静态类和单例:机制、边界与证据
- 类型设计(建议102-112)中的建议107:区分静态类和单例是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议108:将类型标识为sealed:机制、边界与证据
- 类型设计(建议102-112)中的建议108:将类型标识为sealed是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议109:谨慎使用嵌套类:机制、边界与证据
- 类型设计(建议102-112)中的建议109:谨慎使用嵌套类是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议110:用类来代替enum:机制、边界与证据
- 类型设计(建议102-112)中的建议110:用类来代替enum是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议111:避免双向耦合:机制、边界与证据
- 类型设计(建议102-112)中的建议111:避免双向耦合是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议112:将现实世界中的对象抽象为类,将可复用对象圈起来就是命名空间:机制、边界与证据
- 类型设计(建议102-112)中的建议112:将现实世界中的对象抽象为类,将可复用对象圈起来就是命名空间是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。