DDD 和持久性。再次

DDD and Persistence. Again

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

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 的形式对我的任务建模,我最终会遇到另一个问题——所谓的贫血模型。此外,我不想为某些属性提供设置器。

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

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

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

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

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

据我了解,Entity Framework 不如 Hibernate 灵活,因此您必须在模型中做出更多妥协。 Vaughn Vernon,实施领域驱动设计 (IDDD) 的作者 shows a great way 保持自封装实体,同时使用 Entity Framework.

轻松保持其状态

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

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

具体来说,这意味着:

  • 您的 ORM 可以完美地通过无参数构造函数直接访问实体的状态(无论如何它怎么知道要将什么传递给带有参数的构造函数?)和设置器。虽然它确实对您的实体有影响,但您可以将其最小化,例如使构造函数受保护并将 setter 设为私有,即 possible in Entity Framework,我不会称其为 "persistence awareness" 本身。

或者,

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

Event Sourcing 以优雅的方式解决了这个问题。国家重建的概念在实体本身中根深蒂固,因为它知道如何处理发生在它身上的各种事件。它仍然不被认为是持久性意识——当实体再水化时,事件被 重播 ,但这触发了与它们第一次 播放 完全相同的逻辑] 针对实体。

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

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

您的域对象具有 行为 并且具有 形状 。您想要检索形状部分(状态)并坚持 that。加载对象时,您希望将该状态返回给对象 "hydrate"。这是momento模式。

事件溯源 (ES) 也确实遵循这些思路。事件表示对象中的某些状态变化。重新加载事件会在内部更改状态以保持一致。

在 ES 中一个 Snapshot 用于在事件非常多的情况下提高性能。 快照 表示特定时间点的状态,并且还会应用该时间点之后的所有事件。 Snapshot 也遵循 momento 模式。

所以它归结为你想如何公开你的对象的状态。