【问题标题】:In DDD how to pass Value Objects via DTO?在 DDD 中如何通过 DTO 传递值对象?
【发布时间】:2015-04-23 13:07:48
【问题描述】:

在我的域中,每个域实体可能有许多值对象。我创建了值对象来表示金钱、重量、计数、长度、体积、百分比等。

每个值对象都包含一个数值和一个度量单位。例如。 money 包含货币价值和货币 ($, euro,...) , weight 包含数值和重量单位 (kilo, pound, ...)

在用户界面中,这些也并排显示:字段名称、其值及其伴随的单位,通常在属性面板中。域实体具有向 UI 公开的等效 DTO。

我一直在寻找将 DTO 中的值对象传输到 UI 的最佳方式。

  1. 我是否只是将特定值对象公开为 DTO 的一部分?
  2. 我是否公开了在 DTO 中提供名称/值/单元的通用“值对象”等效项?
  3. 我是否在 DTO 中将其拆分为单独的名称/值/单元成员,只是为了在 UI 中重新组合它们?
  4. 我是在 DTO 中将它们作为 KeyValuePair 还是 Tuple 传输?
  5. 还有别的吗?

我进行了深入搜索,但似乎没有其他问题可以完全解决这个问题。非常感谢任何建议!

编辑: 在 UI 中,值和单位都可以更改并发送回域进行更新。

【问题讨论】:

  • 1) 似乎是最简单的方法,为什么要让它变得更复杂?
  • @dbugger,很多关于值对象的讨论。大多数似乎表明不将域对象公开为反模式。我愿意公开它们,但想了解利弊。
  • 是的,在核心域实体的情况下可能存在耦合问题。然而,在这种情况下,添加一组重复对象和额外的转换代码似乎增加了撤消的复杂性。
  • 当用户编辑“货币”时,“MoneyDTO”的金额字段会发生什么变化?这是否会对“订单”中的其他字段产生连锁反应(例如总计、增值税等)。现在需要更改吗?是这样吗:(1)谁负责保持这些一致,(2)您是否将这个修改后的 MoneyDTO 实例封送回不正确的状态(更改货币,未更改金额)?
  • @Alex,这个域不关心增值税等。这可以被认为是主数据。例如(一个简单的例子)供应商以美元报价他们的购买价格,即使他们可能位于加拿大。出于某种原因,未来的新价格以加元报价。该域已经具有随时间变化的汇率,如果需要,只需将所有货币价值转换为基础货币(可能是美元),然后再以单一货币计算整体最优计划。自然,货币的变化不会经常发生,但价值会发生。

标签: c# domain-driven-design dto value-objects


【解决方案1】:

我倾向于同意上面 debuggr 的评论如果这些是单向传输;值对象真的不是域对象 - 它们没有可以改变其状态的行为,因此在许多方面它们只是专门的“位桶”,您可以在不丢失上下文的情况下对它们进行序列化。

但是;如果您遵循 DDD 实践(或者如果您的后端使用多线程等),那么您的值对象是不可变的,即它们可能看起来像这样:

public class Money
{
    readonly decimal _amount;
    readonly string _currency;
    public decimal Amount {get{return _amount;}}
    public decimal Currency {get{return _currency;}}

    public Money(decimal amount, string currency)
    {
        //validity checks here and then
        _amount=amount;
        _currency=currency;
    }
}

现在,如果您需要从客户端发回这些内容,则无法轻松地直接在 DTO 对象中重新使用它们,除非您拥有的任何 DTO 映射系统(自定义 WebAPI 模型绑定器、Automapper 等)可以轻松地让您绑定使用构造函数将 DTO 转换为值对象...这对您来说可能是也可能不是问题,它可能会变得混乱 :)

  • 不过,对于这样的事情,我倾向于远离“通用”DTO 对象,请记住,在 UI 上,您仍然希望客户端代码可以使用“域” (无论是网页上的 Javascript 还是表单/控制台上的 C# 或其他)。此外,找到具有 Name/Value/Unit/Plus One Weird Property specific to that Value concept

  • 的异常值对象往往只是时间问题
  • 处理此问题的唯一“万无一失”*** 方法是每个值对象一个 DTO;虽然这是额外的工作,但你不会真的出错 - 如果你有很多这样的值对象,你总是可以编写一个简单的 DTO 生成工具或使用 T4 模板为你生成它们,基于公共属性你的价值对象。

***不是保证

【讨论】:

  • “他们没有行为”这是完全错误的。当行为是行为的自然归宿时,您应该努力将行为置于 VO 上。
  • @plalx 是的,你是对的,我应该澄清他们没有变异行为,这就是我的意思。答案已更新。
  • @StephenByrne,是的,客户可以更新这些并将它们发回。因此,如果我理解正确,如果在域中我有一个带有Money 值对象的Order 根实体,那么以你的“防傻”***方式,我也会有一个包含MoneyDTOOrderDTO
  • @StefandeKok - 是的,差不多 - 然后你有某种映射工厂或助手将 MoneyDTO 转换为 Money,Money 转换为 MoneyDTO 等。Automapper 之类的对于这个,但你也可以自己滚动 - 主要是保持映射集中,所以你只有一个地方可以配置它。
【解决方案2】:

DDD 完全是关于行为和明确表达意图,旁边是明确识别您要解决的问题的有界上下文(事务和组织边界)。这比您要求回答的“结构性”问题的类型重要得多。

即从可能具有“值对象”的“域实体”开始,其中“域实体”被映射为“DTO”以在 UI 中显示/编辑是关于您如何构建事物的声明,没有说明什么用户试图在此 UI 中实现的目标,以及组织需要为此做什么(即真正的业务规则,例如奖励折扣、更改送货地址、推荐用户可能感兴趣的其他产品、更改计费货币等)。

从您的描述中可以看出,您有一个域模型,该模型反映了需要在 UI 上查看/编辑的内容。这有点像“把马放在马车后面”。现在您有很多“层”,它们没有提供任何附加值,并且增加了很多复杂性。

让我试着解释一下我的意思,使用在“订单”和“金钱”中提到的(简化的)示例。使用上面提到的方法,尝试在屏幕上显示它可能涉及以下步骤:

  1. 读取给定 OrderId 的“订单实体”及其相关的“货币”值(可能在具有给定数量和单价的特定产品类型的订单行中)。这将需要一个包含多个连接的 SQL 语句(如果使用 SQL DB)。
  2. 以某种方式将其中的每一个映射到镜像“域对象”结构。
  3. 再次将这些映射到镜像“DTO”对象层次结构。
  4. 将这些“DTO”对象映射到 UI 中的“View”或“ViewModel”对象。

在这个例子中,有很多工作没有产生任何好处,因为模型应该捕获和执行业务逻辑。

现在,作为下一步,用户正在编辑 UI 中的字段。而且您必须以某种方式使用反向路由将其编组回您的域实体,并尝试从已更改的字段中推断出用户的意图,然后对其应用业务规则。

例如,假设用户更改了订单项的“MoneyDTO”上的货币。用户的意图是什么?将此作为新的计费货币并为所有其他订单项更改它吗?这与业务规则有什么关系?您是否需要查看汇率并更改所有订单项的“货币”?波动性更大的货币是否有不同的业务逻辑?您是否需要切换到有关增值税的新规则?

这些问题类型似乎与您的领域更相关,并且可能会导致领域实体和服务的结构与在 UI 上查看/修改的模型不同。

为什么不简单地将视图模型存储在您的数据库中(例如,作为 Json 以便可以通过单个查询检索并直接呈现),这样您就不需要额外的翻译层来向用户显示它。此外,为什么不构建您的 UI 以揭示意图,并将其映射到要发送到您的域服务的命令。例如。 “更改送货地址”命令可能与您组织的“送货”有界上下文相关,“更改计费货币”与“计费”有界上下文相关。

此外,如果您使用从您的域生成的域事件来补充这一点,表示“已经发生”的事情,您可以获得额外的好处。例如,“添加订单行”事件可以由“用户可能感兴趣的其他产品”服务获取,作为响应更新用户界面中的“建议产品”视图模型。

我建议您查看 CQRS 中的概念,作为处理此类问题的一种可能方法。作为一个非常基本的介绍以及一些更详细的参考资料,您可以查看 Martin Fowler 对此的看法:http://martinfowler.com/bliki/CQRS.html

【讨论】:

  • 嗨@Alex,感谢您的详细回答。我故意尽可能简单和通用地陈述我的问题。实际上,我的域非常复杂,但是值对象在 UI 和域之间确实具有非常相似的相似性(我应该相信)。我见过的大多数 DTO 都包含许多原语。对我来说,在 DDD 中似乎是错误的方式,但在这方面找不到任何具体的指导方针。
  • @StefandeKok Stephan,我同意使用反映域语言的类型和不可变值类型(例如包含货币的 Money 类型)通常是一种很好的做法。然而,将 UI 镜像为域模型(或相反),通常不是。然后你基本上是在做 CRUD + Forms,中间有很多不必要的(非增值)层。
  • 我认为我们完全同意。我 +1-ed 你的答案,因为它确实包含很多有用的提示,但可能会将 Stephen Byrne 的答案标记为官方答案,因为它解决了我正在努力解决的具体问题。等待几天给其他人提供其他答案的选项。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-11-01
  • 1970-01-01
  • 2018-04-03
相关资源
最近更新 更多