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

以以下问题为例:

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

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

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

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

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

请参见

Rockford Lhotka对他选择CSLA框架的主管路线的原因进行了很好的讨论,我对该框架有一些历史,可以看到他以多种方式并行化域对象的业务对象的想法。但是,为了更加遵守DDD理想,我想知道协作何时会变得过多。

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

您的想法?

评论

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

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

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

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

这是从行为(非以数据为中心)角度对域建模时使用的一些技术示例:medium.com/@wrong.about/…

#1 楼

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



持久性永远不应该存在于域模型中。永远不能。这就是如果模型的一部分需要执行诸如检索模型的不同部分之类的操作,并使用依赖项注入或某种类似的技术来连接实现的原因,则依赖诸如IRepository之类的抽象类型的原因。因此,请从记录中删除。
授权通常不是域模型的一部分,除非它实际上是域的一部分,例如如果您正在编写安全软件。允许谁在应用程序中执行哪些操作的机制通常在业务/域层的“边缘”处处理,实际上允许UI和Integration组件与之交谈的公共部分-MVC中的控制器,服务或SOA中的消息传递系统本身...您就会明白了。

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

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


因此,一旦您将所有这些草皮草皮化,就只能进行验证。那是唯一一个棘手的问题。

验证是域模型的一部分,但它也是应用程序其他所有组件的一部分。根据相似但又不同的概念模型,您的UI和数据库将具有自己的相似但又不同的验证规则。并没有真正指定对象是否需要一个Validate方法,但是即使有,它们通常也会将其委托给验证器类(不是接口-验证在域模型中不是抽象的,这是基本的)。 />
请记住,验证器在技术上仍然是模型的一部分;它不需要附加到聚合根,因为它不包含任何数据或状态。领域模型是概念性的事物,通常在物理上转换为装配体或装配体集合。如果您的委派代码与对象模型非常接近,则不要强调“贫乏”的问题。它仍然很重要。

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

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

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

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

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

除去所有无关紧要的内容,您会发现域模型确实是贫乏的,那么这可以很好地表明DDD只是您的项目的错误范例。

有些领域确实贫血。社交书签应用程序实际上并没有太多的“领域”可言。您所有的对象基本上都是没有功能的数据。另一方面,销售和CRM系统的领域非常繁重。当您加载一个Rate实体时,有一个合理的期望,您可以实际使用该费率执行商品,例如将其应用于订单数量,并让其找出批量折扣和促销代码以及所有有趣的商品。 />
仅保存数据的域对象通常确实意味着您具有贫乏的领域模型,但这并不一定意味着您创建了错误的设计-可能仅意味着领域本身是贫乏的,您应该使用其他方法。

评论


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

– Aaronaught
11年4月28日在14:18

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

– Aaronaught
2011年4月30日15:40



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

– Aaronaught
2014年1月23日在1:49

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

–安迪
17年7月15日在18:09

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

–安迪
17年7月15日在18:10

#2 楼


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


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

如果将授权与域模型分开进行组件化,则跨层(在UI中以及在服务或命令处理程序中)共享授权策略会变得更加容易。 br />
可能遇到的一个棘手的部分是上下文授权,在上下文授权中,不仅可以基于用户角色,而且还可以基于业务数据/规则来允许或不允许命令。


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


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

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

我宁愿在几个相似的类中复制一些属性,也不愿将验证框架的要求强加到我的实体中。这不可避免地使实体类混乱。


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


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

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


持久性。 ...为什么在我们的域对象上没有Save
方法


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


如果我最后遇到了
IAuthorizationService,IValidator,
我的IFactory和IRepository
聚合根,还剩下什么?是否具有
一个Publish方法,该方法将对象的
状态从Draft更改为
已发布,足以将类
视为非贫血领域对象?


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

评论


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

– SonofPirate
11年4月27日在19:15

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

–quentin-starin
2011年4月27日在19:21

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

–quentin-starin
2011年4月27日在19:25

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

– Aaronaught
2011-4-27 21:57



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

– SonofPirate
11年4月28日在12:29

#3 楼

好的,这适合我。我会先这样说:


过早的优化(包括设计)通常会引起问题。
IANMF(我不是Martin Fowler);)
一个肮脏的小秘诀是,在小型项目(甚至可以说是中型项目)上,方法的一致性至关重要。

授权

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

验证
对我来说,验证是对象的一部分,因为我认为验证是定义对象是。

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

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

对象创建

我谨慎地查看Factory类。我经常看到xyxFactoryFactory类;)

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

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

持久性

所以这场辩论绕圈而行,我总是对此有两种看法。

有人说,如果您以“纯粹”的方式看待物体,持久性不是核心属性,而只是外界关注的问题。

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

出于对安全性的关注,我确保编辑/删除/添加在调用对象上的save / update / delete方法的服务上正确设置了/ whatever权限。如果我真的很偏执,甚至可以设置域对象本身的权限。

HTH!

#4 楼

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

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

评论


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

– Victor Sergienko
11年4月29日在11:01

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

–安迪
2011年10月22日,下午1:56

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

– jpierson
2011年12月16日下午5:20

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

–安迪
2011-12-17在1:18

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

–安迪
2011年12月17日的1:20

#5 楼

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

DDD体系结构中没有“领域模型”。 />
让我们以授权为例。让我考虑一个问题:假设有两个不同的用户对您的系统进行身份验证。一个用户有权更改某个实体,而另一用户则无权。为什么不呢?

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

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

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

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

因此,也许有人可能会认为在授权上下文中的非贫乏领域模型与将鞋子运送到库存量低或路由少的商店的上下文不同网站访问者根据其点击的广告来访问相应的目标页面。

如果您发现自己的领域模型很贫乏,也许您只需要花更多的时间在上下文映射上就可以开始编写代码。