仓库
了解如何在领域驱动设计(DDD)中通过实现仓储模式,将业务逻辑与数据访问逻辑相分离。
有时,应用程序需要与各种类型的持久化系统进行交互。这类持久化系统可能是 SQL 或 NoSQL 数据库、文件,甚至是外部服务。为此,我们需要一种机制来与这些系统进行通信。关键在于,业务逻辑必须与数据访问逻辑保持分离。
什么是仓库(Repository)?
仓库并不一定涉及数据库。仓库 是一种设计模式,用于隔离业务逻辑与数据存储层的交互逻辑。它在内存中表现为一组对象的集合,隐藏了底层数据层中对聚合(aggregate)信息进行管理与查询所需的数据存储细节。当一个聚合需要与自身以外的数据交互时,它会调用仓库中的方法。聚合仅知道内存中的一组对象,而无需了解仓库背后的具体实现。
值得注意的是,领域层应当对持久化机制无感知。这意味着业务对象不包含任何关于数据如何从数据存储中读取或写入的实现细节。
仓库的特征
一个良好的仓库实现应具备以下特征:
-
每个聚合应仅与一个仓库交互,同样地,每个仓库也应只服务于一个聚合。如下图所示,每个聚合都与其对应的仓库配对使用。
-
仓库接口应仅暴露与业务逻辑相关的行为。方法及其参数的命名应基于其所属领域的业务语义。如下图所示,
getAccount(Account)方法以非技术性术语命名,并接收“账户”这一业务实体作为参数;而错误的做法则是定义一个技术性方法getAccountById(id),并仅接收一个 ID。 -
持久化操作应具备原子性。当用户需要保存某个领域对象的新状态时,必须确保整个聚合的状态更新是原子完成的。下图展示了多个聚合之间事务的原子性行为。
仓库的优势
使用仓库模式具有以下优点:
- 有助于将领域模型与存储层解耦,使我们能够在不影响业务逻辑的前提下更换底层存储技术。
- 提供更灵活的单元测试支持,因为可以使用模拟(mock)的存储系统替代真实数据源。
- 促进领域层与数据访问层之间的关注点分离。
仓库的缺点
在使用仓库模式时,也需注意以下不足之处:
- 当聚合规模较大时,可能会出现性能问题。存储或读取大型聚合可能需要执行多次数据库操作。
- 可能导致基于条件的查询难以表达。如前所述,仓库中的方法应使用业务语言命名,这在需要实现特定查询时可能带来复杂性。
仓库示例
假设一位客户希望通过电商平台购买商品。首先,客户会在网站上搜索商品。为了返回相关信息,名为 Sells 的聚合根将通过仓库获取所有相关商品。该仓库的具体实现为一个模拟数据库。
模型结构如下:
main 方法将调用 Sells 对象中声明的 ListProducts() 方法来列出商品。要实例化 Sells 对象,必须先创建一个实现了 ProductRepository 接口的对象。当执行 ListProducts() 时,系统将返回对应的商品列表。
运行以下代码可查看实际效果。
Go
1package main 2 3// Sells 是一个聚合根,包含销售相关的属性 4type Sells struct { 5 product Product 6 repository ProductRepository 7} 8 9// NewSells 是创建 Sells 对象的唯一方式 10func NewSells(repository ProductRepository) Sells { 11 return Sells{ 12 repository: repository, 13 } 14} 15 16// ListProducts 返回仓库中的所有商品 17func (c *Sells) ListProducts() []Product { 18 return c.repository.GetAllProducts() 19}
Java
1import java.util.List; 2 3// Sells 是一个聚合根 4public class Sells { 5 6 // Sells 的属性 7 private Product product; 8 private ProductRepository repository; 9 10 // 构造函数:创建 Sells 对象的唯一方式 11 public Sells(ProductRepository repository){ 12 this.repository = repository; 13 } 14 15 // ListProducts 返回仓库中的所有商品 16 public List<Product> ListProducts(){ 17 return this.repository.GetAllProducts(); 18 } 19}
总结
仓库是一种战术设计模式,旨在解决领域逻辑与数据访问逻辑之间的隔离问题。