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

    DDD的方向无疑是正确的,但是看了Eric的DDD以及banq的jivejdon源码后不禁又有了一些疑问。
    "贫血模型"无疑是不对的,领域建模中的领域对象应该是有行为的。在jivejdon中看到的领域对象其实并没有多少行为,而大部分的领域行为都让Dao或者Repository占去了。这不仅让我有一种似曾相似的感觉,原来在service的那些方法以另外一种面貌出现在了Repository层中。从Dao或者Repository本来的字面意思上可以看出,Repository层仅仅是领域模型与数据库的隔离层,显然不应该包含领域逻辑的。


public class ForumServiceImp implements ForumService{
......
public void createForum(EventModel em) {
Forum forum = (Forum)em.getModelIF();
logger.debug(" enter create Forum" );
try {
Long forumIDInt = sequenceDao.getNextId(Constants.FORUM);
forum.setForumId(forumIDInt);

//创建时间使用long字符串
long dateTime = System.currentTimeMillis();
forum.setCreationDate(Long.toString(dateTime));
forum.setModifiedDate(Long.toString(dateTime));
[u]forumDao.createForum(forum);[/u]
} catch (Exception e) {
logger.error(
" createForum error: " + e);

}
}
....
}

    从以上代码可以看出,对于创建这个动作,Service并没有把它交给Forum对象,而是直接给了forumDao.在我看来,这是违背了DDD的本质的,应该由forum对象承接这个动作,然后才由领域对象再传给forumDao。
    也许有人说,CRUD可以由领域对象直接承接动作,那更复杂的场景应该怎么办呢。呵呵,其实答案很简单,因为这个问题的答案已经在问题中说了,就是“场景”。场景是让领域对象具有行为的实际生存条件,所以DCI才是DDD中对于领域对象行为建模的有益补充。
    我建议抛弃掉DDD中关于工厂的提法,只有这样才会让我们回归真实业务,那只是一种“创建场景”,而不仅仅是那所谓的“工厂模式”,因为复杂创建场景中还可能包含复杂的流程或者策略对象的东西。


public abstract class MessageDaoSql implements MessageDao {
......
public void createMessage(ForumMessage forumMessage) throws Exception {
logger.debug("enter createTopicMessage for id:" + forumMessage.getMessageId());
// differnce with createRpleyMessage: parentMessageID,

String INSERT_MESSAGE =
"INSERT INTO jiveMessage(messageID, threadID, forumID, "
+
"userID, subject, body, modValue, rewardPoints, creationDate, modifiedDate) " + "VALUES(?,?,?,?,?,?,?,?,?,?)";
List queryParams = new ArrayList();
queryParams.add(forumMessage.getMessageId());
queryParams.add(forumMessage.getForumThread().getThreadId());
queryParams.add(forumMessage.getForum().getForumId());
queryParams.add(forumMessage.getAccount().getUserId());
MessageVO messageVO = forumMessage.getMessageVO();
queryParams.add(messageVO.getSubject());
queryParams.add(messageVO.getBody());
queryParams.add(new Integer(0));
queryParams.add(new Integer(messageVO.getRewardPoints()));

long now = System.currentTimeMillis();
String saveDateTime = ToolsUtil.dateToMillis(now);
String displayDateTime = constants.getDateTimeDisp(saveDateTime);
queryParams.add(saveDateTime);
[u]forumMessage.setCreationDate(displayDateTime);[/u]

queryParams.add(saveDateTime);
[u]forumMessage.setModifiedDate(displayDateTime);[/u]

try {
jdbcTempSource.getJdbcTemp().operate(queryParams, INSERT_MESSAGE);
} catch (Exception e) {
logger.error(e);
throw new Exception(
"messageId=" + forumMessage.getMessageId() + " happend " + e);
}
}
......
}

    以上“forumMessage.setCreationDate(displayDateTime);”部分应该属于领域业务逻辑,不应该出现在Dao中。
    另外,我也不太赞同DDD中关于聚合的做法,因为保证领域模型内部对象的一致性并不是只有通过层层封装才能做到。在我看来,这种层层封装并不是领域内部的业务场景的真实反应,反而造成领域内部逻辑的复杂和僵化。在jivejdon中,Dao、Factory、Builder、Kernel之间的命名关系一直让我困惑,至少感觉逻辑很沉重,与业务逻辑对应关系也不清爽。
    总结一下,DDD=Service+富领域的对象+基于事件的DCI+不含业务逻辑的仓储。
    呵呵,其实jivejdon有很多值得我学习的地方,希望大家共勉。
[该贴被flyzb于2010-12-11 22:04修改过]

2010年12月11日 21:59 "flyzb"的内容
对于创建这个动作,Service并没有把它交给Forum对象,而是直接给了forumDao.在我看来,这是违背了DDD的本质的,应该由forum对象承接这个动作,然后才由领域对象再传给forumDao ...

我觉得这里挺有道理的,但做起来很难,如果把所有的领域知识都给了领域模型,那么模型会不会很大?

2010年12月11日 21:59 "flyzb"的内容
对于创建这个动作,Service并没有把它交给Forum对象,而是直接给了forumDao ...

严格来说,这里创建应该给ForumFactory,这个你从ForumThread或ForumMessage的创建可以看出,而Forum的创建因为简单,就没有增加工厂这一环节。其实这一环节相当重要。

DDD也是建议使用工厂从仓储中创建实体对象。

但是我个人不认可有实体对象自己创建自己(自己都没诞生,怎么生自己呢,万物皆有母),在Spring一些系统中,经常委托实体对象的一个静态创建方法来创建自己,这合理解释就是在简单情况下省略工厂。

2010年12月11日 21:59 "flyzb"的内容
我建议抛弃掉DDD中关于工厂的提法,只有这样才会让我们回归真实业务,那只是一种“创建场景”,而不仅仅是那所谓的“工厂模式”,因为复杂创建场景中还可能包含复杂的流程或者策略对象的东西。 ...

关键是要分清楚实体对象自身的创建和实体扮演角色后的场景创建这两个事情,这是两个不同的东东。

DDD中工厂是有关实体对象自身和相关子对象,也就是聚合体创建,不是关于场景创建。

DDD可以说只是停留在静态的结构特征阶段(数据库属于静态时代原始人),还没有进入动态行为方面领域,这方面倒是很欠缺一个类似DDD的经典学说,可惜现在函数式语言和动态类型正在发展,估计还没有成熟,领域事件和DCI属于这个领域相对成熟的。

在JiveJdon中主要是以领域事件为主,因此没有破坏原来DDD的静态结构特征,但是很显然,这使得整个架构应当动态性质应用比较力不从心,除非你使用工作流或规则引擎将应用中动态性质分离,这也是过去成熟的做法。

但是未来这些都可能被DCI或函数式语言新技术全部打乱,进入一个以动态为主的设计新时代。

    准确来讲,领域对象创建自身也是不太好的。不过我还是建议在DDD中加入Context层,由Service负责把外界响应传给Context层,然后由Context层负责与领域对象和仓储层打交道。这也就是说由Context层负责创建领域对象,其中可以根据具体情况考虑选择用工厂模式、策略或者工作流之类的东西,对于一致性可以用对象间的事件响应来保证。
    目前,我感觉DDD中对工厂和仓储的定位不清,导致大量的业务逻辑散落在仓储层里。仓储只是一个领域模型获取和存储数据的接口,是不应该涉及业务逻辑的,比如上面我指出的在仓储中修改对象的问题。
    如果业务逻辑不放在仓储里,那应该放到哪里呢?显然,DDD中缺少了对场景层的定位,而工厂只是其中的一种简单的“创建场景”而已。所以我对于场景是动态还是静态的并不是特别关心,但关注的是如何把这一层的内容从仓储层剥离出来一样,其实这与DDD当初把业务逻辑从Service层剥离出来的初衷是一样的。这也就是说既然Service层是薄的,那么仓储层也应该是薄的,而只有Context层才应该是厚的。
[该贴被flyzb于2010-12-12 16:21修改过]

2010年12月12日 16:05 "flyzb"的内容
我感觉DDD中对工厂和仓储的定位不清,导致大量的业务逻辑散落在仓储层里。仓储只是一个领域模型获取和存储数据的接口,是不应该涉及业务逻辑的,比如上面我指出的在仓储中修改对象的问题。 ...

仓储只是对象还原创建或与数据库格式的转换,这是DDD定义,肯定不会在仓储中带有业务逻辑啊,关键在于你对业务逻辑的理解了。

在DDD中,如果不首先将实体创建出来是无法开展业务逻辑的,如果认为实体创建会涉及到业务逻辑,可能是把实体作为业务逻辑的输出结果,是业务逻辑的儿子,而不是业务逻辑的父亲,这可能受数据库思路的影响,数据库思路中,业务逻辑在Service中调度访问数据表,逻辑计算后使用对象来装载计算结果,这里实体对象是一种被业务逻辑强行插入的;而DDD讲究业务逻辑蕴含在实体中,是与实体一体的。

所以,业务逻辑中一些静态部分包括职责行为可以分解到实体及其聚合子对象中,一些动态部分可以在实体DATA与具体角色混合后,在Context中动态实现,比如一些活动与时间有关的功能。所有这一切的开始就是首先要有实体Data,而实体Data是由工厂创建的,实体的创建没有任何业务逻辑,那只是宇宙混沌之初。

jivejdon工厂中基本都是和业务逻辑无关的,其实和Hibernate做的工作差不多,就是在对象和关系数据表中做映射,工厂中这些代码其实可以用Hibernate完全替代,也说明其不含业务逻辑。可能jivejdon个别地方没做到位。

关键把实体DATA和场景Context混淆在一起,这两个不是同样的,场景当然和Service靠近一些,实体Data和仓储靠近一些,但不能因为近朱者就赤,他们都有很明晰的区别。

但是厚的Context也不是很好,其实Context只是动态的一瞬间汇集,很多职责行为还是分散在实体及其聚合子对象的行为方法中,好的设计应该是:没有厚的地方,至少没有厚代码的地方,Context是很重要,表面上应该很厚,但是我们跑进去一看,又细分为各种实体职责行为,化为无。

掌握这个原则,无论你用Service还是MVC的控制器或DCI,都说明你的系统粒度非常细,最大化的松耦合,有良好的扩展性和维护性,软件质量过关了。

[该贴被banq于2010-12-12 18:40修改过]

2010年12月12日 18:01 "banq"的内容
好的设计应该是:没有厚的地方,至少没有厚代码的地方,Context是很重要,表面上应该很厚,但是我们跑进去一看,又细分为各种实体职责行为,化为无。 ...

非常认同,其实面向对象最基本,也最重要的就6个字:高内聚,松耦合,无论是DDD也好,设计原则也好,设计模式也好,其实都是为了高内聚,松耦合来服务的。

    呵呵。。没错,我也一直在反思“DDD中的业务逻辑究竟是什么”,这是一个非常关键的问题。我想有以下几个方面,大家可以批评指正:
  1.领域对象的创建过程(这是我不建议在DDD中把“工厂”放在仓储中的原因)。
  2.更改领域对象的状态(这是上面我指出的Dao中涉及业务逻辑的地方)。
  3.各种业务场景下领域对象的一致性保证(DDD中是通过聚合和工厂来完成的,这是我感觉不太顺畅的地方。因为在业务不断变化发展的复杂场景中,这种聚合会变得脆弱,甚至发现原来的业务建模根本就是错的,所以根据我的项目经验让对象间变得松散一些,用事件会让逻辑更加灵活和强壮)。
  4.各种对查询逻辑的过滤和转换。
    另外,“高内聚,低耦合”并不是说起来那么简单的。在一个项目从小到大,从简单到复杂的变化过程中,我们往往遇到的难题是原来逻辑太“高内聚”了。这也是不同系统集成时面临的问题,旧系统的逻辑都内聚性太高了。所以写“高内聚”的代码很容易,但是写“松耦合”的代码很难。因为这首先不是技术上的原因,而是业务认识的原因。而设计的灵活性相对于业务永远是最差的,这也是因为没有一个万能的设计模型可以让我们适应所有的业务变化问题。所以DDD中的“聚合”让我感觉到别扭,当然这只是针对复杂业务场景而言的。其实我不认为那种“封装式的聚合”才是真正的聚合,而“事件与响应”才是业务最本质的聚合。这也是我为什么一种在强调“事件”重要性的原因。
[该贴被flyzb于2010-12-12 23:30修改过]

    在简单场景中,对象的创建只涉及自身,但在复杂场景中一个核心对象的创建往往是一个过程,会涉及到一大堆的对象的创建和查询过程。对象的创建是一个领域模型实例化的过程,这不仅仅是业务逻辑,而且是非常重要的一种业务场景。这就好像工作流需要初始化一样。
[该贴被flyzb于2010-12-13 08:55修改过]

2010年12月13日 08:43 "flyzb"的内容
但在复杂场景中一个核心对象的创建往往是一个过程,会涉及到一大堆的对象的创建和查询过程。对象的创建是领域模型一个实例化的过程,这不仅仅是业务逻辑,而且是非常重要的一种业务场景。 ...

这方面我们理解是有差异的,打个比喻:如果软件系统没有技术上启动,假设一直在运行,那么我们就无需对象的创建,因为它们一直活着。

当然,也可能对“对象创建涉及业务”中的对象是在场景下的一些临时对象,这类似与将SQL语句输出包装到一个对象中,这个对象创建就涉及业务。

我们这里谈的对象创建无需业务,应该说是实体根对象创建无需业务,因为根母还没有出来呢,就象宇宙之初一样,万物皆有母,业务逻辑也有母亲啊。

所以,这里区别是:DDD中是将业务逻辑置于聚合根等母实体之中;而传统数据库概念是将业务逻辑静态化扁平化到数据库中保存,然后从数据库的SQL中导出业务逻辑,业务逻辑再创建对象。在这里,我们把数据库当成了业务逻辑的容器,而在DDD中,我认为应该是将实体根对象作为业务逻辑的容器。

前者情况下,数据库是在磁盘上持久存在的,因此,我们在软件启动时,无需初始化这个数据库容器,这就养成了我们经常在场景中直接写业务逻辑,然后再创建一些具体对象。而在DDD中,软件启动时,需要初始化业务逻辑的容器根实体,由根实体产生业务逻辑,进而再产生一些具体和场景有关的对象。为了做到这点,就要求我们从需求分析就用对象封装技术来封装业务逻辑,这其实是DDD的核心。

最后总结一下:根实体对象的创建准确说属于初始化,而一些具体对象创建属于业务逻辑一部分。

[该贴被banq于2010-12-13 09:08修改过]

2010年12月11日 21:59 "flyzb"的内容
"贫血模型"无疑是不对的,领域建模中的领域对象应该是有行为的。在JiveJdon中看到的领域对象其实并没有多少行为,而大部分的领域行为都让Dao或者Repository占去了 ...

理解DDD一个首先是找核心领域模型,JiveJdon的核心领域模型是ForumMessage或者ForumThread,而Forum不是,所以,你从Forum上面是看不到DDD的影子的,只是简单的分层。

为什么一般人认为Forum是论坛系统的核心领域模型呢?其实论坛的核心不是论坛本身,而是可以发贴,如果一个不懂什么是论坛,你要解释:论坛就是大家可以发贴。

所以,这里可以看到“论坛”是“帖子”的容器,或者说外在部分,帖子才是论坛的核心。这就是找核心的方法,划定边界,从外部和内部两个角度看,将屁股坐到边界外部或边界内部来看,防止不识庐山真面貌。

这其实是分析的方法,如果变成你的潜意识和习惯,那么就不会被名词表面现象迷惑,也不会产生沟通问题。看别人讨论或发问,首先搞清楚他们是从事物外部来讨论这个事物(我取名为形或象);还是从事物内部来分析量化抓住本质来讨论它。如果别人只是从形象来讨论,你去讨论事物内部本质,就产生不同角度,相互理解即可。

罗嗦半天,好像又在谈哲学,打住了。

我一直相信“软件和哲学思考都是同一样东西——世界”。不过为了避免放大问题的争论,还是只从软件的角度出发吧。

>>1.领域对象的创建过程(这是我不建议在DDD中把“工厂”放在仓储中的原因)。
<<工厂是“材料”-》“成品”的场所,对于软件就是“数据”-》“实体”的制造者,在领域中理解为实实在在的“工厂”,在DDD架构层上,则理解为制造者。可以说领域和外界有个中间工厂,外界所有东西都得经过制作成实体,才能进入领域。当然我个人为了区别一些与领域无关的,称领域无关的为制造者(一样意思的,只是区分下而已,注意,这不是建造者)。而在领域中,实体间的聚合关系,用的是建造者——组装过程嘛。若果是dao放子repository的话,那么工厂就跟随dao进入repository了,这很正常的。

论坛的例子:会员(已登录者)发表帖子,则需要把“内容”和“基本资料”构造成“帖子”,再在领域服务中运作,帖子组装为论坛,则用建造者去组合。注:在领域当中,存在的都是“实体”,不可能存在“材料”。服务则是处理实体交互和关系等。

>>2.更改领域对象的状态(这是上面我指出的Dao中涉及业务逻辑的地方)。
<<对于jivejdon的dao,我也是存在一点异议的——dao不应该出现在service中。领域服务应该为领域中的实体服务的,不应该过多考虑其他东西,如io,数据库等。数据库,是保存实体状态的,他应该只知道实体本身,不应该涉及到交互问题(若果涉及到交互,也就有面向数据库思维之嫌),也就是说,持久化模块应该对“需要跟踪的实体”(不同一般实体)进行监察,当“需要跟踪的实体”发生变化时则响应持久化(可设置为“响应性持久”,也可以“周期性持久”)。

在jivejdon中,个人认为jdon应该有监察@model实体的功能和提供与数据库连接的接口,鉴于用的还是关系型数据库,所以还得提供转换规则接口。总的来说就是把dao从repository去除,作为jdon架构的一个可选部分——到达没有数据库都能运行的境地。

>>3.各种业务场景下领域对象的一致性保证(DDD中是通过聚合和工厂来完成的,这是我感觉不太顺畅的地方。因为在业务不断变化发展的复杂场景中,这种聚合会变得脆弱,甚至发现原来的业务建模根本就是错的,所以根据我的项目经验让对象间变得松散一些,用事件会让逻辑更加灵活和强壮)。
<<是领域实体的一致性吧,因为每个实体都有唯一的id,所以,通过唯一的id拿到的实体就能确保一致性(实体在仓储中,仓储包含cache。而缓存机制中,应该加入,当model管理者发现需要的model不存在时,应该向第三方(数据库)获取(即cache向持久化模块请求,当然这是框架的工作了)。至于聚合变得脆弱一说,可以说是对象关系的理解错误造成,对象交互和对象关系是两个方面。错误形成重构这是正常的,若果连一些基本关系都扑捉错误的话,那么这个领域是相当不稳定的,也就是这个领域没人能描述清楚,或者有五花八门的版本,那么这些就是,一开始就需要获取到的信息。

在论坛中,若果论坛不是论坛,帖子不是帖子,jivejdon能说是论坛吗?领域是相对稳定的,不稳定就不是领域了。

>>4.各种对查询逻辑的过滤和转换。
<<这是什么意思?这是没有观点的标题?

>>另外,“高内聚,低耦合”并不是说起来那么简单的。······
<<正所谓“物极必反”,加一个“太”字,我也很难说出什么反面的观点了。高内聚,低耦合是体现模块化的思想,“数据库就在数据库那边搞,别过来,发帖的发帖,别来这边看电影”。
>>而“事件与响应”才是业务最本质的聚合。
<<业务聚合?事件哪里体现出聚合了?体现在松耦合吧···也就是解决直接调用问题吧。

[该贴被SpeedVan于2010-12-13 13:34修改过]

    呵呵。。感谢banq,让我进一步了解DDD,不过我还是要提出自己的疑问。
    从“各层均衡”的角度看,我感觉仓储层的代码太厚了,这里面主要是“工厂”的原因。
    对于“工厂”,我不觉得一次就把领域模型进行全加载有必要。我更倾向rest架构,所以希望是一种细粒度的按需加载。这也是为什么我总感觉jivejdon中"工厂"部分代码的沉重。
    可能有道友总不太理解我说的业务变化是什么,那么我们还是以“论坛”为例吧。上面banq说过,在jivejdon中“论坛是一个容器和边界,其核心功能是发帖和回帖”。从目前看jivejdon还基本没有脱离“论坛”的业务范畴。可是从jivejdon的宗旨看,jivejdon是要“打造一个高质量的道友们交流技术的真正天地”,那就不仅仅是一个“论坛”了,其核心功能就不仅仅是“发帖和回帖”了,而是应该变成一个SNSJdon。这种业务发展和变迁非常合理。仔细看看,业务容器变了,从“论坛”变成了“技术交流SNS”,变化不可谓不大。那么在这剧烈的业务变更下,目前的jivejdon的领域模型能不变吗?这根本不可能,越是富领域的模型变化越大。当然也有不变的,就是发帖和回帖的基本功能不变,而“论坛”的概念可能都没了,像“ShortMessage”之类会“单飞”形成一个独立的领域模型。大家可以推理和分析一下,变动的逻辑还有很多。
    从上面我举得例子可以分析出,在这种业务变更下,“工厂”会变得多么脆弱。随着讨论的深入,我愈加感觉到目前DDD过于强调静态结构而让整个领域模型过于“刚性”了,也就是说内聚太强了,对于业务变化适应度差。我们要明白一点对象会因业务而内聚,也会因业务变化而分离。比如说在上面的业务变化场景里,“发帖和回帖”相对不变,但会和新的领域对象融合起来。
[该贴被flyzb于2010-12-14 08:48修改过]

TO flyzb
你说的没错,也正是DDD所局限的地方,以前的帖子就说到,DDD适合相对稳定的领域,过度地改变领域,会使领域形如虚构,这样的情况下用DDD的话,只会无所适从。对于这种状况,我们就考虑到DCI了,每一个场景就像细化后的且相对稳定的领域,这样扩展起业务就有很好的支持了。相比REST的面向资源,DCI的面向业务(角色交互)更为直观。
[该贴被SpeedVan于2010-12-14 09:44修改过]

2010年12月13日 22:57 "flyzb"的内容
在这种业务变更下,“工厂”会变得多么脆弱。随着讨论的深入,我愈加感觉到目前DDD过于强调静态结构而让整个领域模型过于“刚性”了 ...

DDD只是缩短需求分析和代码实现之间距离,或者将两者直接连接起来;如果需求不稳定,经常变换,只要我们更换需求分析,那么代码就能够立即更换过来。DDD往往是和敏捷工程结合的。

所以,DDD下一步就是DSL,声明式编程,把仓储 工厂这些玩意都塞到DSL语言内部中,就不需要象JiveJdon将这么多低级工厂代码暴露给程序员了。

我再强调一遍,仓储中这些工厂是可以用ORM框架如Hibernate或JPA实现的,只不过JiveJdon没有选择,凡是dao包下面代码都可以用框架掩盖掉,使用这些框架创建的领域实体对象就不会那么脆弱了,你只要告诉它数据表在哪里,它就用它万能和强劲的工厂给创建任何领域对象,JiveJdon的工厂是只针对当前实体,没有做得那么通用,所以,当然看上去很脆弱,关键这些不是JiveJdon这个软件的主要任务。

如何应付需求变动导致软件变化是软件行业一个永久的课题,DDD和DSL做了一个探索,如果能够永久解决这个问题,软件就不需要发展。

需求是变化的,但不是将灵魂变了,需求变化有一个相对边界,象楼主上贴提出将论坛变成一个SNS社区,那么我认为这是魂变了,那么我们就要从SNS社区这个更高层重新开始分割,看JiveJdon论坛在SNS社区中处于一个什么子系统,这也是切分分割的一个方法,而不是要将JiveJdon拓展到SNS社区,这就强人所难了。楼主玩笑开大了,哈哈。



[该贴被banq于2010-12-14 10:13修改过]