Go中的DDD存储库设计模式


根据 DDD 原则实现存储库和聚合可以封装领域逻辑并增强应用程序的可维护性。确定聚合边界需要深入考虑域和表设计,但这可以说是软件开发中更有趣的方面之一。

让我们考虑一下电子商务网站的购物车界面。假设有购物车和购物车商品的表。应用层用例 X 利用购物车和购物车商品的数据。

非 DDD :
表和存储库之间具有一一对应的关系。

存储库是为 ShoppingCart 和 CartItem 表定义的。

package repository

type ShoppingCart interface {
  GetByID(id uuid.UUID) (*model.ShoppingCart, error)
  Insert(*model.ShoppingCart) error
  Update(*model.ShoppingCart) error
}

type CartItem interface {
  GetByShoppingCartID(id uuid.UUID) ([]*CartItem, error)
  Insert(*CartItem) error
  Update(*CartItem) error
}

模型定义:
代表每个表的一条记录的结构被定义为模型。没有接口,领域逻辑就无法封装

package model

type ShoppingCart struct {
  ID uuid.UUID
  Status model.ShoppingCartStatus
}

type CartItem struct {
  ID uuid.UUID
  ProductID string
  Quantity int
}

这种方法存在以下潜在问题:

  • 模型仅仅成为表记录的容器。
  • 领域逻辑在用例 X 中实现。
  • 在应用层确保聚合不变量。
  • 用例 X 需要考虑新的和更新的数据库操作。

符合 DDD 的存储库示例
现在,让我们按照 DDD 原则设计一个购物车存储库。
聚合的根是 ShoppingCart 实体。CartItem 实体封装在 ShoppingCart 聚合中。
首先,定义与 ShoppingCart 聚合对应的存储库。与前面的示例不同,这里没有 CartItem 存储库。这是因为存储库是按聚合定义的,而不是按表定义的。

存储库接口:
存储库封装了读取和更新聚合的过程,因此通常不需要分开Insert和Update。存储库在Save方法中对此负责。

package domain

type ShoppingCartRepository interface {
  GetByID(id uuid.UUID) (ShoppingCart, error)
  Save(ShoppingCart) error
}

聚合模型定义:

聚合的根是 ShoppingCart 实体。聚合封装了领域逻辑,因此它们被定义为接口,而不是结构,并在包中定义domain。这是聚合接口定义:
package domain

type ShoppingCart interface {
  ID() uuid.UUID
  AddItem(Product Product, Quantity int) error
}

改进点:

  • 存储库可以完全封装数据库复杂性(插入/更新决策)。
  • 无需为每个表创建存储库,减少冗余的SQL编码。
  • 通过连接模型,可以封装领域逻辑。
  • 应用层消除了领域逻辑和数据库复杂性。

采用 DDD 明确了领域逻辑、数据库访问和应用程序层的职责。这一改进降低了应用程序的复杂性和认知负荷,从而随着时间的推移提高开发速度。

聚合和存储库的实现应该分开吗?
模型和存储库接口的实现应该放在哪里?没有明确的答案,但我个人认为最好将存储库和模型的实现放在同一个包中。

同一包中定义存储库和聚合模型实现的原因:
存储库的作用是封装数据库访问、创建和保存聚合的复杂性。存储库需要初始化属性以实例化聚合。由于它引用和更新接口中未公开的属性,因此需要在同一个包中定义它。

深度模块:简单接口的重要性
深度模块是斯坦福大学教授在《软件设计哲学》一书中解释的一个概念。

  • 深层模块是指表面上接口简单、狭窄,但内部功能丰富且复杂的模块。
  • 相反,具有复杂接口但内部功能很少的模块称为浅模块。

深度模块具有低认知负荷、高可重用性和易于理解的优点。

例如

  • Go语言的net/http封装是一个简单的接口,但封装了实现HTTP服务器的许多功能和复杂性,使其易于使用。
  • 操作系统的文件系统是深度模块的另一个示例。

深度模块Deep Module的概念近年来得到了IT行业的广泛支持。

如何保持较小的总体边界
我们如何才能保持较小的总体边界?要做到这一点,我们需要从表格设计阶段就考虑。

当服务或产品将来扩展时,聚合的边界将成为异步处理的边界。因此,您必须确定边界,以便即使在拆分事务时也不会出现数据不一致的情况。有时,表格需要以意想不到的方式分隔。

随着聚合变大,同步事务中更新的表数量也会增加,从而导致性能下降和维护问题。即使看起来实体应该在一笔交易中更新,但经过仔细检查它们可能是可分离的。

重要的是不要被先入为主的观念所束缚。

保持聚合的边界尽可能小。这是为了减少数据库事务的大小,进而减少领域模型的技术债务。

没有什么灵丹妙药可以正确确定聚合的边界。正确设置聚合边界需要考虑数据库设计、可扩展性和领域知识。在复杂的领域(例如库存管理系统)更具挑战性。