对领域驱动设计的初步认识(六)

    第一次看到banq关于JF的PPT时,我突然发现原来可以这样建模,领域模型是这样丰富,让领域对象充满了色彩。过去看到的OO都是“简单实体建模”:即从需求中挖出几个实体对象,然后填充属性,至于其它的东西只要会查询就行了。这种建模方式随处可见,实在是太流行了,似乎让我们相信其实OO就是“贫血对象”。DDD之所以让领域对象富有生命,是因为值对象的存在。DDD采取的是“特征建模”思想,领域对象不仅仅有实体对象,还有表示状态的值对象。
    DDD强调“充血模型”。在复杂业务中,这很有用,可以提高对象的可复用性。但是要注意,“充血模型”虽然很好,但其背后却隐藏玄机。复杂场景中,如果所有的行为都放到了对象中,那领域对象就会变得沉重无比,带来各种副作用。同时,描述状态的值对象往往不是一蹴而就,而是充满着变化,难以把握。当业务不明时,"贫血模型"容易把握,这也就是为什么现在”充血模型“不多,而"贫血模型"大行其道的原因。
    DDD认为"领域对象应该是有行为的",大家都认可。如果说"贫血模型"造成了对象和行为的分离的话;那么"充血模型"也是有问题的,因为DDD中简单地认为把行为放到对象中就行了,殊不知这其实是关于对象和行为之间的一种静态地僵化的看法。而DCI对这种思想进行了修正,认为对象在场景中才具有行为。对于DCI中的对象(数据),我的理解和banq不同。我认为DCI中的对象(数据)是一种裸对象,但却不是简单意义上的"贫血对象"。DCI中的对象和行为是一种动态关系,是依赖于场景而存在的。这也就是说对象不会无缘无故的产生行为,(呵呵,那是神经病),对象具有行为一定是有意义的。也许有人担心场景会变成"贫血对象"模式下service那样的噩梦,造成对象和行为的分离。其实不用担心,在DCI中可以用"事件"把对象和行为联系在一起就避免了"贫血对象"的问题。呵呵,这是不是应了中国的一句老话:"分久必合,合久必分"。
    关于jivejdon中的Account,在我看来不太准确。我是从复杂场景来看这个问题的,我们都知道在企业里account这个领域对象是属于HR模块的,真正的Account对象并不独属于Jive这个域。所以在jivejdon中应该有一个独立的account对象,然后有一个AccountInJive对象,这个AccountInJive对象才拥有Account和AccountMessageVO值对象。(大家可以讨论一下:如果Account在另外一个服务器上,那么这个account在JiveJdon是贫血对象还是领域对象?)
    另外,对于DDD中的聚合,我也有不同的理解。其实DDD提出聚合的概念是为了保证领域内对象之间的一致性问题。但是我对DDD中对"聚合"这个概念的落地形式表示质疑,DDD特别强调聚合根的封装性,然而这可能会导致领域内对象之间的逻辑强耦合。也许有人说领域内部的对象是高内聚的,这样做没关系。但是在实战中,领域模型内部的值对象往往存在着变数,这是我们认识客观世界的必然规律。然而这会导致领域模型的不稳定性。所以我认为及时领域内部的对象也应该注意低耦合,这个问题同样需要靠事件来解决,事件才是保证领域对象一致性的关键。
[该贴被flyzb于2010-11-17 22:56修改过]

写得很好,前面我基本赞同,我感觉如果你能再看一下“如何从职责和协作中发现丰富对象?”,其中主要是讲相互作用,事件的,会对你的思路有很大补充。

2010年11月17日 22:48 "flyzb"的内容
然而这会导致领域模型的不稳定性。所以我认为及时领域内部的对象也应该注意低耦合,这个问题同样需要靠事件来解决,事件才是保证领域对象一致性的关键。 ...

领域聚合内部的松耦合因为天然的业务的高聚合新,就只能通过软件设计上的松耦合方式来实现,比如设计模式等。

为保证一致性,一般采取在实体根对象中放入对值对象或其他状态实体修改的方法行为,外界不可以绕过根实体直接修改状态。

另外,值对象我过去也认为可适合保存状态,当时也有一些争论,现在看来不是很合适,状态应该属于实体的一个重要部分,如果状态复杂,可以用专门的状态模式等设计方式消肿。

所以,对于大家担心“充血模型”会变得沉重无比是多余的,因为这里面忽视了软件设计模式的作用。

总体设计来讲:分两个方面:根据业务的设计;根据纯软件设计目标松耦合的设计,通过这两种切分方式,可以将业务真正落实为好的软件,而我们过去常常注重业务的切分设计,忽视软件层面设计,也就是设计模式。

当然,掌握设计模式的人去进行业务分析设计时,发现设计模式毫无用处,这是因为他没有掌握业务建模设计的原因,业务建模设计好了,自然软件设计就派上用上。

业务建模设计和软件设计相当于切菜的大刀和小刀,对于整只猪,必须先用大刀,然后再用小刀。

有了这两把刀,就不用担心胖对象的沉重,相反,如果你一开始做成很瘦的只有数据的贫血模型,设计模式真的一无用处了,然后,你的系统就走上了依赖数据库数据表的系统,根本没有设计,用所谓OO语言,做delphi的活,白折腾,白牺牲脑细胞。


呵呵,和我想的差不多,但DDD也有存在价值的,就是对于一些“相对稳定”的领域,仍然具有快速开发的优势存在。例如一些不会考虑扩展业务的领域。“一般稳定”的目前是EDA和DCI具有优势。对于一些“绝对不稳定”的(简单桌面软件之类,关注面不在用户,只在功能上,时不时增加功能的那种,类似监控程序等),果断面向功能开发。

我觉得jivejdon是DDD上再作EDA的优化,框架混合使用本来就是很自然的事情╮(╯▽╰)╭

若要在实际中选择合理的架构,则需要架构师的拥有相当实力了。

而DDD中的聚合问题,我觉得更多的是生命周期一致性的问题。至于值对象,看它是统计什么,例如是统计message的,则看message聚合到哪里,是forum,则把该值对象聚合到forum,从这看来也是生命周期问题。想优化性能则引入异步事件。其实DDD是把以前忽略掉的生命周期问题,重新放上来,而且当作重点,也是为什么DDD领域中充满“生命”的原因。还有生命周期一致不代表需要同步。只是同生共死而已,不是你走一步,我走一步。所以存在变数是没什么问题的。


[该贴被SpeedVan于2010-11-18 15:05修改过]
[该贴被SpeedVan于2010-11-18 15:10修改过]
[该贴被SpeedVan于2010-11-18 15:11修改过]

    嗯。。banq说的我都明白,DDD的道路是正确的,这个没有问题。我只是想反思一下:为什么“贫血模型”大行其道,而DDD成功的案例少,失败的多呢?难道仅仅是懂DDD的人太少了吗?为什么有道友说“稳定的业务才适合DDD”呢?在这背后还有什么是我们忽略的东西呢?


业务建模设计和软件设计相当于切菜的大刀和小刀,对于整只猪,必须先用大刀,然后再用小刀。

    看来banq也同意DDD需要对业务精雕细琢才可以,可是浮躁而且不了解企业业务的软件公司对业务分析常常会发生偏差,甚至是大的错误,这会导致业务模型在后期发生较大的变化,而原来的基于错误业务的设计模式反而成了掣肘。对于DDD而言,很可能是“领域特征”——值对象发生了剧变,那么有关值对象的一切逻辑都要变化。而DDD采取的以下做法会造成领域内部的业务逻辑难以重构。


为保证一致性,一般采取在实体根对象中放入对值对象或其他状态实体修改的方法行为,外界不可以绕过根实体直接修改状态

    “一致性”究竟是什么呢,我认为“一致性”是事物相互作用的本质内在联系,也就是在一定场景下外界刺激在沿着一定路径传递而导致一系列对象的变化。所以“外界不可以绕过根实体直接修改状态”并不能反应这一本质,因为外界刺激并不全都是先作用在根对象上面的。在我看来,这种非本质的封装反而会造成耦合,尤其是采用“直接调用”的形式。应该说,“直接调用”是造成对象耦合最大根源,因为“直接调用”是在强调对象的上下级关系,这很生硬。如果我们换一种方式,用一种平等的心态去看待对象间作用关系,用“告诉我做什么”的方式而让对象间解耦。
    其实这里还有一个关于“边界”的问题,如果“外界不可以绕过根实体直接修改状态”,那么就会出现2个边界:Service和根对象。显然不应该出现2个边界,我认为边界只有一个,那就是service。在service(边界)内部,对象的关系应该尽量的平等,“对象间用消息来相互沟通”。我认为采取这种设计方式,既能保证对象间的一致性,又能保证领域对象的可扩展性。
    呵呵。。这点分歧可能有点大,欢迎大家讨论。
[该贴被flyzb于2010-11-18 22:14修改过]

2010年11月17日 22:48 "flyzb"的内容
呵呵,这是不是应了中国的一句老话:"分久必合,合久必分"。 ...

在思考语言范式时,我曾这样想过,面向对象,行为与属性绑得太紧,面向过程,行为与属性放得的太松。但这里不是仅仅选择“分”或“合”那么简单,“贫血模型”与“充血模型”实际上与“面向过程”与“面向类(对象)”的矛盾是相似的。

“贫血对象”是将“行为”与“属性”完全放开的一种表达,而“充血对象”则又矫枉过正,把“行为与属性”绑得太紧。

类是表达共性的概念,而对象则是充满个性,而且这些个性是依赖场景的,离开场景将失去意义。所以,在“充血模型”中,用类表达对象时,实际是将“个性”统统视为“共性”,在任何具体的场景中,对象的角色或职责都已经定义好了,这显然是不合适,因为一个对象可以多种角色参与不同的活动或场景(可能使得类继承体系非常庞大和复杂),而且在参与新的活动或者场景时,以“类”及“继承”的方式定义对象则更是力不从心。

而在“贫血模型”中,则将“共性”统统视为“个性”,这是抹掉“共性”的做法,与“充血模型”抹掉“个性”的做法刚好相反。前者是“白马非马”,后者则是“白马即马”。都没有协调好“共性”与“个性”的关系。

因此真正自然的“领域模型”应该是这样的,如果对象的某些行为在任何场景都是通用的,那么就放在领域中去,将其绑定,这是尊重“共性”的约束;如果对象的某些依赖于具体的场景,那么则在具体的场景中注入相应的行为,赋予对象相应的角色,这是尊重“个性”的自由。

那么“贫血模型”与“充血模型”,就没有用了吗?也许是。但这两个概念还有意义的:“贫血模型”与“充血模型”实际表达的是两个极端的情景。如果一个对象,没有任何共性的行为,其行为完全依赖于场景,则可用“贫血模型”表达;如果一对象,在所有的场景,都是以同样的角色身份参与,那么可用“充血模型”表达。

“贫血模型”与“充血模型”都可以实现领域建模,与“面向过程”与“面向对象”都可以描述一切画面一样。只是针对的问题不同时,有合适与不合适的差别。

所以,对象的行为该不该放入“领域模型”,我们要先分析一下这些行为是对象所固有的,还是依赖于场景的,如果是固有的,即是共性的,就放入领域模型(domain),如果不是则延迟在具体的场景(service)中注入,赋予其角色的个性(DCI)。

那么设计模式将如何运用呢?

设计模式可以在领域模型中使用(domain),也可以在具体业务场景(service)中使用。设计模式是在局部、微观层面的一种支持变化的机制,在具体业务场景中使用再合适不过了。将来可能会出现的现象是,在领域层(domain)各个模型中用的更多是“结构型”模式,而在业务层或服务层(service)的各个场景中用得更多的是“行为型”模式,两者都可以使用“创建型”模式。

此外,我认为DCI与MVC是互补(对称)而不取代的关系,这点与banq不同。有时间,我将描述自己如何从不同的思路得出这个结论,尽量浅显易懂,让更多的人理解,Jdon有时的讨论,让人不知所云,也许有话题较深入的原因,但也不能不自我反省。此外,我上面的分析,没有道出领域建模的真正意义和OO最初的构想(梦想),以后有时间一块补上。

期待关于领域建模的意义和OO的最初梦想!

2010年11月19日 17:49 "zjsong"的内容
期待关于领域建模的意义和OO的最初梦想! ...

呵呵,个人以为领域建模的意义就是实现OO的最初构想,而OO的最初构想自然要在OO的先驱Alan kay等人那里找了。有兴趣自己先找找吧。我不一定有时间整理、发帖子。

2010年11月18日 21:49 "flyzb"的内容
应该说,“直接调用”是造成对象耦合最大根源,因为“直接调用”是在强调对象的上下级关系,这很生硬。如果我们换一种方式,用一种平等的心态去看待对象间作用关系,用“告诉我做什么”的方式而让对象间解耦。
    其实这里还有一个关于“边界”的问题, ...

我前面已经讲了,使用设计模式和接口等设计可以解决"直接调用"的耦合关系,我个人认为"直接调用"是无法消除的,但是可以通过好的软件设计降低松耦合。

如果“外界不可以绕过根实体直接修改状态”,那么就会出现2个边界:Service和根对象,显然不应该出现两个根或边界,这是因为我们假设了Service也是聚合边界内一个元素,其实我认为根本不是。

DDD中Service应该是跨根实体的一个概念,也就是说:Service不属于根实体内部,恰恰属于根实体为首的聚合边界之外,联合几个根实体,实际上,它是注重功能发生的地方。

当然DDD的Service和SOA中的Service也是有区别的,主要是粒度粗细,但本质上我认为他们都是场景发生地,与DCI架构有竞争关系。

目前我是这样认为:DCI架构中Data实际就应该是DDD中实体或根实体,包括聚合对象群;而Context场景实际就是Service;交互行为则是属于对象的职责。通过DCI架构,将过去这些分裂的设计技术融合在一起,形成未来新的架构趋势。
[该贴被banq于2010-11-20 18:50修改过]

嗯...感谢banq。我正在尝试做一个大的企业框架,会面临很多复杂场景的挑战。我希望能够按照DDD的思路去构建富领域模型,从场景的角度通过事件来分配对象职责。从目前JF来讲,我迫切需要一个非常强的“事件处理器”,希望banq能在新的JF中考虑。
[该贴被flyzb于2010-11-21 23:32修改过]

2010年11月21日 23:31 "flyzb"的内容
从目前JF来讲,我迫切需要一个非常强的“事件处理器”,希望banq能在新的JF中考虑。 ...

多谢,关键不是很明白你对事件处理器这个定义是什么,和ESB消息有什么区别?

你可能在DCI中没有看到事务 安全等处理,这里有一篇文章可供你参考,利用现有EJB Spring等技术来提供事务安全给DCI的场景。DCI和服务Services (EJB)

刚刚出炉的,正好被我看到,看来我们讨论关心的也和老外是同步的。

[该贴被banq于2010-11-22 15:35修改过]

    参考InfoQ:面向对象编程——走错路了
    其实我说的“事件处理器”和EJB中的消息的本质是相同的,但层次不同。EJB的消息是宏观层面的,而我说的“事件处理器”是微观层面的,是领域层面的。我希望有一个更彻底的基于领域驱动的EDA架构能在JF中实现。在我看来,一切领域内的对象行为都是由“事件”来激发的,包括“创建、存储、获取”,比如聚合的创建实质上就是从聚合根到聚合内部对象的一系列事件来完成的。同时,这个“事件处理器”还支持基于不同场景的策略而导致不同响应。为了保证灵活性,我建议最好能用XMl配置形式而不用注解。另外,对于“直接调用”,也可以用“事件处理器”去指定“事件队列”的顺序来完成。当然,事件的异步或者同步也是可以设定的。
    对于DCI,我更倾向你在JF的做法,认为场景是无形的。说实话,对于目前DCI架构采取的形态,我感觉很形式化,在复杂场景中会变得很笨拙。
[该贴被flyzb于2010-11-23 12:49修改过]

2010年11月18日 23:30 "jdon007"的内容
类是表达共性的概念,而对象则是充满个性,而且这些个性是依赖场景的,离开场景将失去意义。所以,在“充血模型”中,用类表达对象时,实际是将“个性”统统视为“共性”,在任何具体的场景中,对象的角色或职责都已经定义好了,这显然是不合适,因为一个对象 ...

非常同意这个观点,在现在的设计开发中,那种背其而行的太多。你跟他们讲,他们拿出某个成功的安例来说服你。或者拿大家都在那样用的例子来争论。这就是目前我经常碰到的事,不知道是不是一种杯具呢?

DCI和MVC更多的是意义上的互补,但实际上却是冲突的。banq以前的文章也讲到,DCI把MVC的M和C已经打散再组装了,MVC模型已经崩溃了,所以想说互补的话,也只能说“有损互补”而已,而且损得不少。

若果使用事件的话,的确设计模式的需要程度会变弱(领域实体间的交互已经被事件截断),但依然有需要的地方,一个是场景内,一个是事件间(一个事件包含多个事件响应)。剥离设计模式的具体实现,从宏观上看,设计模式思想也可以在设计思想上表现出来的。

我们现在发现两个极端,也就是想追求设计师最想要的调轴。可调性可使系统更符合实际使用,更符合经济角度的需求。

刚刚接触DCI,对于行为和场景有些疑惑,如果行为是场景赋予的,会不会出现一个场景赋予一个数据不合适的行为呢?


我认为场景只是激活了对象的行为,而不是赋予,行为不会无中生有。比如,鸟(数据)可以在天空(场景)中飞(行为),而人不能在天空中飞,因为人没有这个能力。

TO joshuayan
人和鸟是两个角色,若果用户能够成为鸟,则可以进入天空场景,实行飞行。若果只能为人不能成为鸟,则进入不了天空。