在DDD中分离领域模型和持久化模型

84

我一直在阅读关于领域驱动设计以及如何使用代码优先方法生成数据库的实现方式。根据我的阅读和研究,这个主题有两种观点:

  1. 有一个类同时担任领域模型和持久化模型的角色。

  2. 有两个不同的类,一个用于实现领域逻辑,另一个用于代码优先方法。

现在我知道观点1)被认为是简化没有领域模型和持久化模型之间太多差异的小型解决方案的做法,但我认为它违反了单一责任原则,并且引入了大量问题,当ORM的约定与DDD相冲突时。

让我惊讶的是,有很多实现观点1)的代码示例。但我没有找到一个实现观点2)的单一示例以及如何映射这两个对象。(可能有这样的示例,但我没有找到C#的示例)

所以我试着自己实现一个示例,但我不确定这是否是一个好方法。

假设我有一个票务系统,票有过期日期。我的领域模型如下:

/// <summary>
/// Domain Model
/// </summary>
public class TicketEntity
{
    public int Id { get; private set; }

    public decimal Cost { get; private set; }

    public DateTime ExpiryDate { get; private set; }

    public TicketEntity(int id, decimal cost, DateTime expiryDate)
    {
        this.Id = id;
        this.Cost = cost;
        this.ExpiryDate = expiryDate;
    }

    public bool IsTicketExpired()
    {
        if (DateTime.Now > this.ExpiryDate)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
}

使用Entity Framework作为ORM的持久化模型看起来几乎相同,但随着解决方案的增长,情况可能并非如此。

/// <summary>
/// ORM code first Persistence Model
/// </summary>
public class Ticket
{
    [Key]
    public int Id { get; set; }

    public decimal Cost { get; set; }

    public DateTime ExpiryDate { get; set; }
}

目前一切看起来都很好。现在我不确定的是从哪里获取Ticket持久化模型最好,以及如何将其映射到TicketEntity领域模型。

我已经在应用程序/服务层中完成了这个过程。

public class ApplicationService
{
    private ITicketsRepository ticketsRepository;

    public ApplicationService(ITicketsRepository ticketsRepository)
    {
        this.ticketsRepository = ticketsRepository;
    }

    public bool IsTicketExpired(int ticketId)
    {
        Ticket persistanceModel = this.ticketsRepository.GetById(ticketId);
        TicketEntity domainModel = new TicketEntity(
            persistanceModel.Id,
            persistanceModel.Cost,
            persistanceModel.ExpiryDate);

        return domainModel.IsTicketExpired();
    }
}

我的问题如下:

  1. 除了加速开发和重用代码之外,有没有其他原因支持观点1)比支持观点2)更好?

  2. 在映射模型时,我的方法是否存在任何问题?如果解决方案不断增长,是否会出现问题?


27
我希望我能够给这个问题点赞1000次。很高兴看到有人理解分离的必要性,以避免由于ORM约定而损害领域模型。 - jgauffin
4
非常好的问题。 - JosEduSol
2
为什么在实体中要放置“Id”?实体不是通过它们的引用来区分的吗?难道“Id”只是持久性的吗?如果我的持久性是基于二进制序列化的,那该怎么办?我根本不需要“Id”。 - Sergio
1
@Adrian Hristov,你最终选择了选项1还是选项2?你有没有找到任何好的选项2的例子?我同意在github上找到的所有例子似乎都使用选项1。 - w0051977
5
自从我发了这个问题以来已经过了几年,我想现在分享一下我的观点是合适的。我可以说,在这段时间里我参与的所有项目中,我总是选择了选项2。正如下面一些答案所提到的,选项1可能被视为违反单一责任原则。根据我的经验,任何现实世界的项目都有足够的领域和实体类之间的差异,因此即使一开始感觉有些过度,将它们分开对于项目还是有好处的。当然,在持久化数据等方面会有额外的开销,但我愿意接受这种开销。 - Adrian Hristov
一个使用领域实体作为ORM模型的示例,由Oskar Dudycz提供:https://github.com/oskardudycz/EventSourcing.NetCore/blob/850022e06f9a7cd7fb284a70e0179be18d16ccc3/Sample/CRUDToCQRS/02-CRUDWithCQRS/ECommerce/Core/Repositories/Repository.cs - undefined
3个回答

37

除了加快开发速度和重用代码之外,是否有任何理由优先选择观点1)而不是观点2)?

仅仅因为纯粹的懒惰和想象中的增加开发速度而选择选项1是不可取的。虽然这些应用程序会更快地建立版本1.0。但当这些开发人员达到应用程序的第3.0版时,由于ORM映射器中所做的所有妥协,他们并不认为维护应用程序很有趣。

我的模型映射方法是否存在问题?在解决方案增长时是否会出现问题?

是的。存储库应负责隐藏持久性机制。它的API只能使用领域实体而不是持久性实体。

存储库负责将转换为/从领域实体(以便能够将其持久化)。获取方法通常使用ADO.NET或像Entity Framework这样的ORM来加载数据库对象/实体。然后将其转换为正确的业务实体,最后返回它。

否则,您将迫使每个服务都具有持久性和使用域模型的知识,从而具有两个职责。

如果按照DDD定义使用应用程序服务,则可能需要查看命令/查询分离模式,该模式可以替代应用程序服务。代码变得更加清晰,并且您还可获得包装域模型的轻量级API。


2
嗨,我很高兴有人和我有相同的信念 :). 但我仍然有一些不清楚的事情:如果我有一个实体和一个值对象,但在数据库中我想把它们呈现为2个不同的表怎么办?例如 ClientsAddresses,您可能有2个地址并将它们呈现为值对象,但是将它们放在同一个数据库表中将导致非规范化。领域模型和持久化模型(现在有两个)应该如何映射以及在哪里?因为通常我会有一个 ClientsRepository 和一个 AddressesRepository - Adrian Hristov
1
如果存储库负责将持久性模型映射到域模型,那么当DM中的大多数属性具有私有setter时,您如何实际映射PM到DM? OP使用构造函数,但是如果有太多属性要设置,这种方法如何扩展? - james
1
设置太多属性表明实体具有过多的职责。我倾向于在存储库中使用反射来设置属性。在我看来,这并不违反封装性,因为只要存储库之外的任何内容不违反封装性,所有实体都以有效状态存储在数据库中。 - jgauffin
1
完全的懒惰和想象中的开发速度提升,我非常赞同。 - Kerem Baydoğan
2
@jgauffin,对于“因此,映射应该在存储库中进行”的观点表示赞同。请问您能否确认您的意思是:1)存储库从数据库获取数据并将其放入持久化对象中,2)在存储库方法内,将持久化对象映射到域对象,3)该方法返回域对象。谢谢。 - w0051977
显示剩余7条评论

22

我在一项大型项目中遇到了这个困境,这真的是一个非常艰难的决定......我想花几个小时谈论这个话题,但我会为您概括我的想法:

1)持久性和领域模型相同

如果您在一个从零开始设计数据库的新项目中,我可能会建议采用这个选项。是的,领域及其相关知识将不断变化,这将需要进行重构,并且会影响您的数据库,但我认为在大多数情况下这是值得的。

使用Entity Framework作为ORM,您可以使用流畅的映射几乎完全将领域模型与ORM问题分开。

好处:

  • 快速、简单、美观(如果数据库为该问题设计)

坏处:

  • 也许开发人员会在对领域进行更改/重构时三思而后行,因为他们担心它会影响数据库。这种恐惧对领域来说并不好。
  • 如果领域与数据库开始分歧,您将面临一些难以将领域与ORM保持和谐的困难。越接近领域,配置ORM就越困难。越接近ORM,领域就会变得更加杂乱。

2)持久性和领域模型为两个独立的事物

它将使您自由地对领域进行任何操作。不用担心重构,没有源于ORM和数据库的限制。我建议在处理遗留或设计不良的数据库时采用此方法,这可能最终会搞乱您的领域。

好处:

  • 完全自由重构领域
  • 可以轻松深入其他DDD主题,例如Bounded Context

不好的部分:

  • 在各层之间进行数据转换需要更多的努力。开发时间(可能还有运行时)会变慢。

  • 但是,最重要的并且相信我,可能会让你更加痛心的问题是:你将失去使用ORM的主要好处!比如跟踪更改。也许你最终会使用像GraphDiff这样的框架,甚至放弃使用ORM,转向纯ADO.NET。

我的模型映射方法存在问题吗?

我同意@jgauffin的看法:"映射应该在存储库中进行"。这样一来,持久化模型就永远不会离开存储库层,最好没有人能看到这些实体(除非是存储库本身)。


1
一种实现选项2并保持两者之间松耦合的方法是让领域以不可变DTO的形式发布事件,当领域事务完成时将其映射回持久性模型。这样,您的领域可以选择保存甚至事件本身,以便有变更历史记录,允许您重播在您的领域中发生的事件。在这种情况下,版本控制可以在事务级别上实现。 - The Prophet
2
第一种选择没有好处。即使“如果数据库是为该问题设计的”,因为有一天某个DBA可能会说“我要为了速度去规范化这两个表之间的关系!”而为了这个简单的更改,您必须重构您的领域模型。持久性模型更改不应强制要求领域模型更改,这是领域驱动设计的最大支柱之一。 - Kerem Baydoğan
2
是的,@KeremBaydoğan,当您选择选项1时,就会面临我在该选项的缺点中所描述的风险(“如果域开始与数据库分歧过大…”)。但这可能永远不会发生,即使它发生了,在它发生之前,您将拥有该选项的优势(主要是简单性)。个人而言,我不太喜欢那个选项,并且在大多数情况下可能不会鼓励它,但它是一个有效的选择,取决于您是否决定承担这种风险。 - fabriciorissetto

4
除了加快开发和重用代码之外,还有其他理由支持观点1)优于观点2)吗?个人认为有一个很大的理由:没有“持久化模型”。您所拥有的只是数据库中的关系模型和域对象模型。在两者之间进行映射是一组操作,而不是数据结构。此外,这正是ORM应该做的事情。现在,大多数ORM都支持从一开始就应提供的内容——以直接在代码中声明这些操作的方式,而无需触及您的域实体。例如,Entity Framework的流畅配置使您可以这样做。您可能会认为没有持久性模型=违反SRP并践踏DDD,因为您可以在许多实现中找到这样的情况。但事实并非如此。

准确地说,我完全停止使用属性,并转向流畅的 API。这种方式使域模型摆脱了任何 EF(Entity Framework)冗余代码。但一些存储(比如Mongo)仍然需要你这样做。但正如Eric Evans所说,这是一个足够好的设计选择,因为它在大多数情况下都起作用。 - Code Name Jack

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接