C# DDD与持久性。阿盖恩

C# DDD与持久性。阿盖恩,c#,.net,domain-driven-design,persistence,C#,.net,Domain Driven Design,Persistence,我在域驱动设计中与持久性作斗争。据我所知,域模型永远不应该是持久感知的。假设我正在构建一个简单的待办事项列表应用程序。我有一个具有以下界面的任务: interface ITask { bool IsCompleted {get;} string Description {get;} void Complete(); void ChangeDescription(string description); } 通用实现应该如下所示: class SimpleTask :

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

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

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


如何解决这个问题?

如果您查看DDD()作者提供的示例项目,您可以看到实体有一个私有的空参数构造函数,作为对象持久化的Hibernate要求的解决方法。
因此,我认为,由于语言或基础设施的技术限制,模型对象中存在一定程度的“肮脏”是允许的。
无论如何,我建议您通过一些工厂方法(可能是聚合根)公开ToDo实体的创建,以便您可以强制执行业务规则来创建对象。这样,您就为客户提供了一种惯例,以便以正确的方式操作您的模型。

您无法避免客户机创建无效的状态模型(考虑反射、字节码插装等),因此真正重要的是设计正确的方法来使用您的模型并指导客户机

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


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

据我的理解,实体框架比Hibernate灵活得多,因此您必须在模型中做出更多的妥协。沃恩·弗农(Vaughn Vernon),实现域驱动设计(IDDD)的作者,该设计保持自封装实体,同时使用实体框架轻松保持其状态


如果您可以使用自己选择的持久性存储,那么您也可以使用一种不涉及太多阻抗失配的存储。

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

具体而言,这意味着:

  • 您的ORM完全可以通过无参数构造函数(它怎么知道用参数传递给构造函数什么?)和setter直接访问实体的状态。虽然它确实会对实体产生影响,但您可以将其最小化,例如,使构造函数受到保护,而setter是私有的,我不会称之为“持久性感知”
或者

  • 您必须从主实体对象分离持久化状态。经典方法见plalx和Adrian的答案

以优雅的方式解决这个问题。国家重建的概念在实体本身根深蒂固,因为它知道如何处理随之而来的各种事件。尽管如此,它仍然不被认为是持久性感知——在对实体重新水化时,事件会被重放,但这会触发与第一次对该实体播放事件时相同的逻辑。

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

我不喜欢ORM,有些需要太多的领域类,比如标记方法、虚拟方法、恐怖、属性

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

事件来源(ES)也确实沿着这些路线。事件表示对象中的某些状态更改。加载回事件会在内部将状态更改为一致

在一个序列号中