领域驱动设计 - 领域模型 vs Hibernate实体

18

Hibernate实体和领域模型是同一个吗?

看下面的例子。

方法1 - 领域模型和实体是同一个类。领域模型“是一个”实体

@Entity
@Table(name = "agent")
class Agent
{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(name = "agent_number", unique = true, nullable = false)
    private String agentNumber;

    @Column(name = "agent_name", nullable = false)
    private String agentName;

    // Busines logic methods
}

方法2 - 域和实体是不同的函数。 域模型“具有”实体

class Agent
{
    // Hibernate entity for this domain model
    private AgentEntity agentEntity;

    // Getters and setters to set the agentEntity attributes

    // Business logic
}

从上述两种方法中,哪一种是实现DDD的正确方法?我认为第二种方式是正确的,因为您基本上控制了对敏感对象和封装对象(领域模型)的访问,而封装对象拥有领域模型的所有业务逻辑/操作。

但是我的同事们认为它们本质上是相同的。根据他们的说法,Hibernate Entity 的目的是在给定系统中代表领域模型。将实体建模为领域模型实际上使设计更简单。这是因为存储库接受实体来执行CRUD操作。

因此,如果模型“拥有”实体,则必须将存储库注入到领域模型中以保存实体。这会使设计变得不必要地复杂。

2个回答

23

既然在这个案例中提到了Hibernate技术,那么你所谈论的就是一种实现。领域驱动设计涉及抽象层面(例如模型)和它的实现。

模型可以用不同的方式实现。在你的例子中,你展示了两种表示相同模型的不同实现。

这篇文章讲解了你所面临的问题。

你问领域模型Hibernate实体是否相同。答案是不相同

Hibernate实体是一种特定于技术的东西,在这种情况下,它是ORM框架的一部分。与DDD定义的DDD实体不同,Hibernate实体是一个具体的Java对象,被实例化、跟踪、持久化、丢弃并垃圾回收。

人们只是用相同的术语来指称不同的事物,这可能会导致混淆(不能怪他们,给事物命名是软件中的两个难题之一)。

您可以使用 Hibernate实体 或其他类型的技术,比如 Entity Framework 实体(即同样是OO程序中的对象)来实现一个领域模型。相同的领域模型可以使用不同的语言和技术来实现。这些实现将根据技术提供的内容而有所不同。
例如,如果您正在编写使用MongoDB的NodeJs后端,并且想要使用ORM来实现领域模型,那么您将只能使用Active Record 模式(可能是Mongoose),因为这些是人们实现的唯一框架(至少我找不到其他不是Active Record的框架,如果您找到了请告诉我)。以这种方式实现DDD可能会非常棘手(而且可能真的很糟糕)。
DDD书籍中,Eric Evans谈到了技术如何帮助您实现模型或者如何与您抗争。当它与您抗争或者没有提供良好的机制时,您只需要绕过它。有时ORM有自己的要求,但你不希望将这些暴露给其他代码,因此你可以像方法2中那样使用包装器。其中一些包括公共get set方法、公共构造函数等。大多数使用反射,并且可能具有私有内容,但仍然存在许多问题,比如具有没有参数的私有构造函数以满足框架,而你的代码则混乱不堪,有很多与你的模型无关但由于框架需要而存在的东西(呕吐!)。这也可能导致错误。相比于默认构造函数,使用具有参数或静态工厂方法的好构造函数易犯错误。这个包装器可以代表一个更纯粹的领域模型,而不必拥有框架所携带的必要的恶魔,因此你可以使用它们。
在某个项目中,这变得非常丑陋,我们决定在Repositories中使用原始SQL,以便不必处理所有框架的问题。实现很好,很纯粹,我们完成速度更快。有些人认为框架可以加速开发进程,这在大多数情况下是正确的,但当框架与你作对,代码出错时,调试并不好玩,因此编写原始SQL可能更容易。在这种情况下,遵循DDD的指南,通过使用聚合,我们的模型被很好地解耦,而且没有使开发变慢的复杂查询。

19
Hibernate实体与域模型并不完全相同。实际上,它们之间的界限可能非常模糊。领域驱动设计的一个要点是你可以将持久化关注点与域模型分离开来。域模型在内存中保存了业务当前状态的表示以及监管该业务状态随时间变化的领域规则。仓储充当一种边界,在应用程序部分认为域实体都存储在本地内存的某个地方,而代码部分则知道数据的非易失性存储。换句话说,仓储从某种意义上来说是两个函数;一个知道如何从“聚合”中获取数据并存储,另一个知道如何从存储中读取数据并构建聚合。ORM是将数据从外部关系数据库传输到本地内存的一种方法。因此,您的加载过程可能看起来像是:
Use an identifier to load data from the database into a hibernate entity
copy the data from the hibernate entity into an aggregate
return the aggregate

商店可能长成这样

Copy data from the aggregate into a hibernate entity
Save the hibernate entity.

实际上,这有点麻烦。ORM 表示通常需要关注诸如代理键、跟踪哪些数据元素是脏的以便优化写入等问题。

因此,您经常会看到领域逻辑被编写到 ORM 实体中,并添加一堆注释以明确哪些部分存在,因为它们是 hibernate 所必需的。

如果您查看 DDD Cargo shipping 示例,就会看到他们采用了第二种方法,其中聚合在底部隐藏了少量 Hibernate 支持

Domain 和 Entity 是不同的功能。领域模型 "has-an" 实体

你的同事们是正确的:在最重要的方面,它们是等价的。领域模型依赖于您的 hibernate 实体。

它们都不符合 Evans 在他的书中描述的那样。

这两者都看起来像很多团队在实践中所做的事情。直接将领域逻辑放入 hibernate 实体中是我所知道的常见方法。

如果您真的要分离这两个功能,那么您的存储库将看起来像

Agent AgentRepository::find(id) {
    AgentEntity e = entityManager.find(id)
    Agent a = domainFactory.create( /* args extracted from e */ )
    return a
}

void AgentRepository::store(Agent a)
    AgentEntity e = entityManager.find(id)
    copy(a, e)
}

// I think this is equivalent
void AgentRepository::store(Agent a)
    AgentEntity e = entityManager.find(id)
    entityManager.detach(e)
    copy(a, e)
    entityManager.merge(e)
}

仔细观察,您会发现域模型与Hibernate模型无关,但是存储库依赖于两者。如果需要更改持久性策略,则域模型不变。

这种额外的分离值得麻烦吗?这取决于情况。描述域模型所使用的面向对象模式与无状态执行环境之间存在强烈的认知失调。


感谢您的回复。使用Java Hibernate时,存储库采用实体进行CRUD操作。因此,如果我需要实现方法2,则域模型应该注入存储库依赖项,以便保存实体,因为域模型在方法2中“具有”实体。在将组件依赖注入到域模型中时,我认为这会使设计非常丑陋。如果我错了,请纠正我。 - Vino
这是一个非常好的方法,将业务模型和实体分开。但我看到一个问题,当你有关系,例如“一对多”时,如何管理这部分。您总是需要加载关系以映射到业务对象。您总是有一个急切的配置。 - Brayme Guaman

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