以JiveJdon案例说明对象职责和SOLID原则应用

最近我和oojdon讨论给帖子加上浏览阅读次数这个功能,起初我们并没有从职责角度来考虑阅读次数这个功能,就简单地在Service中获得Thread方法时,添加一些代码,用来统计次数。

因为我们这时重点是如何用Domain Events来实现阅读次数持久化问题,也就是说,阅读次数并不是每次阅读就保存持久化一次,而是增加内存中计数,持久化保存是每隔60分钟保存,阅读次数这个数据并不是一种状态,不会对论坛功能产生重大影响,因此,我们没必要紧张兮兮每次保存。

从这里我们可以看出,持久化问题还是在OO设计中占据主要精力,就忽视了其他更重要的考虑,当然,目前考虑是是否需要持久?比以前考虑如何持久这个战术问题已经进步很多。

忽视的重要问题是:我们考虑持久,实际过度关注了对象中数据和属性,忽视了对象和单纯数据主要区别是对象有行为,行为职责才是对象根本点,和首要考虑的。

那么造成什么问题呢?就造成我们将阅读次数计数这个功能放到了Service中,表面好像是阅读次数数据和次数计数行为分离了,类似桥模式,实际这是一种面向过程的非OO设计,这种情况大量存在Spring框架的设计中,很多人不知觉罢了。

DDD告诉我们Service中是放一些不属于领域对象的功能行为,所以,我们从一开始就没有抓住重点:阅读次数计数这个功能是否是领域对象ForumThread的职责?

如果我们一开始分析需求时,从面向对象分析角度来分析,根据“对象职责模式”,抓住对象行为这个根本点,那么我们的重点就是在考虑:阅读次数计数这个功能是否是领域对象ForumThread的职责?

答案是肯定的,既然是领域模型的职责,那么就要成为其方法,所以,阅读次数计数应该是ForumThread的行为,这个功能应该在ForumThread中实现。

考虑对象的职责,就要用命令 触发 事件 状态 监视 等行为模式的思路来考虑,在Service中getForumThread方法中,需要调用这个ForumThread中的计数方法,调用者实际是一个客户端或者说命令源泉,是命令command的触发点,有了这个认识,我们就可以再进行考虑:这个命令触发点放在这里是否合适?阅读次数应该是和界面有关,放在Service中,如果是其他内部功能调用getForumThread方法也会触发计数,显然不妥,所以,这个触发点应该放在表现层界面层,至于是用AJAX和Action来实现,属于战术问题了。

分析到这里,我们看到:对象职责和DDD结合是非常重要的,如果不抓住对象职责模式,我们就可能走到面向过程编程;相反,如果一开始考虑持久问题,无论是否要持久以及如何持久问题,因为持久的只能是数据,所以,就会引导我们走向面向数据库面向过程老路,千里之行,始于足下,第一步决定方向,很重要。

当我们完成职责发现和分配以后,这时SOLID原则就跳出来对我们进行指导了,首先是单一职责问题,ForumThread中已经有addNewMessage等职责,现在我们加入阅读次数计数职责viewCountAction方法,很显然,这两种职责不属于同一种,不满足单一职责,如果我们将这两个方法抽象重构到接口中,就成为:


ForumThreadIF{
void addNewMessage();
void viewCountAction();
}

这个接口的命名你都觉得困难,我这里暂时使用ForumThreadIF,这是不准确的,因为这两个方法无法用统一行为来命名,实则因为它们就不是同类别,道不同不相与谋,按照SOLID原则,应该划分到两个接口中。

那么ForumThread这个领域模型就实现两个接口,当然,如果考虑除以上两个职责,ForumThread还有更多职责,那么就要有更多接口,最后,ForumThread就成为实现多个接口的肥胖的大类了,这类似SOLID原则一文施乐公司的那个大JoB类,绕了半天,肥胖大类又出来了。

其实在这里,我们陷入一个误区,我们可能把屁股当成脑袋了,ForumThread这个肥胖类中的这么多职责是我们运行JiveJdon软件时的结果,也就是说,在JiveJdon运行时,ForumThread必须是一个肥胖“对象”,拥有有这么多职责,但是这并不意味着,我们在设计ForumThread就一定要搞一个肥胖“类”,我们可以将ForumThread切分成多个类包括接口(用边界封装它们),在JiveJdon运行时,再组合成ForumThread肥胖对象。

这实际是 DCI架构是什么?中一部分含义,AOP/Mixin、面向函数语言以及动态类型语言都可以实现“在运行时刻,根据角色场景,动态组合生成ForumThread肥胖对象。”

我想:这个美好的设计架构就是下一步我们需要的。编码和运行阶段分离将是目前我们一直孜孜不倦追求的。

[该贴被banq于2010-02-27 10:02修改过]

2010年02月27日 09:53 "banq"的内容
其实在这里,我们陷入一个误区,我们可能把屁股当成脑袋了,ForumThread这个肥胖类中的这么多职责是我们运行JiveJdon软件时的结果,也就是说,在JiveJdon运行时,ForumThread必须是一个肥胖“对象”,拥有有这么多职责,但是这并不意味着,我们在设计ForumThread就一定要搞一个肥胖“类”,我们可以将ForumThread切分成多个类包括接口(用边界封装它们),在JiveJdon运行时,再组合成ForumThread肥胖对象。

我现在又结合CQRS想了一下,可以这样理解:
ForumThread其实和这个阅读次数没有任何关系,阅读次数的出现只是浏览帖子的人作用在ForumThread对象上一个事件的结果,如果这样这样理解,ForumThread领域类就和viewThreadAction剥离了,viewThreadActon其实是CQRS中CommandHandler的责任,事件的结果由EvenHandler记录再由Factory组装,不知道banq意见?
[该贴被oojdon于2010-02-27 10:59修改过]


2010年02月27日 10:54 "oojdon"的内容
ForumThread其实和这个阅读次数没有任何关系,阅读次数的出现只是浏览帖子的人作用在ForumThread对象上一个事件的结果,如果这样这样理解,ForumThread领域类就和viewThreadAction剥离了,viewThreadActon其实是CQRS中CommandHandler的责任

这个方向我也考虑过,不过,我先按照对象职责这个方向考虑,按照对象职责模式检验标准:确定一个职责是否是对象的,可以从对象是否知道这个角度考虑。

ForumThread是否知道自己的阅读次数呢?是应该知道的,这个职责属于事物外职责,和addNewMessage这样在ForumThread内部自己增加跟帖这种内部职责是不同。

如果按照CQRS查询命令分离,阅读是查询,但是阅读的次数计数则不应该属于查询,查询概念是只读,对查询进行监督跟踪的应该属于命令了。

嗯,越讨论越深入了,我也是刚发现这点的。呵呵

以前我们设计对象都是首先想想对象应该有什么属性,然后再根据属性推断对象应该有什么行为,其实这样方向错了。设计对象应该从职责和行为入手,有什么样的职责会需要哪些属性,那么这些属性就属于对象,如果职责不需要的属性就不应该属于对象本身。

就本例子来说,我觉得帖子应该要要知道自己被阅读了,从外面来看,是用户浏览帖子,好像这个行为应该属于service,但是用户浏览帖子会产生一个效果,这个效果就是帖子本身要知道自己被浏览了多少次,因此将浏览次数增加的职责应该属于ForumThread,而如果这个职责属于ForumThread,那么与这个职责有关的属性,我们就会想到阅读次数的属性了。
[该贴被xmuzyu于2010-02-27 20:35修改过]

2010年02月27日 20:35 "xmuzyu"的内容
以前我们设计对象都是首先想想对象应该有什么属性

这个已经成为分析习惯,成为无意识,包括我自己,看来OO思维培养不是那么容易。

这里有一个现象:我们通常使用名词方式来从需求中寻找对象,名词这玩意实际是一个数据,容易导向我们以名词为中心的分析,DDD让我们使用特征、描述等来分辨实体和值对象,所谓唯一标识也是一种数据。

当我们找出对象后,就不应该觉得建模结束,以为将一些特征数据塞入对象中,然后使用setter/getter方法就可以了,其实这就是MF批判的贫血模型。

这时,应该开始发现寻找对象的职责,往往我们这一步没有去做,这样导致把行为方法都塞入Service中,而DDD中虽然声明将不属于实体的行为放入Service,但是没有指出什么是属于实体的行为,当然也偶尔提到维护边界一致性等行为,其实这些只是对象职责中的一个小子集,没有完整从对象职责高度来讲解,当然DBC也提到。

总之,在进行领域建模面向对象分析设计时,不能有片面和倾向,要全面来分析,个人觉得四色原型相对还是比较合理,帮助你理顺需求中角色 活动 特征,什么人什么时候对什么物体做了什么事情,比较全面,接下来结合DDD和对象设计:角色、责任和协作"(Object Design: Roles, Responsibilities, and Collaborations)一书来指导,比较具有实战意义,是一种目前来说比较好的敏捷方法。
[该贴被banq于2010-02-28 10:57修改过]

banq大哥,viewCountAction如果属于ForumThread职责的话,那是不是应该也有messageCountAction的职责呢

2010年02月28日 21:40 "spawnyy"的内容
viewCountAction如果属于ForumThread职责的话,那是不是应该也有messageCountAction的职责呢

是的,目前ForumThread有messageCount这个方法,是ForumThread中的根贴(主题贴)的回复贴次数计数。

banq大哥,上文提到过是职责决定应具有哪些属性,ForumThread应该有viewCountAction和messageCount方法,那是不是就应该有viewCount和messageCount两个属性,可建模的时候DDD不是应该将与模型不必要的属性归于值对象,那么职责怎么解决属性呢

你这个问题非常好,其实数据 属性有两种类型:一种是经过职责行为后的结果数据;一种是没有行为作用,天生就有的固有属性。

打个不好比喻:职责结果的数据就像拉的屎,我们很多时候建模,往往把屎当成天生固有属性,忽视其中有一个经过业务行为处理的过程,这实际上可能也源于面向数据表的编程思路,因为数据表不但保存原始数据,还保存处理后的结果数据。

对于天生固有属性,按照DDD的值对象来定义,而和职责有关的数据结果,则更加高凝聚原则,应该和职责行为在一起,这种数据是可变的,可以认为是领域对象的状态,属于CQRS中命令导致状态变化的范畴;而值对象是不可变的,要么全部替换,换一个新的值对象。

当然,如果有的职责导致一批结果,这些结果也是可以做成值对象封装,以表明这些结果整体性,是一个命令导致的全部状态变化。

以上只是个人经验,DDD和对象职责结合言论属于本人思考,只能作为参考。

banq大哥,大致是明白了,与职责相关的属性应该放在领域模型中,可是目前messageCount属性与职责都在ForumThreadState值对象中


public class ForumThreadState extends ModelState {
private volatile int messageCount = 0;
/**
* @return Returns the messageCount.
*/

public int getMessageCount() {
return messageCount;
}

/**
* @param messageCount
* The messageCount to set.
*/

public void setMessageCount(int messageCount) {
if (messageCount >= 1)
this.messageCount = messageCount - 1;
else
this.messageCount = messageCount;
}

public void addMessageCount() {
this.messageCount = messageCount + 1;
}
……
}

2010年03月01日 19:01 "spawnyy"的内容
可是目前messageCount属性与职责都在ForumThreadState值对象中

这个ForumThreadState原来我以为是一个值对象,实际不应该是值对象,值对象的值是不可变的,也就是说,所有的属性数据可以使用final修饰,如果不能就不能算值对象。

现在是一个职责对应一个数据结果,如果一个职责行为产生N多个数据结果,就应该把这些数据结果打包在值对象中。

现在是一个职责对应一个数据结果,如果一个职责行为产生N多个数据结果,就应该把这些数据结果打包在值对象中。banq大哥,这句话怎么理解呢,能详细解释一下么,因为只知道一创建即为最终状态,如果有状态变化需要重新创建。而文章提到的一个职责行为产生N个数据结构,怎么看呢

2010年03月01日 19:25 "banq"的内容
这个ForumThreadState原来我以为是一个值对象,实际不应该是值对象,值对象的值是不可变的,也就是说,所有的属性数据可以使用final修饰,如果不能就不能算值对象。

语义上ForumThreadState是值对象,语法上Java除基本类型外都是引用(个人认为是Java语言对值和引用区分的设计缺陷)。值对象重在值,比如:我的一块钱和你的一块钱是相等的;引用对象重在同一性,比如:3岁的我和20岁的我是同一个人。值对象的值不是不可变,而是属性变了就不是原来的那个值对象;而引用对象的属性改变后只要对象标识(引用或id属性)不变就仍然是同一对象。
[该贴被weidagang2046于2010-03-01 21:04修改过]

楼上说得很有道理,另外,值对象的不可变性是Immutable意思,我们在设计时通过不可变和可变性这个标准进行对象区分,这样,在多线程环境中就会减少锁的应用,将锁的粒度更细。

"一个职责行为产生N多个数据结果"意思是:一个业务行为会产生一系列结果,你就想象成一个方法执行后,有多个结果返回值,我们将这些结果返回值包装成一个值对象返回。


很久很久以前,看到ibm devloper网站上有一位达人主过,值对象其实就是我们常说的dto...