`
taowen
  • 浏览: 193455 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

再论领域模型的困境

阅读更多
距离上次发帖讨论领域模型,已经有半年了。这么久没有炒,估计饭又冷了。我再来炒炒领域模型这锅冷饭吧。且不着急点回退按钮,最近领域驱动设计社区在Greg Young同学的带领下有不少新的发展。保证这一次不会是重复贫血充血的老调调。
上回我们说到领域模型实践中的两个困境。一个是框架带来的Entity无法注入的问题。另外一个是Java无Mixin带来的类膨胀的问题。没有看过上文的同学请先回去复习一下:http://www.iteye.com/topic/281289。今天我们就不谈其他,就这两点来谈一谈吧。

类膨胀

这是一个只有你真正把逻辑都放到领域模型了才会遇到的高级问题。当我们把逻辑不断地从Service层抽到Domain层的时候,一些核心的Entity类往往会变得巨大无比。直觉告诉我们,这肯定违反了Single Responsibility Principle(所谓SRP)。那么我们怎么才能解决这个问题呢?

Mixin
当时我发帖的时候,觉得解决这个问题的方向是Mixin。Ruby有Module,C#有Extension Method,Java缺乏语言原生的支持,所以有qi4j这样的项目(我同意,qi4j的实现确实是有点那个。。。)。但是经过一段时间来的学习和思考,觉得Mixin只是一种头痛医头脚痛医脚的办法,根本没有从根本上解决问题。
使用Mixin只是把行为的定义分开了,分散到了几个源文件去定义了。但是逻辑上行为仍然是在那个Entity上的。而且运行时,行为也是在那个Entity上的。从代码阅读的角度来说,确实一次看到的源代码行数是变少了,但是从整体理解的角度来说,读懂一个Entity的复杂度并没有降低。从某种角度来说,Mixin就像是从前的宏(Macro),都是神奇般地给你的代码加点料。

职责委托
发生膨胀的类往往是一些Aggregate Root,把聚合了很多子Entity。比如说ShoppingBasket聚合了Package,而Package聚合了Item。很多时候,我们可以把职责委托给这些子Entity。比如Item可以计算自己的价格,然后Package再把Item的价格加总,然后Basket再把Package的价格加总。通过把职责委托出去,Aggregate Root更多的是一个Mediator,协调各方面来完成任务,而不是事事都必须亲历亲为。
要把职责委托出去就必须让这些职责有一些接收方。如果之前productPackages只是一个List,这个时候就可以创建一个自定义的ProductPackages类来持有相关的逻辑。如果之前几个field联系紧密(比如一个叫fromDate,一个叫toDate),就可以把这些联系紧密的field打包成一个类把相关的职责委托给它。
当我们把职责委托出去之后,Aggregate Root在某些场合只是Middle Man。比如
ProductPackage findByName(String name) {
  return productPackages.findByName(name);
}

如果对这些纯委托的方法感觉不爽,不妨提供getProductPackages方法让外边直接调用findByName好了。

Bounded Context
一般来说,通过有效的职责委托,完全可以避免一个Entity的尺寸过大。但是这必须建立在你所写的系统的职责单一的基础之上。也就是说单个类的SRP必须建立里在系统的SRP之上。如果像这位同学说的那样:
coolnight 写道

我们的系统有很多模块组成, 各模块基本上通过数据库来共享信息。
主要的模块大致有: 核心系统(非web), 网站、 bbs、 网站后台,核心系统后台,BI, 推广员等等
原来的打算是写个rich domain model供各模块来使用,以便代码重用减轻个模块开发的工作量
一个简单的例子, User 原有 changePassword, getFriends, addFriend ... 等等方法撇开配置以及获取User对象的复杂性不谈, 实际开发中发现, 这些东西重用的地方太少了,对网站来说很好的rich domain model, 在网站后台里面就麻烦,很多方法在那里根本就是对后台开发人员的干扰,而很多方法对核心系统、BI等等根本就毫无用处。

那么他所说的User是无论如何做不到SRP的。用Eric Evan的术语就是我们在处理不同的Bounded Context。所以对于之前我画的那个图,现在就有不同理解了:

当时我的理解是一个类在不同的Context下有不同的职责(Role),所以需要实现不同的Interface代表这些Role。于是乎类就是封装一组数据在不同的Context下的行为。又由于系统往往有很多的context,而类所封装的数据又要被这些context给共享(比如User),所以一个类就无可避免地要变得非常的膨胀。
我犯了两个错误。首先Interface代表的Role不是Bounded Context这个级别的。让一个User去实现ForumUser接口,NewsUser接口,SnsUser接口从而被不同模块共享是不现实的,也没有人去这么做。其次,在边界划分良好的情况下,一个系统内应该不会有太多的context,如果一个系统做了很多不同的事情,那是在系统规划设计上就出了问题,而这样的问题比面向对象设计一个类的问题要大得多。
所以,从根本上避免类膨胀,就必须首先避免系统承担的职责的膨胀。理想情况下一个团队负责一个模块/系统,只处理一个Bounded Context。然后跨Bounded Context的集成不是靠一个对象封装一组数据实现不同系统的接口来实现(那简直是开玩笑),而是靠Context Mapping来实现。具体的Mapping的措施,在下文中讨论。

Entity依赖Service

之前我也讨论过,很多朋友也讨论过如何用各种各样tricky的技术实现对Entity的依赖注入。但是,Entity为什么会有这些依赖?没有这些依赖存在的话,Entity就无法完成自己的职责,我们就必须把逻辑写到所谓的Application Service之中吗?
总结起来,Service依赖有三种情况:

没有,就很慢
理论上来说,Domain Model就是一个大的对象图。对象之间可以通过之间的关系彼此获得。通过Navigate对象图,我们可以从一个节点到达了任意地方。但是由于效率的原因,很多对象之间的关联必须人为打断。比如说你是一个User,用户可以发信。如果User有一个sentEmails的属性,我们去访问这个属性的成员的时候就可能触发成千上万条SQL。所以从实践中,像User这样的长生命周期对象是不会有链接到Email这样的短生命周期的对象的。
一旦Domain Model不再是一个完整联通的对象图,我们的Entity就无法通过Navigation拿到和自己协同工作的对象了。所以,往往Entity需要一些DAO或者Repository来拿到自己的关联对象。这样的优化我们称之为Replace field with query。解决办法在以前贫血不贫血的讨论中已经有反复提及了:http://www.iteye.com/topic/191261。唯一欠缺的是具有Production Quality的实现方案而已。折衷的措施是把Repository当参数传递进去,或者使用Query Object模式。或者干脆就放到Application Service中做好了。

没有,数据就拿不到,服务拿不到
这种情况是一些业务操作需要另外一个系统提供的数据,比如说是一个提供pricing的web service。如果没有这个web service,我们就只能把计算总价的职责从domain model中拿出来,因为它没有办法很容易的拿到一个web service的引用。
再比如说,验证一个ShoppingBasket是不是合法,可能需要规则引擎中定义的一些规则(规则可能是业务专家用Excel定义的)。这样basket就不能validate自己了,这样我们也不能让basket告诉我们是不是可以checkout了。

没有,数据就发布不出去
另外一种情况是一些业务操作要把一些数据发出去。比如说publication.distribute需要用ftp把元数据和附件传给一些第三方系统。
又比如,你给一个meeting添加一个note需要给meeting的参与者发一些alert,告诉他们有人更新了meeting的note了。如果这种alert不是系统的内,比如是email或者是MSN的消息,那么就需要在domain model里做一些向外发布数据操作。

Bounded Context Mapping
第一种情况是对象图存取的问题,属于另外一个范畴的问题。不过第一种情况是大部分人想要给Entity注入Service的动因。但是这种情况下,注入不是一个好主意。理想的情况应该是Infrastructure(Hibernate这一层的东西)能够提供更好的Replace field with query的支持。
第二三种情况是因为Bounded Context A对Bounded Context B需要做Context Mapping。Mapping可以是从A到B的(发),也可以是从B到A的(取)。根据Mapping发生的时机又分为预先取,实时取(同步),实时发(同步),实时发(异步),事后发。

预先取
这种情况适用于另外一个Bounded Context的数据的实时性不强,而且尺寸不大。可以预先获取并缓存。

实时取(同步)
这种情况是需要Domain Service的唯一情况。Eric Evan的书中并没有详细说什么情况下需要Domain Service。很多同学都把Domain Service和Application Service搞混了。Domain Service存在,必须是Bounded Context A对于Bounded Context B有实时的同步的获取服务的要求。Shipping那个例子里的ScheuleService,Online Shopping的PricingService,或者依赖于某规则引擎都适用于这种情况。

实时发(同步)
一般来说都不需要是同步的,因为只是发。推荐把同步发改为异步发。不然也需要提供一个Domain Service来做同步的发。

实时发(异步)
这就是Greg Young同学非常津津乐道的Distributed DDD的基本原理了。如果Bounded Context A需要给Bounded Context B发消息,可以在Bounded Context A中建立一个List代表Bounded Context B的InBox。我们只需要把以往的DTO改名为Message然后往队列里一扔就代表我们给B的InBox发了一封信了。然后由Infrastructure取监听那个List取做真正的跨进程通信,可能是调用某个web service,也可能是往message queue发消息。

事后发
如果实时性不强的话。上面提到的那个List都不需要是实时监听的。只需要在业务操作完成之后检查一下List是不是非空。如果有东西,就发出去。

结论
上篇帖子提出的两个阻碍领域模型应用的因素按照分析可以列为:
  • 类膨胀
  • 框架没有提供Replace field with query的能力
  • Entity引用Domain Service
  • Entity做Messaging

对于类膨胀,我们一方面要把职责委托出去,另外一方面是关注应用程序本身(而不仅仅是类)的职责是不是太多。
依旧期待框架提供更好的Replace field with query的能力。
Entity引用Domain Service的情况不多。如果有,可以考虑用参数传进去。注入也可以考虑,如果不麻烦的话。
Entity做Messaging一般人都用不着。如果需要,实现起来也不难。
分享到:
评论
4 楼 testoktest2 2009-08-06  
为什么要:让一个User去实现ForumUser接口,NewsUser接口,SnsUser?

不能 ForumUser类/NewsUser/SnsUser 都继承 User类吗
f/n/s 有自己不同的方法
listMessage()
listNews()
changeNews()
deleteNews()

怎么管理用户
有adminUser类,有方法
deleteUser/changeUserPassword/createUser

被管理的user就是 User类,有啥Password属性
那有没有listNews()方法,肯定没有马,被管理的user 当前根本不是NewsUser
如果要看被管理的user,一共发了多少新闻,有个userRole属性,
还有user.userRole.NewsUser
之后就是
user.userRole.NewsUser.NewsCount()
user.userRole.NewsUser.listNews()

这样难道不行?

3 楼 raymond2006k 2009-06-17  
楼上说的没错, 领域建模仍要保持简单的原则。

我们实践中,可能早上听了一堂《领域建模》的培训,觉得无比优雅,“就应该这样”;可是下午因为项目赶进度,就随意添加属性和方法,而违背了领域建模的原则。 更深的原因确实是 framework 没能提供一个符合domain思想的建模规范和约束,例如 Hibernate 侧重ORM,它的 Domain Modeling 还是以 Data Model 为中心的领域建模,而没有上升到行为和事件(虽然它也支持Event,但是是数据级的)。

当然,类膨胀是要势待解决的问题。遵守domain思想下,设计思路要有所突破,怎样优雅的委托出去,怎样做 context mapping等。



2 楼 firebody 2009-06-03  
观点都是正确。

但是我觉得这么多正确的观点,反而忽略了一个最基本的观点:简单,美妙的代码需要简单、美妙的设计作为底层支撑。
设计体现在 领域模型的设计,整体架构的设计这些基本方面。


很赞同某位软件大师说过的话,具体什么话忘了,大概意思是这么说: 怎么定义这个代码是简单、美妙的呢? 你只需要看它是否自始至终都保持一个核心设计理念。  如果他能做到这点,那么他就是简单美妙的。

所以,很有意思的是,如果你发现你自己写的代码膨胀了, 立即重构,重构有两个层次: 代码级别的重构,设计级别的重构。

前者大家经常做,后者大家也别忘了要经常做,后者的原则就是一点: 保持简单美妙的核心设计理念,贯穿在你所有代码里面。

做到这点了,也不需要像楼主这么费心费力了,呵呵,开玩笑。



1 楼 yimlin 2009-06-03  
搬个板凳先

相关推荐

    博弈论-囚徒困境思路

    其中,“囚徒困境”是博弈论中的经典模型,它展示了个体理性可能导致集体非理性的结果。在这个模型中,两个被捕的囚犯面临是否互相背叛的选择,如果两人都保持沉默,他们都能获得较轻的刑期;如果一方告发另一方,...

    博弈论的几个经典模型.ppt

    博弈论的经典模型还包括囚徒困境模型、纳什均衡模型等。囚徒困境模型是指两个人在面临共同的处罚时,选择是否合作的博弈。纳什均衡模型是指参与者在博弈过程中选择的策略,使得自己的支付最大化。 博弈论是一个研究...

    数学建模常用模型论文汇总(1)

    经典的博弈论模型包括零和博弈、合作博弈和非合作博弈,如著名的“囚徒困境”。 2. **层次分析法(AHP)**:层次分析法是由托马斯·塞蒂提出的决策分析工具,用于处理多目标、多准则的复杂问题。它通过将目标、准则...

    博弈论中的“囚徒困境”模型1

    "囚徒困境"模型是博弈论中的一个经典案例,由Tucker在1950年提出,用于描述在非合作博弈环境下,个体最优选择可能导致集体次优结果的现象。该模型通常分为四种形式:完全信息静态博弈、完全信息动态博弈、不完全信息...

    博弈论与囚徒困境展示PPT教案学习.pptx

    在博弈论中,囚徒困境(Prisoner's Dilemma)是最经典、最著名的博弈模型之一。它可以扩展到许多经济问题,以及各种社会问题,可以揭示市场经济的根本缺陷。 囚徒困境模型是由梅里尔·弗勒德(Merrill M. Flood)和...

    数据模型与决策博弈论.pptx

    博弈论的经典模型有多种,包括囚徒困境、旅行者困境、零和博弈等。这些模型都可以用来解决实际问题,如资源分配、风险投资等。 博弈论的应用非常广泛,包括经济学、管理学、政治科学、社会学、生物学等领域。例如,...

    我国上市公司财务困境预测模型实证研究

    文章中提到的关键词包括财务指标、预测系统、财务困境、Logit回归模型,这些关键词涵盖了本研究的主要内容和方法论。文章的中图分类号为F275,文献标识码为A,表示该文章属于财务领域的重要研究论文。此外,文章还...

    博弈论三大模型.docx

    在博弈论中,有三个著名的模型,分别是“囚徒困境”、“智猪博弈”和“斗鸡博弈”。 首先,我们来看“囚徒困境”。这个模型源自1950年代,由梅里尔·弗勒德和梅尔文·德雷希尔提出,并由艾伯特·塔克以囚犯的场景来...

    博弈论的几个经典模型整理.ppt

    博弈论的经典模型包括囚徒困境、纳什均衡、帕累托最优、少数者博弈等。这些模型都是研究互动决策和策略选择的重要工具。 在博弈论中,策略选择是指行动方在考虑他人的策略选择时所采取的行动。策略选择的结果取决于...

    对策论模型

    ### 对策论模型详解 #### 一、引言与历史背景 对策论,又被称为博弈论,是一种专门研究具有竞争或斗争性质现象的数学理论与方法。它不仅被视为现代数学的一个分支,也是运筹学领域的重要组成部分。对策论的研究...

    博弈论经典模型全解析汇报(入门级).doc

    本篇文章主要介绍了三个经典的博弈论模型:囚徒困境、智猪博弈和枪手博弈,这些都是理解博弈论基本概念的重要例子。 1. **囚徒困境**: 囚徒困境展示了在合作与背叛之间的两难选择。两个囚犯如果都保持沉默,可以...

    复杂网络囚徒困境博弈matlab源程序

    在IT领域,尤其是在复杂系统建模与仿真方面,囚徒困境博弈(Prisoner's Dilemma)是一个重要的理论模型,它常被用来研究合作与背叛的行为动态。此模型结合了数学、经济学和计算机科学,而Matlab作为一种强大的数值...

    论文研究-模型驱动架构的研究及工具实现 .pdf

    MDA将软件系统的模型分为平台无关模型(PIM)和平台相关模型(PSM),并通过转换规则将它们统一起来,旨在摆脱需求变更带来的困境,并促进设计层次的交换和重用。 平台无关模型是对系统高层次的抽象,不包含与实现...

    数据模型与决策--博弈论.pptx

    1994年的诺贝尔经济学奖表彰了纳什、哈萨尼和泽尔腾在博弈论领域的开创性工作。哈萨尼和泽尔腾的研究扩展了非合作博弈的理论,而纳什的贡献在于提出纳什均衡和纳什讨价还价解,为理解互动决策提供了基础。 信息经济...

    博弈论囚徒困境的四种形式.pdf

    博弈论中的“囚徒困境”是一个著名的理论模型,它展示了两个理性个体在特定条件下如何做出非合作决策,即使合作对他们双方都有利。该模型由美国数学家亚瑟·潘洛斯·塔克(Arthur Samuel Tucker)在1950年提出,被...

    博弈论模型[借鉴].pdf

    博弈论是一种应用数学模型,用于分析决策者之间互动行为的结果,尤其在经济学、政治学、社会学和生物学等领域有着广泛的应用。在这个模型中,我们主要讨论了三个经典的博弈论案例:囚徒困境、智猪博弈和枪手博弈。 ...

    博弈论模型.pdf

    博弈论是一种分析决策者之间互动行为的数学工具,广泛应用于经济学、政治学、社会学以及生物学等多个领域。在教育中,学习博弈论可以帮助人们理解复杂情境下的决策制定和策略选择。 1. 囚徒困境是博弈论中的经典...

    基于matlab实现规则网络模型囚徒困境的模拟 可以作为对演化博弈感兴趣的一个例子学习.zip.rar

    囚徒困境是博弈论中的一个经典模型,用来研究合作与背叛的决策问题。两个囚犯面临两个选择:合作(双方都沉默)或背叛(一方揭发另一方)。如果两人都合作,他们会得到较小的惩罚;如果两人都背叛,他们都会受到较大...

    博弈模型.pdf

    囚徒困境模型是博弈论中一个经典的模型,假设两个小偷合伙作案时被捕,分别关在不同的屋子里,如果双方都拒绝承认同伴的罪行,则由于证据不足两人都会被轻判;如果一方出卖同伴,而另一方保持忠诚,则背叛者将无罪...

    数学建模经典模型汇总

    在数学建模领域,经典模型是理解和应用数学解决实际问题的关键。这个名为“数学建模经典模型汇总”的压缩包文件,显然为初学者提供了一个全面的学习资源,涵盖了基础的数学建模模型。以下是对这些模型的详细介绍: ...

Global site tag (gtag.js) - Google Analytics