`
taowen
  • 浏览: 190614 次
  • 性别: 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  
搬个板凳先

相关推荐

    论文研究 - 基于Web of Science的结直肠癌患者护理导航模型的文献计量分析

    本研究旨在确定在中国大肠癌治疗中应用的患者导航模型的现状和困境。 方法:采用文献计量法分析从Web of Science数据库检索的有关患者导航模型在结直肠癌治疗中的应用的已发表文章。 统计数据是使用书目项目共现...

    论文研究 - ADDIE模型在中学生德育模块哲学探究方法发展中的应用

    因此,解决这一困境的方法是为教师提供一个有效的模块,以发展学生的道德推理和批判性思维。 这项研究的目的是:1)描述ADDIE模型在道德教育(PIME)模块中发展哲学探究方法的用途,以及2)识别模块内容的有效性。 ...

    社会困境中博弈决策的理论模型与神经机制 (2014年)

    简要介绍社会困境的涵义,详细阐述博弈决策的理性选择理论、认知转换理论和生物进化理论,并且从认知控制、社会认知...未来研究应力求突破先前理论模型的不足,深入考察个体合作行为的神经机制,并向实际应用领域发展.

    论文研究-图像传感器数据插值算法的改进.pdf

    随着对它研究的深入,这个问题已经作为一种模型被应用到现实社会的很多领域:经济学、社会学、商务谈判。这就提出了一个问题:如何有效地求解囚犯困境问题。利用遗传算法,对囚犯困境问题进行了求解。并且根据所提的...

    兼而有之和管理-损害服务的探索/开发领域:以租车为例-研究论文

    困境的勘探/开发要求并强调相关的需求,首先是从获得的竞争优势中大量使用投入和专业知识,其次,公司需要在新领域保持合乎逻辑和探索性的创造力。 根据与实施租车公司相关的组织创新的分析,似乎:-组织学习是按...

    中西企业经营中的管理合作困境

    同样,外商投资企业在外贸领域和经济中的作用是宏观经济舞台上的亮点之一。 然而,沟通问题困扰着中西业务,事实上它们困扰着大多数跨文化合资企业。 在中国的案例中,根源在于商业情境中西方普世价值观与中国特殊...

    没有安可的安可? 基于网络的审查衡量的道德问题-研究论文

    一对计算机科学家最近开发了一种巧妙的方法来衡量世界范围内的互联网过滤和审查,包括... 我们希望我们提出的问题对在自己的工作中面临类似困境的研究人员以及技术学科和法律和哲学等其他领域的研究伦理学学生有用。

    病毒式营销:通过消费者网络识别可能的采用者-研究论文

    2)从消费者网络构建的属性可以改进通过采用可能性对目标客户进行排名的模型,以及 3)观察网络允许公司瞄准那些可能会陷入困境的新客户,因为他们不会仅仅基于公司用于营销的传统属性集。 我们最后讨论了该领域...

    从软件工程的角度对面向代理的编程的概述

    软件工程领域中编程语言研究的经验和实践可以为我们摆脱困境提供一些启发。 本文旨在从软件工程的角度对AOP进行调查,包括AOP的研究历史以及有关面向代理的编程概念和模型,语言,CASE工具和运行方式的最新研究。 ...

    Thinking in Java 中文第四版+习题答案

    8.4.5 再论枚举器 8.5 排序 8.6 通用集合库 8.7 新集合 8.7.1 使用 8.7.2 使用 8.7.3 使用 8.7.4 使用 8.7.5 决定实施方案 8.7.6 未支持的操作 8.7.7 排序和搜索 8.7.8 实用工具 8.8 总结 8.9 练习 第9章 违例差错...

    Think in Java(中文版)chm格式

    8.4.5 再论枚举器 8.5 排序 8.6 通用集合库 8.7 新集合 8.7.1 使用Collections 8.7.2 使用Lists 8.7.3 使用Sets 8.7.4 使用Maps 8.7.5 决定实施方案 8.7.6 未支持的操作 8.7.7 排序和搜索 8.7.8 实用工具...

    JAVA_Thinking in Java

    8.4.5 再论枚举器 8.5 排序 8.6 通用集合库 8.7 新集合 8.7.1 使用Collections 8.7.2 使用Lists 8.7.3 使用Sets 8.7.4 使用Maps 8.7.5 决定实施方案 8.7.6 未支持的操作 8.7.7 排序和搜索 8.7.8 实用工具 8.8 总结 ...

    Java初学者入门教学

    8.4.5 再论枚举器 8.5 排序 8.6 通用集合库 8.7 新集合 8.7.1 使用Collections 8.7.2 使用Lists 8.7.3 使用Sets 8.7.4 使用Maps 8.7.5 决定实施方案 8.7.6 未支持的操作 8.7.7 排序和搜索 8.7.8 实用工具 8.8 总结 ...

    ThinkInJava

    8.4.5 再论枚举器 8.5 排序 8.6 通用集合库 8.7 新集合 8.7.1 使用Collections 8.7.2 使用Lists 8.7.3 使用Sets 8.7.4 使用Maps 8.7.5 决定实施方案 8.7.6 未支持的操作 8.7.7 排序和搜索 8.7.8 实用工具 8.8 总结 ...

    java 编程入门思考

    8.4.5 再论枚举器 8.5 排序 8.6 通用集合库 8.7 新集合 8.7.1 使用Collections 8.7.2 使用Lists 8.7.3 使用Sets 8.7.4 使用Maps 8.7.5 决定实施方案 8.7.6 未支持的操作 8.7.7 排序和搜索 8.7.8 实用工具 8.8 总结 ...

    thinkinjava

    8.4.5 再论枚举器 8.5 排序 8.6 通用集合库 8.7 新集合 8.7.1 使用Collections 8.7.2 使用Lists 8.7.3 使用Sets 8.7.4 使用Maps 8.7.5 决定实施方案 8.7.6 未支持的操作 8.7.7 排序和搜索 8.7.8 实用工具 8.8 总结 ...

    Thinking in Java简体中文(全)

    8.4.5 再论枚举器 8.5 排序 8.6 通用集合库 8.7 新集合 8.7.1 使用Collections 8.7.2 使用Lists 8.7.3 使用Sets 8.7.4 使用Maps 8.7.5 决定实施方案 8.7.6 未支持的操作 8.7.7 排序和搜索 8.7.8 实用工具 8.8 总结 ...

    java联想(中文)

    8.4.5 再论枚举器 8.5 排序 8.6 通用集合库 8.7 新集合 8.7.1 使用Collections 8.7.2 使用Lists 8.7.3 使用Sets 8.7.4 使用Maps 8.7.5 决定实施方案 8.7.6 未支持的操作 8.7.7 排序和搜索 8.7.8 实用工具 8.8 总结 ...

    Thinking in Java(中文版 由yyc,spirit整理).chm

    8.4.5 再论枚举器 8.5 排序 8.6 通用集合库 8.7 新集合 8.7.1 使用Collections 8.7.2 使用Lists 8.7.3 使用Sets 8.7.4 使用Maps 8.7.5 决定实施方案 8.7.6 未支持的操作 8.7.7 排序和搜索 8.7.8 实用工具 8.8 总结 ...

    JAVA_Thinking in Java(中文版 由yyc,spirit整理).chm

    8.4.5 再论枚举器 8.5 排序 8.6 通用集合库 8.7 新集合 8.7.1 使用Collections 8.7.2 使用Lists 8.7.3 使用Sets 8.7.4 使用Maps 8.7.5 决定实施方案 8.7.6 未支持的操作 8.7.7 排序和搜索 8.7.8 实用工具 8.8 总结 ...

Global site tag (gtag.js) - Google Analytics