【问题标题】:Designing a class diagram for a domain model为领域模型设计类图
【发布时间】:2016-01-28 17:03:45
【问题描述】:

首先,不要认为我试图让其他人完成工作,但我正在尝试为域模型设计类图,而我所做的事情可能是错误的,因为我被卡住了,所以我只是想获得关于我没有正确执行的提示以继续...

例如,用户需要从产品列表中按类别搜索产品。每个类别可能有子类别,子类别可能有子类别等。

我制作的第一张图是这样的(简化的):

用户还需要获得至少有一种产品的类别树形列表。 例如,如果这是所有类别树:

  • 乐器
    • 字符串
      • 吉他
      • 小提琴
    • 打击乐
  • 书籍
    • 漫画
    • 小说
    • 浪漫

我无法返回类别树至少有一个产品,因为我还会获得所有子类别,但不是每个子类别都有与之关联的产品。

我也无法从 Category.subCategories 集合中删除项目以仅保留具有关联产品的项目,因为它会更改 Category 实体,该实体可能会在其他地方共享,这不是我想要的。

我想过做一个副本,但我会在同一上下文中获得同一实体的 2 个不同实例,这不是一件坏事吗?

所以我重新设计了这个:

现在每个类别都没有我不想要的子类别的集合,我只知道它的父类别,这没关系。

但是,这会创建一个只能从底部到顶部导航的类别树,对于始终需要顶部 -> 底部导航类别的 ProductList 客户端来说,这是没有意义的。

作为一个解决方案,我想到了下图,但我不确定它是否很好,因为它有点重复,而且 CategoryTreeItem 在域语言中似乎没有多大意义。

我做错了什么?

【问题讨论】:

  • 其实category中的自引用创建了一棵树。
  • @ThomasKilian 我在第二张图之后更新了关于没有树的声明。实际上有一棵树,但 ProductList 的客户端总是需要一个 top->bottom 可导航树,但在这种情况下,树只能从底部到顶部导航
  • 第一个变体可以双向导航。一个通过集合/组合,反之亦然通过父级。

标签: uml domain-driven-design class-diagram


【解决方案1】:

这是一个算法问题,而不是模型问题。您的第一种方法完全可以,除非您对约束保持沉默。因此,您可以为任何产品分配类别或子类别。如果您分配子类别,这意味着根据此模型,产品也将具有父类别。为了清楚起见,我将附加一个约束,告诉产品需要分配到最精细的已知类别粒度。例如。吉他产品将被分配到Guitar 类别。像 Stick 这样更奇怪的乐器会获得Strings 类别(这并不意味着它是吉他小提琴,而只是在更高的类别中。

现在,当您将实现 Category 时,您可能会想到一种方法来返回 assignedInstruments() 的集合,对于 Guitar 将返回 FenderAlhambra 等。您可以将此 assignedInstruments(levelUp:BOOL) 扩充为获得上述类别的乐器。

通常,您必须清楚类别分配的基本含义。如果您更改分配,产品最终会出现在另一个列表中。

【讨论】:

  • +1 表示产品的约束需要附加到最知名的类别,这是真的。但不确定第二部分,您在谈论如何从一个类别中检索其他类别,但我需要从 ProductList 中的所有产品中检索类别,在自上而下的树中,而不是从特定类别开始.
  • 产品被分配了一个类别。并且类别(根据第一个设计)具有子类别的组成。所以你在各个方向都是开放的。没有看到树前的森林?
  • 也许不是...如果我返回类别的集合并且每个类别都有其所有子类别的集合,我不能只返回至少关联一个产品的类别...例如有一个是 String 类别的产品,另一个是 Guitar 类别的产品,如果我 GetCategories 返回 String 类别,它还会返回所有 String 的子类别,不仅是 Guitar...
  • 在第一层,您的 CAT 收藏位于 one 层。更深的层次不直接返回。不过,您可以通过递归从顶层检索它们。
  • 好吧,问题是我需要向客户返回一个可从上到下导航的类别树,并且只包含至少有一个产品与之关联的类别+子类别,而不是所有子类别.如果我返回一个顶级类别的集合,客户端将递归导航并查看所有子类别,而不仅仅是包含产品的子类别。
【解决方案2】:

这取决于图表的用途。您是否应用了某种软件开发方法来定义此图在特定上下文中的用途和目标读者?

因为您谈论的是“领域模型”,所以我想您的目标是提供一种概念模型,即将应用程序的功能传达给最终用户、测试人员等所需的概念模型。在这种情况下,第一个和第二个图都是有效的,但没有操作(FilterByCategory 和 GetCategories),因为这些与该受众无关。 GUI 只显示完整类别树的一个子集这一事实通常不会用 UML 图表来表示,而是用纯文本表示。

另一方面,如果您的目的是为开发人员提供技术设计,那么第三张图是有效的。开发人员可能需要一个类来在数据库中保存类别('Category')和一个单独的类来为 GUI 提供类别('CategoryTreeItem')。你是对的,这种区别在领域语言中没有意义,但在技术设计中,通常会有这样的附加类。请与开发人员核实您的模型是否与他们使用的编程语言和库/框架兼容。

最后一句话: 在第一个图中,您在父端指定了 multiplicity=1。这意味着每个 Category 都有一个父级,这显然不是真的。第二张图具有正确的多重性:0..1。第三个图在 CategoryTreeItem 的组成上的 multiplicity=1 不正确。

【讨论】:

  • 关于第二张图的分类树,我的说法并不准确,确实是排列成一棵树,但是树只能从下往上导航,这对于客户端,它需要一个可以从顶级类别导航到子类别的树。
  • +1 指出了多重性问题。在第二段中,您的意思是您只会向客户端返回一个平面类别并让它显示在树中,但这只是移动问题,因为客户端总是需要一个自顶向下的树,看起来它应该以这种方式返回。将 down->top 树反转为 top->down 似乎也相对复杂
  • 我添加了一段关于双向导航关联的段落。
  • 但这棵树包含所有子类别,而不仅仅是那些与产品相关的子类别
  • 我看到你或多或少地和我一样争论。在我看来是树木/森林问题;-)
【解决方案3】:

在我看来,您的设计过于复杂。

围绕查询需求构建域模型通常是错误的方法。领域模型对于表达领域行为最有用。换句话说,在正确的边界内处理命令并保护不变量。

如果您的 Product Aggregate Root (AR) 通过 id 引用 Category AR,并且此关系存储在关系数据库中,那么您可以通过简单的数据库查询轻松完成任何提到的查询用例。您首先要收集树的平面表示,然后可以使用它来构建内存中的树。

这些查询可以通过ProductQueryService 公开,该ProductQueryService 是应用程序层的一部分,而不是域,因为它们不用于强制执行域规则或不变量:我假设它们用于满足报告或 UI 显示需求。在那里你可以有一个概念,例如ProductCategoryTreeItemDTO 用于内存中的表示。

根据图表中的 DDD 战术模式,您还使用了错误的术语,这非常具有误导性。 AR 是 Entity,但 Entity 不一定是 AR。实体术语主要用于指代仅在其 AR 边界内唯一标识的概念,而不是全局标识。

【讨论】:

  • 所以如果我只查询一个简单的对象列表并且除了显示之外不做很多事情,你说我不应该使用域模型吗?是的,这对于简单的查询是有意义的,但在我的情况下,有一些(中等复杂的)业务规则来决定哪些产品将包含在列表中,在数据库查询中移动这些规则对我来说似乎不是一个好主意。你觉得呢?
  • 对于 DDD 条款,我看不出我哪里错了?这 2 个实体也是它们自己的 AR,ProductList 是一个值对象,因为具有相同产品的 2 个列表被认为是相同的。产品稍后将关联其他类以最终创建一个聚合(例如价格、零件等)。在我展示的设计中可能令人困惑的是,我没有包含负责生成产品列表的实体。有一个 PointOfSale 实体,它将根据某些上下文构建不同的产品列表。
  • @Jonathan 查询谓词通常并不真正被视为域规则,但当您认为它们是时,您可以尝试将它们保留在域中。例如,没有什么可以阻止您拥有一个专门的域服务来封装逻辑以确定必须包含哪些产品。然后查询服务可以使用该域服务来约束查询,但整个查询不必在域中。例如。域服务可能会返回必须包含的List<ProductId>
  • @Jonathan 我的意思是,您应该在图表中将 AR 标记为聚合根而不是实体,因为不清楚这些实体是否也是它们自己 AR 的根。例如,通过查看图表,我可以认为 Category 实际上是 Product AR 下的一个实体。
  • 您将查询服务与域分开的观点很有趣。但我一直认为对持久对象的所有访问都应该通过域对象(通过存储库加载)完成,客户端代码处理的是域对象,而不仅仅是数据。
猜你喜欢
  • 2013-03-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-06-13
相关资源
最近更新 更多