有了所有这些服务,我怎么会变得贫血呢?


90

我们在委托与封装业务逻辑之间划清界限的地方是什么?在我看来,我们委派的越多,我们变得越贫乏。但是,委派还促进重用和DRY主体。那么什么是合适的委托,我们的领域模型应该保留什么呢?

以以下问题为例:

授权。域对象应该负责维护其访问控制规则(例如CanEdit属性)还是应该委派给另一个单独负责管理访问的组件/服务,例如IAuthorizationService.CanEdit(object)?还是应该将两者结合?也许域对象具有CanEdit属性,该属性委派给内部IAuthorizationService来执行实际工作?

验证。与上述相同的讨论涉及验证。谁维护规则,谁负责评估规则?一方面,对象的状态应该属于该对象,而有效性是一个状态,但是我们不想重写用于评估每个域对象的规则的代码。在这种情况下,我们可以使用继承...

对象创建。工厂类与工厂方法与“更新”实例。如果我们使用单独的工厂类,则可以隔离和封装创建逻辑,但要以将对象的状态打开给工厂为代价。如果我们的域层在单独的程序集中,则可以通过公开工厂使用的内部构造函数来进行管理,但是如果存在多个创建模式,这将成为一个问题。而且,如果工厂所做的所有工作都在调用正确的构造函数,那么拥有工厂的意义何在?

类上的工厂方法消除了打开对象内部状态的问题,但是由于它们是静态的,因此我们无法像通过单独的工厂类那样通过注入工厂接口来打破依赖关系。

坚持不懈。有人可能会争辩说,如果我们的域对象要在向另一方委派执行授权检查的职责时将CanEdit公开给他人(IAuthorizationService),为什么在我们的域对象上没有执行相同操作的Save方法呢?这将使我们能够评估对象的内部状态,以确定是否可以在不破坏封装的情况下执行操作。当然,这要求我们将存储库实例注入到我们的域对象中,这对我来说有点气味,那么我们是否应该引发域事件并允许处理程序执行持久化操作?

看看我要去哪里?

Rockford Lhotka对他为什么选择CSLA框架中的班级主管路线进行了精彩的讨论,而我对该框架有一些了解,可以看到他的业务对象以多种方式与域对象并行的想法。但是,我试图成为更好的DDD理想的坚持者,我想知道何时协作变得过多。

如果最终以聚合根为IAuthorizationService,IValidator,IFactory和IRepository,还剩下什么?是否具有将对象状态从“草稿”更改为“已发布”的Publish方法足以将类视为非贫血领域对象?

你的意见?


好问题。我没有答案,因为出于几乎相同的原因,我几乎总是在设计上完全陷入贫乏状态-使用/使用/从许多不同的上下文或不同的应用程序公开服务。
hromanko 2011年

很好的问题是,希望看到unclebob,martinfowler,ericevans等人对此感兴趣。现在让我离开并进行深思
Martijn Verburg

我发现自己一直在发展为贫血模型。所以我正为同样的事情而苦苦挣扎。好问题!
L-四

这也是我使用DDD的经验。我们在我工作的地方这样做,我们总是贫乏。我以前(实际上仍然会使用)使用Csla。我们的架构师不喜欢我们贫血,但如果您指出的所有内容无法在对象内完成,则无法给我很好的答案,说明对象应如何处理。归根结底,尝试成为纯粹的DDD似乎创造了比其价值更大的工作。我个人认为,如果您愿意将DDD教条抛在脑后,Csla和DDD会相处(它们在原理上似乎相同)。
安迪

下面是从建模行为(非数据为中心的)透视域时使用的一些技术的示例:medium.com/@wrong.about/...
Zapadlo

Answers:


66

大多数困惑似乎是围绕根本不应该在域模型中存在的功能:

  • 持久性永远不应该在域模型中。永远不能。这就是您依赖抽象类型的原因,例如IRepository模型的一部分是否曾经需要执行诸如检索模型的不同部分之类的工作,并使用依赖注入或某种类似的技术来连接实现。因此,从记录中删除该内容。

  • 授权不是一般您的域模型的一部分,除非它实际上是域名,例如,如果你正在编写安全软件的一部分。允许谁在应用程序中执行哪些操作的机制通常在业务/域层的“边缘”处处理,实际上允许UI和Integration部件与之交谈的公共部分-MVC中的控制器,服务或SOA中的消息传递系统本身...您就会明白。

  • 工厂(在这里我指的是抽象工厂)在域模型中并不是很糟糕,但是它们几乎总是不必要的。通常,只有当对象创建的内部机制可能改变时,您才有一家工厂。但是您只有一个域模型的实现,这意味着将永远只有一种工厂总是调用相同的构造函数和其他初始化代码。

    如果需要,您可以拥有“便利”工厂-封装构造函数参数的常见组合的类,等等-但是说实话,通常来说,如果您的域模型中有很多工厂,那么您只是在浪费行数代码。

因此,一旦您将所有这些都草皮了,就只剩下验证了。那是唯一一个棘手的问题。

验证 域模型的一部分,但它也是应用程序其他所有组件的一部分。基于相似但又不同的概念模型,您的UI和数据库将具有自己的相似但又不同的验证规则。并没有真正指定对象是否需要一个Validate方法,但是即使对象确实具有方法,它们也通常会将其委托给验证器类(不是接口-验证在域模型中不是抽象的,这是基本的)。

请记住,验证器在技术上仍然是模型的一部分;它不需要附加到聚合根,因为它不包含任何数据或状态。领域模型是概念性的事物,通常在物理上转换为装配体或装配体集合。如果您的委派代码与对象模型非常接近,则不要强调“贫乏”的问题。它仍然很重要。

这一切最终归结为,如果您要进行DDD操作,则必须了解域什么。如果您仍在谈论诸如持久性和授权之类的问题,那么您就走错了路。域表示系统的运行状态-物理和概念上的对象和属性。与对象和关系本身不直接相关的任何内容都不属于域模型周期。

根据经验,在考虑某项是否属于域模型时,请问自己以下问题:

“仅出于技术原因,此功能能否更改?” 换句话说,不是由于对实际业务或领域的任何可观察的变化?

如果答案是“是”,则它不属于域模型。它不是域的一部分。

有一天,您很有可能会更改持久性和授权基础结构。因此,它们不是域的一部分,而是应用程序的一部分。这也适用于算法,例如排序和搜索;您不应该将二进制搜索代码实现推入您的域模型中,因为您的域仅关注搜索的抽象概念,而不关注搜索的工作原理。

如果在剥离了所有无关紧要的内容之后,您发现域模型确实是贫乏的,那么这可以很好地表明DDD只是您的项目的错误范例。

某些领域确实贫血。社交书签应用程序实际上并没有太多的“领域”可言。您所有的对象基本上都是没有功能的数据。另一方面,销售和CRM系统的领域非常繁重。当您加载Rate实体时,可以合理地期望您可以按照该费率实际完成工作,例如将其应用于订单数量,并让其计算出数量折扣,促销代码以及所有有趣的内容。

仅保存数据的域对象通常确实意味着您具有贫乏的领域模型,但这并不一定意味着您创建了错误的设计-可能仅意味着领域本身是贫乏的,您应该使用不同的方法。


2
此外,@ SonOfPirate并非一定要在某个时候更改整个安全模型;基于角色的安全性通常被废弃,而倾向于基于声明或基于权限的安全性,或者甚至您想要字段级的安全性。想象一下,如果发生这种情况,尝试重做整个域模型。
亚伦诺特,2011年

1
@SonOfPirate:听起来好像您仍然对旧的“业务层”模型有些犹豫,其中有一个“中间层”,基本上是数据层上的薄薄板,实现了各种业务规则以及通常的安全规则。那不是域层。域是其他所有内容所依赖的域,它表示系统要管理的现实对象和关系。
亚伦诺特2011年

1
@ LaurentBourgault-Roy:对不起,我不相信你。每个公司都可以对每一个应用程序说一遍;毕竟,更改数据库困难。但这并不使它成为您领域的一部分,而与持久层耦合的业务逻辑仅意味着糟糕的抽象。域模型专注于行为,而持久性恰恰不是。这不是人们可以发明自己的定义的主题。DDD中清楚地阐明了这一点。你常常不需要的CRUD或报告应用领域模型,但你也不能说你有一个当你不知道。
Aaronaught

1
授权绝对属于域层。谁决定存在哪些权限?该业务。谁决定谁可以做什么?该业务。几周前,我们刚刚有一个功能请求,要求更改在我们的系统中编辑特定对象所需的授权。如果模板基于主模板,则比通常需要更高的特权来编辑(覆盖主模板中的值)模板。如果不在域中,那么该逻辑属于什么地方?
安迪

1
另一种授权可以是客户帐户限制。普通的客户服务人员可以将其提高到一定程度,但是更高的限制可能需要管理批准。这就是授权逻辑。
安迪

6

授权。域对象是否应负责维护其访问控制规则

不可以。授权本身就是一个问题。由于缺少权限而无效的命令应在域之前尽早被拒绝-这意味着我们经常甚至甚至希望检查潜在命令的授权以构建UI(这样就不会甚至向用户显示编辑选项)。

如果将授权与域模型分开进行组件化,则跨层共享授权策略(在UI中以及在服务或命令处理程序中进一步共享)将变得更加容易。

可能会遇到的一个棘手的部分是上下文授权,在上下文授权中,不仅可以基于用户角色而且还可以基于业务数据/规则来允许或不允许命令。

验证。与上述相同的讨论涉及验证。

我还要说不,不在领域内(主要是)。验证发生在不同的上下文中,并且验证规则通常在上下文之间有所不同。在考虑聚合封装的数据时,很少有简单的,绝对的有效或无效含义。

此外,像授权一样,我们在UI,服务或命令处理程序等中跨层使用验证逻辑。同样,如果DRY是一个单独的组件,则更容易在验证中使用DRY。从实际的角度来看,验证(尤其是在使用框架时)要求公开否则应该封装的数据,并且通常需要将自定义属性附加到字段和属性。我更喜欢将它们放在我的域模型之外的其他类上。

我宁愿在几个类似的类中复制一些属性,而不是尝试将验证框架的要求强加到我的实体中。不可避免地,这会使实体类变得一团糟。

对象创建。工厂类与工厂方法与“更新”实例。

我使用了一层间接。在我最近的项目中,这是用于创建内容的Command +处理程序,即CreateNewAccountCommand。一种替代方法是始终使用工厂(尽管如果其余实体操作由与工厂类分开的服务类公开,则可能会很尴尬)。

不过,总的来说,我尝试在对象创建的设计选择上更加灵活。 new是简单而熟悉的,但并不总是足够的。我认为在这里使用判断并允许系统的不同部分根据需要使用不同的策略很重要。

坚持不懈。...为什么在我们的域对象上没有Save方法

这很少是一个好主意。我认为有很多共享的经验可以支持这一点。

如果最终以聚合根为IAuthorizationService,IValidator,IFactory和IRepository,还剩下什么?是否具有将对象状态从“草稿”更改为“已发布”的Publish方法,足以将类视为非贫血领域对象???

对于应用程序的这一部分,域模型可能不是正确的选择。


2
“可能遇到的一个棘手的部分是上下文授权,在上下文授权中,不仅可以基于用户角色,而且还可以基于业务数据/规则来允许或不允许命令。” -您如何处理?至少对我而言,我们的授权规则是角色和当前状态的结合,这比很多时候都多。
SonOfPirate 2011年

@SonOfPirate:事件处理程序,用于侦听域事件并更新非常符合检查授权的查询需求的表。查看状态并确定角色或个人是否被授权的逻辑存储在事件处理程序中(因此表几乎总是简单的是/否,使auth检查成为简单的查找)。同样,一旦在多个地方使用了任何一种逻辑,它就会从处理程序中重构为共享服务。
2011年

1
总的来说,在过去的几年中,我不再尝试将所有内容整合到一个域或业务对象中。我的经验似乎是,使事情更细化和更少耦合是一项长期的胜利。因此,尽管从一个角度来看,这种方法将业务逻辑放置到域之外(一定程度上),但它稍后也支持敏捷修改。这一切都是为了取得平衡。
2011年

4
在这里引用答案本身:在处理依赖于权限和业务规则的验证时,我发现状态模式非常有用。创建一个抽象状态,该状态被注入到域模型中,并公开将域对象作为参数并验证特定于域的操作的方法。这样,如果您的权限规则发生变化(就像它们几乎总是一样),那么您就不必摆弄模型来适应它,因为状态实现存在于您的安全组件中。
亚伦诺特2011年

1
让我们继续讨论授权问题,让我在桌子上举一个实际的例子,看看您(俩人)将如何处理它。我在域对象上执行了发布操作,该操作要求用户具有发布者角色并且该对象处于某种状态。我想隐藏或禁用UI中的“发布”按钮。您将如何实现?
2011年

4

好的,这适合我。我会这样说来避免:

  • 过早的优化(包括设计)通常会引起问题。

  • IANMF(我不是Martin Fowler);)

  • 一个肮脏的小秘密是,在小型项目(甚至可以说是中型项目)上,方法的一致性至关重要。

授权书

对我而言,身份验证和授权始终是一个交叉问题。在我快乐的Java小世界中,这委托给了Spring安全性或Apache Shiro框架。

验证 对我而言,验证是对象的一部分,因为我将其视为定义对象是什么。

例如,一个Car对象有4个轮子(好的,有些怪异的例外,但是现在让我们忽略这个奇怪的3轮Car)。除非有4个汽车(在我的世界中),否则汽车根本是无效的,因此验证是汽车定义的一部分。这并不意味着您不能拥有帮助程序验证类。

在幸福的Java世界中,我使用Bean验证框架,并在大多数Bean字段上使用简单的注释。无论您位于哪一层,都可以轻松地验证对象。

对象创建

我谨慎查看工厂类。我经常去看xyxFactoryFactory课;)

我倾向于new根据需要创建一个对象,直到遇到需要进行依赖注入的情况为止(并且由于我尝试遵循TDD方法,所以这种情况经常会出现)。

在我快乐的Java世界中,Guice越来越多,但Spring仍然是国王。

坚持不懈

因此,这场辩论在圈子和回旋处进行,我对此始终有两种看法。

有人说,如果您以“纯粹”的方式看待对象,则持久性不是核心属性,而仅仅是外部关注点。

其他人则认为您的域对象隐式实现了“可持久”接口(是的,我知道我在这里进行扩展)。因此,它的罚款有不同savedelete他们等方法。这被视为一种务实的方法,许多ORM技术(在我快乐的Java世界中是JPA)都以这种方式处理对象。

考虑到跨领域安全性,我确保在调用对象上的save / update / delete方法的服务上正确设置了edit / delete / add / whatever权限。如果我真的很偏执,甚至可以设置域对象本身的权限。

HTH!


2

吉米·尼尔森(Jimmy Nilsson)在他关于DDD的书中谈到了这个话题。他从一个贫血模型开始,在后来的项目中进入非贫血模型,最后选择了贫血模型。他的理由是,贫血模型可以在具有不同业务逻辑的多种服务中重复使用。

需要权衡的是缺乏发现能力。您可以在贫血模型上使用的方法遍及其他位置的一组服务中。


听起来像是一项特殊要求-数据的重用(应力“数据”结构)-导致该通用部件简化为普通DTO。
Victor Sergienko

贫血模型可以更好地重用吗?听起来更像是DTO,我个人也不会对重用属性定义感到讨厌。我宁愿重用行为。
安迪

@Andy-但是,如果您的行为在Domain Services中并且它们在贫血的对象上运行(如果愿意,可以使用DTO),那我是否同意呢,那不会增加这些行为的重用性吗?只是扮演恶魔的拥护者。
jpierson 2011年

@jpierson我发现尽管这些行为通常是特定于特定用例的。如果存在重用,那将属于另一个类,但是使用者不会使用这些类,那么他们将使用特定于用例的类。因此,可以说,任何重用都在“幕后”。此外,尝试重用模型通常会使模型更难以供消费者使用,因此最终您将在UI层中创建视图/编辑模型。通常,您最终会违反DRY来提供更丰富的用户体验(例如,编辑模型的DataAnnotations)。
安迪

1
我宁愿为用例构建域模型,并在有意义的地方重用(即,可以重用而无需过多或根本不修改行为)。因此,您有一个智能的可编辑域模型,而不是无用域模型,服务类和编辑模型。我发现使用和维护起来要容易得多。
安迪(Andy)

2

这个问题是很久以前问过的,但已用“域驱动设计”标记。我认为问题本身包含对整个实践的基本误解,而答案(包括已接受的答案)使基本误解永存。

DDD体系结构中没有“域模型”。

让我们以授权为例。让我考虑一个问题:假设有两个不同的用户对您的系统进行身份验证。一个用户有权更改某个实体,而另一用户则无权。为什么不?

我讨厌简单而人为的例子,因为它们常常使人困惑,而不是启发人。但是,让我们假设我们有两个不同的域。首先是用于营销代理的CMS平台。该机构有许多客户,所有客户都有在线内容,这些内容需要由复制作家和图形艺术家管理。内容包括博客文章以及针对不同客户的登录页面。

另一个领域是制鞋公司的库存管理。该系统管理库存,从法国的制造商到美国大陆的分销中心,再到本地市场的零售店,再到在零售店购买鞋子的客户,都可以管理库存。

如果您认为两家公司的授权规则相同,那么可以,这是该域外服务的理想选择。但是我怀疑授权规则是否相同。甚至用户背后的概念也会有所不同。当然,语言会有所不同。营销代理机构可能具有职位作者和资产所有者的角色,而制鞋公司可能具有货运业务员,仓库经理或商店经理的角色。

这些概念可能具有与之相关的各种权限规则,需要在域中进行建模。但这并不意味着它们即使在同一应用程序中也都属于同一模型。因为请记住,存在不同的绑定上下文。

因此,也许可以考虑在授权的上下文中使用非贫富域模型,而不是将鞋类商品路由到库存量低的商店或将站点访问者路由到适当的登陆页面,这取决于他们点击的广告。

如果您发现自己使用的是贫乏的领域模型,那么也许您只需要在开始编写代码之前花更多的时间在上下文映射上即可。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.