18.3 层超类型

为某一层的全部类型提供共同超类型,集中该层共享的基础行为。

学习目标

  • 能识别同一层多个类型真正共享的身份、脏标记或版本机制,并说明为何不应共享业务行为
  • 能编写 TypeScript 层超类型及子类,验证继承边界、状态更新和并发版本规则
  • 能根据重复代码、类型边界和继承耦合的证据,判断何时采用或拒绝层超类型

为什么 18.3 层超类型值得单独学习

客户、订单和商品对象可能都要记录数据库身份、是否修改以及乐观锁版本。如果每个类各自实现这些机制,修复一个持久化缺陷就要同步修改多个类型;但如果把所有便利方法、领域规则和外部服务都塞进基类,基类又会成为不可审查的上帝对象。

的边界是“同一层、同一责任、真正稳定的共同机制”。它可以集中 ID、脏标记和版本检查,却不应让 Customer、Order 和 Product 因为继承关系被迫拥有彼此无关的业务行为。

本章只覆盖 2024 年中文版公开目录中的 18.3 层超类型,依据作者图书页和公开模式目录独立重写。案例、实验、代码、练习和答案均为本课程原创,不复现原书正文、插图或代码。

先建立直觉:把重复机制放在正确楼层

先比较三个类的变化原因:身份字段由持久化层约束,脏标记由变更跟踪约束,而“客户是否可下单”“订单是否可取消”分别由领域规则约束。只有前两类机制具有跨多个类型的同一语义,才有资格进入共同超类型。

需要通过三个问题验证:所有子类是否都需要它?删除它会不会让每个子类重复相同代码?它是否不依赖某个具体业务概念?任一问题回答为否,都应考虑组合、接口或保持局部实现。

继承还带来。层超类型不是免费复用;团队必须记录基类变化影响的类型数量、子类是否能独立测试,以及是否需要把共性机制替换成组合。

目录单元到教学证据

18.3 层超类型

本章精确对应 manifest 单元 poeaa24-pattern-43-layer-supertype。案例用 Customer、Order 和 Product 表示同一领域层的三个类型,学习边界是把身份、脏标记和版本放入公共基础,同时让具体业务规则留在子类或领域服务。

完成本单元要交出三样证据:一张超类型与子类的责任图;一段带版本和脏标记的 TypeScript 代码;一个拒绝样本,证明业务便利方法进入基类后会造成继承耦合或语义污染。

专属案例:持久化实体的共同基础

订单系统的 Customer、Order 和 Product 都需要 idversionisDirty。它们不应该都重新写 markDirty 和版本条件检查;但 Customer 的 canPlaceOrder、Order 的 cancel 和 Product 的 reserve 不共享同一语义,不能为了少写几行代码移动到基类。

评审卡记录如下:单元键为 poeaa24-pattern-43-layer-supertype;模式族为 base;采用条件是“同一层至少有多个类型拥有稳定、无业务特化的共同机制”;观测项包括重复实现数量、基类变化影响面、子类测试隔离和组合替代成本;拒绝条件是“基类需要知道具体业务类型,或子类只是为了继承便利方法而被迫接受无关状态”。

专属可视化实验:看共性停在哪里

先预测:markDirty 可以放进基类,但 cancel() 是否也应该放进去?观察图中的共同字段、继承关系和子类边界,再判断一个新类型是否真的属于同一层,而不是只因为它也有一个 ID。

Layer Supertype:层内共性提取到基类DomainObject(基类)id: LongisDirty / markDirty()CustomerOrderProduct• 只放真正跨层内类型的共同机制(ID、脏标记、乐观锁版本)• 拒绝膨胀:与具体业务相关的便利方法不属于超类型层内共性机制提取到基类,子类只关注业务差异
层超类型将该层所有类型共有的机制(ID、脏标记等)提取到基类中, 子类只关注业务差异,避免重复代码。

代码实践:把基础机制限制在超类型

下面的代码只把身份、版本和脏标记放入 PersistentEntity。子类保留自己的业务行为,测试可以验证基类机制而不要求基类了解订单或客户。

abstract class PersistentEntity {
  private dirty = false;
 
  constructor(
    public readonly id: string,
    public readonly version: number,
  ) {}
 
  protected markDirty(): void {
    this.dirty = true;
  }
 
  isDirty(): boolean {
    return this.dirty;
  }
 
  nextVersion(expected: number): number {
    if (expected !== this.version) throw new Error("stale entity");
    return this.version + 1;
  }
}
 
class Customer extends PersistentEntity {
  rename(name: string): void {
    if (!name.trim()) throw new Error("name is required");
    this.markDirty();
  }
}
 
class Order extends PersistentEntity {
  cancel(): void {
    // 订单取消规则属于 Order,不属于所有持久化实体的共同机制。
    this.markDirty();
  }
}

这个边界有四个可验收点:基类不导入 Customer 或 Order;共同状态可被所有子类复用;业务方法仍位于具体类型;旧版本写入会失败。若有人把 sendEmailcalculateDiscount 加到 PersistentEntity,应先要求说明它为何对每个子类都有同一语义。

选择与拒绝矩阵

评审问题采用层超类型的证据应拒绝或换方案的信号
范围多个同层类型拥有同一基础机制子类来自不同层或变化原因不同
语义ID、版本和脏标记含义完全一致同名字段在不同类型有不同生命周期
耦合基类不认识具体业务类型基类依赖 Customer、Order 等细节
测试子类可独立测试,基类可单独验证修改基类需要重跑所有业务场景
替代继承表达稳定的 is-a 关系只是为了复用一两个便利函数

常见误区

本章小结

  • 层超类型集中同一层多个类型真正共享的基础机制,而不是所有可复用代码。
  • ID、版本和脏标记可以共享;订单取消、客户授权等业务规则必须留在具体类型。
  • 继承耦合、基类变化影响面和子类测试隔离是采用前必须观察的证据。
  • 不能证明共同语义时,应拒绝继承,改用组合、接口或局部实现。

可验证练习

练习

问题 1:判断提升边界。 Customer、Order 和 Product 都有 idversion,但只有 Order 有 cancel()。哪些内容可以放进超类型,为什么?

问题 2:找出继承耦合。 基类新增 sendEmail() 后,所有实体测试都要配置邮件客户端。请说明问题及一个替代方案。

问题 3:决定是否采用。 两个类只有一个相同的 toLabel() 函数,且未来变化方向不同。应采用层超类型吗?请给出理由。

名词解释

名词解释

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

层超类型

为同一层多个类型提供共同基础机制的超类或共享抽象。

共同机制

同一层内多个类型都需要、且含义和生命周期一致的基础行为。

继承耦合

子类被迫依赖超类状态、初始化或方法契约,基类变化会扩散到子类的风险。

脏标记

表示对象自读取或持久化后发生过修改、需要进一步处理的状态。

变化轴

会推动代码变化的独立原因,例如持久化机制与订单业务规则的变化。

前后导航

参考资料

资料与写作方式声明

本章以Martin Fowler《企业应用架构模式》与公开模式目录权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…