第10章 数据源架构模式

选择领域对象与数据源交互的入口和隔离程度。

学习目标

  • 能解释数据源入口如何影响对象身份、查询职责、写回边界和测试隔离
  • 能用 TypeScript 实现带身份缓存和显式提交的订单数据源端口
  • 能根据领域行为复杂度、映射成本和查询形态,在 Gateway、Active Record 与 Data Mapper 之间作出选择

为什么第10章数据源架构模式值得单独学习

第10章数据源架构模式的核心不是选择一个 ORM,而是决定领域对象如何遇到数据库。订单聚合从一张表开始,后来可能增加订单行、折扣、付款状态和并发写回;如果每个调用者都自行拼 SQL、创建对象并决定保存时机,同一个数据库行就会在内存中出现多个互相矛盾的身份。

不是越抽象越好;它的价值取决于是否隔离了真实变化。本文以 2024 年中文版公开目录限定第10章的范围,并依据 Martin Fowler 的作者图书页和模式目录独立重写;目录只决定要讲什么,案例、实验、判断题和答案均为本课程原创。

先建立直觉:问题、机制与代价

数据源模式需要同时回答四个问题:谁负责查询,谁拥有对象身份,谁决定写回,谁承担映射成本。简单 CRUD 可以由 Table Data Gateway 集中处理 SQL;一行数据有少量行为时可以考虑 Row Data Gateway 或 Active Record;当领域规则需要和关系结构独立演化时,Data Mapper 才值得承担额外间接层。

应与查询入口和事务边界一起评审。

可以减少半成品写入,但也会增加生命周期、冲突和失败恢复责任;

提供隔离,却需要维护映射规则和查询性能。

对第10章数据源架构模式,优先比较的替代路线是:按查询形态选择 Table Data Gateway,按单行行为选择 Row Data Gateway 或 Active Record,按复杂聚合与测试隔离选择 Data Mapper;不要从 ORM 的功能清单反推领域结构。若实验只能显示一条记录读写成功,不能解释对象身份、事务提交和失败后的状态,就不能据此选择数据源模式。

目录单元到教学证据

第10章 数据源架构模式

第10章数据源架构模式要求把“领域对象如何与数据源交互”变成可观察的架构决定:先标出查询入口、身份缓存、写回者和映射边界,再用订单聚合持久化的重复加载、修改冲突与回滚测试验证决定。只有持久化替换不会迫使领域规则重写,数据源边界才真正隔离了变化。

专属代码案例:订单聚合持久化

把订单聚合持久化切成“领域调用 → 身份查找 → 数据映射 → 变更追踪 → 工作单元提交”五个观察点。第10章数据源架构模式的设计草案必须写出谁拥有状态、谁作出业务决定、失败怎样传播,以及 SQL 隔离、对象身份、行为位置、映射成本和测试替身中哪个指标最先提示当前方案不再适用。

设计记录采用五个可换行字段:单元键为 poeaa24-chapter-10-data-source-patterns;模式族为 mapping;裁决是“简单查询保持 Gateway,复杂领域行为使用映射器并集中提交”;观测项包括 SQL 隔离、对象身份、行为位置、映射成本、测试替身;拒绝条件是“同一订单在同一工作单元内被加载成多个可写实例”。

先预测:如果一个请求两次加载订单 42,并分别修改两个实例,提交时可能出现什么问题?先写下身份缓存、脏对象集合和失败回滚的预期,再阅读实现;验证时要能说明映射器解决了什么隔离问题,以及它没有自动解决什么并发冲突。

type OrderRow = {
  id: string;
  status: "open" | "paid";
  totalCents: number;
};
 
type Order = {
  id: string;
  status: "open" | "paid";
  totalCents: number;
};
 
interface OrderDatabase {
  selectOrder(id: string): OrderRow | undefined;
  updateOrder(row: OrderRow): void;
}
 
class OrderMapper {
  toDomain(row: OrderRow): Order {
    return { ...row };
  }
 
  toRow(order: Order): OrderRow {
    return { ...order };
  }
}
 
class OrderUnitOfWork {
  private readonly identityMap = new Map<string, Order>();
  private readonly dirty = new Set<string>();
 
  constructor(
    private readonly database: OrderDatabase,
    private readonly mapper: OrderMapper,
  ) {}
 
  load(id: string): Order | undefined {
    const cached = this.identityMap.get(id);
    if (cached) return cached;
    const row = this.database.selectOrder(id);
    if (!row) return undefined;
    const order = this.mapper.toDomain(row);
    this.identityMap.set(id, order);
    return order;
  }
 
  markDirty(order: Order): void {
    this.dirty.add(order.id);
  }
 
  commit(): void {
    for (const id of this.dirty) {
      const order = this.identityMap.get(id);
      if (order) this.database.updateOrder(this.mapper.toRow(order));
    }
    this.dirty.clear();
  }
}

这段代码把同一工作单元里的对象身份、映射和提交职责集中起来。真实数据库适配器还要处理事务、版本冲突、回滚和批量写入;Identity Map 只保证一次工作单元内的对象一致性,不能替代数据库隔离或跨请求并发控制。

数据源模式族地图

数据源模式不是从简单到复杂的必经升级路径。Table Data Gateway 让一类查询集中在表入口,Row Data Gateway 让一行数据成为对象,Active Record 在数据对象上增加行为,Data Mapper 则让领域对象完全不知道数据库。下面的梯度图把隔离能力、领域行为和映射成本放在同一张证据图中。

数据源模式:从简单到复杂的梯度复杂度 / 灵活度递增 →Table Data Gateway一个类管一张表的所有 SQLfind / insert / update / delete 全在 Gateway1Row Data Gateway一个对象 = 一行数据对象持有字段 + save() 方法2Active Record对象 = 行 + 业务逻辑领域行为直接写在数据对象上3Data Mapper对象与表完全解耦Mapper 独立翻译,领域对象不知 DB 存在4← 简单 CRUD、脚本式应用复杂领域、需要测试隔离 →选择依据:领域逻辑是否需要与持久化解耦?需要 → Data Mapper;不需要 → 越左越简单
数据源模式族包含四个模式,按复杂度递增排列。简单应用从 Table Data Gateway 起步, 领域逻辑复杂时迁移到 Data Mapper,让领域对象完全不知道数据库的存在。

选择时记录真实查询形态、对象行为和测试边界;如果只需要报表投影,就不要为了抽象领域对象而引入完整映射器。

数据源模式决策图

先问数据源访问是否只是结构化查询,再问对象是否拥有独立行为,最后问领域规则是否必须与表结构解耦。越靠右的方案隔离能力越强,但也需要承担映射、身份和事务管理成本。

数据源模式:先看行为,再承担映射成本结构化查询Table Data Gateway集中 SQL + 投影不拥有领域行为单行局部行为Row Gateway / Active Record行对象承担有限行为评估表结构耦合复杂聚合与隔离Data Mapper身份 + 工作单元领域对象独立测试拒绝条件:没有隔离需求却引入完整映射层模式复杂度应由行为、身份和测试隔离证据推动
从 Gateway 到 Active Record 再到 Data Mapper,隔离能力增强的同时也增加身份、事务和映射成本。

选择与拒绝矩阵

评审问题选择数据源模式的证据应拒绝当前方案的信号
责任查询、身份、映射和提交各有所有者每个调用者都能自行拼 SQL 并写回对象
身份同一工作单元内同一标识只有一个可写实例重复加载后不同实例互相覆盖
隔离领域对象可以在替换数据库后继续测试业务规则依赖表字段、连接和 ORM 生命周期
成本映射器和工作单元解决了可测量的变化只有未来可能复杂,却先引入完整间接层

常见误区

可验证练习

练习

本组练习覆盖第10章数据源架构模式,并要求把数据源入口、身份映射、工作单元、数据映射器和测试替身映射到订单聚合持久化。

问题 1:选择入口。 一个后台报表只需要按状态查询订单投影,不修改领域对象,应该从哪种数据源模式开始?

问题 2:处理重复身份。 同一工作单元两次加载订单 42 得到两个对象实例,分别修改后提交,应该补什么设计?

问题 3:评估映射器。 订单规则跨订单、订单行和付款状态,团队希望替换数据库并隔离领域测试,是否值得采用 Data Mapper?

名词解释

名词解释

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

数据源入口

为领域代码提供查询、加载、更新和删除入口,并把数据库细节限制在明确边界内的组件。

身份映射

在一次加载范围内保证同一个持久化标识对应同一个内存对象实例的缓存。

工作单元

把领域对象的新增、修改和删除集中收集,在一次明确提交中协调写回的边界。

数据映射器

独立翻译领域对象与关系记录,使领域对象不需要知道表结构、SQL 或数据库连接。

本章小结

掌握第10章数据源架构模式的标志不是记住 Gateway 或 Mapper 的名称,而是能在订单聚合持久化中解释“领域调用 → 身份查找 → 数据映射 → 变更追踪 → 工作单元提交”的责任链。学习者应利用数据源入口、身份映射、工作单元、数据映射器和测试替身作出可证伪的选择,并在对象身份分裂或提交失败没有恢复语义时明确拒绝当前实现。

前后导航

来源与改写范围

资料与写作方式声明

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

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

讨论

评论区加载中…