如何避免贫血领域模型并保持关注点分离?

时间:2008-10-22 23:01:03

标签: language-agnostic design-patterns oop

似乎决定让你的对象充分认识到他们在系统中的角色,并且仍然避免在数据库和服务层的域模型中有太多的依赖?

例如:假设我有一个具有修订历史的实体,以及数据引用的几个“查找表”,您的实体对象应该有方法从一些查找表中获取详细信息,无论是通过提供访问查找表行,或通过将方法委托给它们,但为了做到这一点,它依赖于数据库层来从这些行读取数据。此外,当实体被保存时,它不仅需要知道如何保存自己,还需要知道将条目保存到修订历史中。是否有必要将对数十个不同数据层对象和服务对象的引用传递给模型对象?这似乎使得理解逻辑远比仅仅将薄模型传递给服务层对象要复杂得多,但我听到很多“智者”推荐这种结构。

4 个答案:

答案 0 :(得分:19)

真的很好问题。我花了很多时间思考这些话题。

通过注意表达域模型与关注点分离之间的紧张关系,您展示了很好的洞察力。这就像我提出的关于Tell Don't Ask and Single Responsibility Principle的问题中的紧张一样。

以下是我对该主题的看法。

域模型很贫乏,因为它不包含域逻辑。其他对象使用贫血域对象获取和设置数据。你所描述的对我来说听起来不像是域逻辑。它可能是,但一般来说,查找表和其他技术语言很可能是对我们意味着什么的术语,但对客户来说不一定是什么。如果这不正确,请澄清。

无论如何,域对象的构造和持久性不应该包含在域对象本身中,因为这不是域逻辑。

所以回答这个问题,不,你不应该注入一大堆非域对象/概念,比如查找表和其他基础设施细节。这是一个问题泄漏到另一个问题。域驱动设计中的工厂和存储库模式最适合将这些问题与域模型本身区分开来。

但是请注意,如果你没有任何域逻辑,那么你最终会得到贫血的域对象,即无脑的getter和setter,这就是some shops claim to do SOA / service layers的方式。

那么你如何才能充分利用两个世界?如何将域对象仅关注域逻辑,同时保持UI,构造,持久性等不受影响?我建议您使用Double Dispatch或某种形式的restricted method access等技术。

这是Double Dispatch的一个例子。假设你有这行代码:

entity.saveIn(repository);

在你的问题中,saveIn()将拥有关于数据层的各种知识。使用Double Dispatch,saveIn()执行此操作:

repository.saveEntity(this.foo, this.bar, this.baz);

存储库的saveEntity()方法具有如何在数据层中保存的所有知识。

除了此设置,您还可以:

repository.save(entity);

只是打电话

entity.saveIn(this);

我重读了这个,我注意到实体仍然很薄,因为它只是将其持久性分配给存储库。但在这种情况下,实体应该很薄,因为您没有描述任何其他域逻辑。在这种情况下,你可以说“螺旋双重调度,给我访问者”。

是的,你可以,但IMO它暴露了你的实体如何实现,而这些访问者分散了域逻辑。我认为唯一应该有get和sets的类是一个名为“Accessor”的类。

我很快就会把它包起来。就个人而言,我不会使用saveIn()方法编写我的实体,因为我认为即使只是使用saveIn()方法也会使分散的域对象乱丢。我使用友元类模式,包私有访问,或者可能使用Builder pattern

好的,我已经完成了。正如我所说,我对这个话题非常着迷。

答案 1 :(得分:1)

当您真正想要编写服务层时,

“服务层对象的瘦模型”就是您所做的。

当您不想编写服务层时,ORM就是您所做的。

当您使用ORM时,您仍然意识到导航可能涉及查询这一事实,但您没有详细说明。

查找表可以是在没有非常完整的对象模型时使用的关系拐杖。而不是引用事物的东西,你有代码,必须查找。在许多情况下,代码只会转换为带有数据库密钥的静态字符串池。相关的方法在软件的奇怪位置结束。

但是,如果有更完整的对象模型,我们有一流的事物而不是这些退化的查找值。

例如,我有一些商业交易,其中有一个 n 不同的“费率计划” - 一种定价模式。现在,遗留关系数据库将费率计划作为查找表,包含代码,一些定价编号和(有时)描述。

[每个人都知道代码 - 代码是神圣的。没有人确定正确的描述应该是什么。但是他们知道代码。]

但实际上,“费率计划”是与合同相关的对象;费率计划有计算最终价格的方法。当应用程序向合同询问价格时,合同会将某些定价工作委托给相关的费率计划对象。

在生成合同价格时,可能会有一些数据库查询继续查询费率计划,但这是两个类之间责任委派的附带条件。

答案 2 :(得分:1)

我与DeadBeef结盟 - 这就是紧张局势。我并没有真正看到领域模型如何“贫血”只是因为它不能自救。

还有更多内容。即。它很贫乏,因为该服务正在执行所有业务规则,而不是域实体。

Service(IRepository) injected

Save(){

DomainEntity.DoSomething();
Repository.Save(DomainEntity);

}

'Do Something' is the business logic of the domain entity.

**This would be anemic**:
Service(IRepository) injected

Save(){

if(DomainEntity.IsSomething)
  DomainEntity.SetItProperty();
Repository.Save(DomainEntity);

}

看到继承的区别?我做:)

答案 3 :(得分:0)

尝试“存储库模式”和“域驱动设计”。 DDD建议将某些实体定义为其他对象的聚合根。每个聚合都是封装的。实体是“持久性无知”。所有与持久性相关的代码都放在一个存储库对象中,该对象管理实体的数据访问。这样,您就不必将与持久性相关的代码与业务逻辑混合在一起。如果您对DDD感兴趣,请查看eric evans书。