【问题标题】:DDD and Persistence. AgainDDD 和持久性。再次
【发布时间】:2015-06-29 05:40:06
【问题描述】:

我正在努力坚持领域驱动设计。据我了解,域模型永远不应该是持久感知的。假设我正在构建一个简单的待办事项列表应用程序。我有以下界面的任务:

interface ITask
{
   bool IsCompleted {get;}
   string Description {get;}

   void Complete();
   void ChangeDescription(string description);
}

通用实现应该是这样的:

class SimpleTask : ITask
{
    public SimpleTask(string description)
    {
       ChangeDescription(description);
    }

    public bool IsCompleted { get; private set; }
    public string Description { get; private set; }

    public void Complete()
    {
       IsCompleted = true;
    }

    public void ChangeDescription(string description)
    {
       // some validation here
       // ...
       Description = description;
    }
}

我希望有一个必要的描述 - 因为这是一个业务规则。因此,从这一刻起,如果我想通过序列化程序保存这个对象,我将失败,因为没有提供无参数构造函数。而且我不应该提供它,因为没有持久性感知规则。如果我以 DTO\POCO 的形式对我的任务进行建模,我最终会遇到另一个问题——所谓的贫血模型。此外,我不想为某些属性提供设置器。

那么解决所有这些问题的方法在哪里?我可以创建一个紧密耦合的保护程序,它将知道如何保存和恢复任务状态。但是我只能访问公共属性和方法,如果任务的内部逻辑很复杂并且无法保存\恢复怎么办?我应该在任务内部标记所有字段并有可能保存对象的内部状态吗?是不是有点代码味道,违反了没有持久性感知规则?

你是怎么解决这个问题的?

【问题讨论】:

标签: c# .net domain-driven-design persistence


【解决方案1】:

据我了解,Entity Framework 不如 Hibernate 灵活,因此您必须在模型中做出更多妥协。 Vaughn Vernon,Implementing Domain-Driven Design (IDDD)shows a great way 的作者,他在保持自封装实体的同时使用 Entity Framework 轻松保持其状态。

如果您可以使用您选择的持久性存储,您也可以使用不涉及太多阻抗失配的different strategy

【讨论】:

    【解决方案2】:

    如果您查看 DDD (http://dddsample.sourceforge.net/) 的作者提供的示例项目,您会发现 Entities 有一个私有的空参数构造函数作为 Hibernate 持久化对象要求的解决方法。 因此,我认为,由于语言或基础架构的技术限制,模型对象中存在一定程度的“肮脏”是允许的。
    无论如何,我建议您通过一些工厂方法(可能是聚合根)公开 ToDo 实体的创建,以便您可以强制执行对象创建的业务规则。这样,您就可以为客户提供一种约定,以便以正确的方式使用您的模型。
    您无法避免客户端创建无效的状态模型(考虑反射、字节码检测等),因此真正重要的是设计正确的方式来使用您的模型并指导客户端。

    【讨论】:

    • 那么如果工厂将成为模型创建的起点,那么这将是什么结束呢?我的意思是,储蓄会上升到哪里?
    • 存储库是负责保存聚合根的域服务。域模型的客户端将使用它来检索和保存 AR。存储库知道他们管理的 AR 和相关实体,而实体不知道存储库。
    【解决方案3】:

    我认为,已经存在的实体的状态不应该在补液时经历同样的不变强制,就好像它是你试图达到的全新状态一样。否则,当您的不变代码更改时,数据库中的实体可能不再有效,如果您不采取补偿措施(填写默认值等),您可能会丢失大量历史记录

    具体来说,这意味着:

    • 您的 ORM 可以完美地通过无参数构造函数直接访问实体的状态(它怎么会知道将什么传递给带有参数的构造函数?)和设置器。虽然它确实对您的实体有影响,但您可以将其最小化,例如将构造函数设为 protected 并将 setter 设为私有(possible in Entity Framework),我不会将其称为“持久性意识”本身。

    或者,

    • 您必须从主实体对象中分离持久状态。有关经典方法,请参阅 plalx 和 Adrian 的答案。

    Event Sourcing 以一种优雅的方式解决了这个问题。状态重构的概念在实体本身中根深蒂固,因为它知道如何处理发生在它身上的各种事件。仍然不考虑持久性意识 - 事件在为实体补水时重播,但这会触发与它们第一次针对实体播放相同的逻辑。

    【讨论】:

    • 从未使用过 ES,但我知道事件存储是对事件进行二进制序列化,还是以 JSON 或其他方式序列化?如果使用二进制序列化,如何处理事件类的变化?
    • 您可以使用升级程序替换事件流,而无需过多关注其内部表示:abdullin.com/post/event-sourcing-versioning
    【解决方案4】:

    就个人而言,我使用洋葱/六边形/端口和适配器样式架构,我的域由引用域模型层的应用程序层组成。我通常会在应用程序层中获得大部分特定于领域的持久性。我从应用程序层执行大部分持久层操作。我在应用层的类和方法以业务功能、流程和工作流命名,它们处理实体的获取和保存。

    要回答您问题的主要部分,我发现在 DDD 中使用 ORM(尤其是代码优先)非常困难。实体框架之类的领域实体与 DDD 中的实体非常不同,它们只是共享同一个词。有 DDD 和 EF 世界的支持者将主张制定程序以使这两个东西一起工作。就个人而言,延迟加载和导航属性的所谓 ORM 好处足以让我将我的 ORM 隐藏在我的存储库后面。 IE。存储库接受并返回域实体,存储库方法中发生的通常是一些与 ORM 生成的实体之间的映射。出于这个原因,我倾向于先使用 DB 生成的实体而不是代码,只是因为我没有从我的 ORM 中获得所谓的优点。

    【讨论】:

      【解决方案5】:

      不幸的是,您的域中总会出现一些与持久性相关的行为。你只需要决定多少:)

      我不喜欢 ORM,而且有些需要过多地处理领域类,例如标记方法为虚拟或,恐怖的恐怖,属性。

      您的域对象具有行为,并且具有形状。您想要检索形状部分(状态)并保持 那个。加载对象时,您希望将该状态返回给对象以“水合”。这是momento模式。

      事件溯源 (ES) 也确实如此。一个事件代表对象的一些状态变化。重新加载事件会在内部更改状态以保持一致。

      在 ES 中,Snapshot 用于在事件非常多时提高性能。 快照 表示特定时间点的状态,并且该时间点之后的所有事件也将被应用。 快照也遵循了瞬间模式。

      所以归结为您希望如何公开对象的状态。

      【讨论】:

        猜你喜欢
        • 2013-08-23
        • 1970-01-01
        • 2015-08-17
        • 2014-08-11
        • 2012-12-11
        • 2018-08-30
        • 1970-01-01
        • 2015-09-17
        • 2011-02-07
        相关资源
        最近更新 更多