JiveJdon聚合的重构DDD设计

上篇

  出现重大BUG,发回帖时无法正确显示回帖情况,如下图状态只有在上线进入生产状态才时而出现无法正常显示:

这种不可捉摸的问题一般都是设计思路引起的,也就是你的设计没有与领域中那个规律共振,所以,难免有不和谐的问题。

我也从并发线程锁等方面考虑,但是发现,其实最好最严格的对象封装设计才是线程最安全的,这个可以从下面理解:

这个顽固问题是每次回帖后,再主题列表中,看最新回复,没有得到更新反应,在JJ中,用ForumThreadState来封装更新信息的,现在问题就是,也许ForumThreadState没有更新,或者更新的ForumThreadState是另外一个对象。不过,对象不一致性已经通过缓存解决了,因为发表回帖时,首先要缓存中获得父贴,而父贴因为之前用户阅读了,肯定在缓存中,不会立即被更新,所以,新的回帖和父贴就公用一个ForumThread,所以,不存在不一致性,可能很小。

那么为什么ForumThread的ForumThreadState有时没有执行呢?代码中ForumThreadState有几个动作:读取TreeModel(用来维持内存中帖子的树形结构);从数据库SQL查询获得最新帖子然后将这个帖子赋予ForumThreadState的lastPost,问题可能就出在这两处:

首先,通过JProfiler发现“读取TreeModel”很费CPU,每次帖子更新,需要从数据库中重新收集树形结构信息,树形结构反复访问很耗数据库性能,为什么要重新刷新呢,难道不能就只是将最新帖子加入已经存在内存树形结构中,不就更好吗?不必要读取数据库了,内存中ForumThread其实有一份,思考到这里,我也很惭愧我潜意识还是数据库编程,完全忽视内存中ForumThread,总是将缓存或内存中对象看成是数据库的备份,而不是将数据库看成是内存中对象的备份,当我的思路转到后面上来时,就豁然开朗了。

这样,我在ForumThread增加AddNewMessage这样的方法,将当前回帖加入内存中ForumThread的TreeModel中,而不是每次回帖后,从数据库中查询后,再创建新的treeModel,这样,性能快多,也避免因为回帖动作刚刚写完数据库,事务过程有可能没有结束(事务通常较慢),数据库隔离级别是read_commit,再读取可能因为没有commit,不能从数据库读取到最新回帖,新的思路也避免了这种情况的存在。

当我思路回到围绕内存中 ForumThread 对象时,该主题中最新一个帖子也是直接在addNewMessage中通过setLastPost迅速直接完成,不必再到数据库按时间select最新帖子了,这样,ForumThread 中state对象都可以通过 ForumThread 的addNewMessage update 等方法完成,ForumThread 也成为一个标准的胖模型,丰富模型。

看到有人将saveToDB放入领域模型中,这是一种生搬硬套,因为saveToDB涉及服务和SQL,这些很重的单例资源是不能绑在领域模型中的,只有该领域模型做聚合根,对其内部各个子对象的封闭性操作方法才是最好的胖模型行为。

而且,这样的领域模型因为保存在缓存中,每个实例只有一个,在多用户访问同一个实例领域模型情况下,也符合并发计算中所谓安全发布,在多线程下,只有将自己的行为封装在自己对象内部,再通过synchronized等内部锁机制保证这个模型对象内部操作一致性和原子性,否则,你将这些本来属于内部操作的,如forumThread的addNewMessag搬迁到service或外部实现,如果上锁,保证写读的封闭性都是很难做到的。

以上,从一个顽固BUG修正,谈到围绕对象编程和围绕数据库编程两个思路导致的问题,以及领域胖模型 和并发线程安全,这些都是统一的,当你思路重点在对象上,你才会关注领域模型的质量,才会关注并发线程安全。

问题依然存在

经过上述设计重构,发现问题依然。
前面认为出现这个BUG有两个原因:

  1. 回帖更新的ForumThread可能不是浏览时的ForumThread,前面排除这个原因,焦点还是转移到这里。
  2. 回帖更新动作很费CPU,或者从数据库中读取最新贴这一过程过于重量,经过前面重构,通过丰富ForumThread行为,增加addNewMessage等方法来直接在内存中更新。

既然第2点已经做得很好,那么就是第一点还是有问题,也就是说回帖更新的ForumThread就不是浏览时的ForumThread。这个情况出现是因为Thread的生命周期掌握不对。
至于在哪里不对,也无法从代码中一一定位,因为生命周期问题不会出现在某个具体代码处,它是整个代码在运行时刻一个总体表现,只有从两个方面来解决这个问题:

  1. 断点跟踪回帖更新流程,仔细核对流程中thread的生命周期状态。
  2. 重新考虑Thread模型性质:是实体 还是值对象?

这两个方面考虑都得到进展:
在核对回帖流程中,会想到一种可能:thread平时都保存在cache中,但是cache会定时清除,这样thread被cache清除后,下次再访问thread时,必须重新创建一个新的thread对象;而thread又可能被Message引用,这些Message还在缓存中,没有清除,这样这些message中的thread就一直存在,这些thread就和重新创建的thread不是同一个对象,thread不是共享了,变成复制有多份,回帖更新只是其中一个thread。
重新考虑thread的模型性质,它实质是根message,根贴这样实体的代表,thread应该和RootMessage根贴生命周期是一致的。生命周期控制是在Factory中实现的,因此Factory代码中应该加入这两者一致的约束性,之前没有做到,这部分代码要重构。
Thread虽然是实体RootMessage代表,但是它又有值对象Value Object影子,thread一般不变的,只有更新时才会变化,这个变化并不违反DDD中关于值对象不变的约束。
值对象有两种被其他对象使用方式:复制和共享,复制是被推荐的方式,因为值对象是不变的,没有标识,当然可以使用FlyWeight大量复制,每个引用对象人手一份,虽然每个引用对象引用的值对象不是同一个实例,但是他们内容相同,而且在传统的分布式环境中复制很容易传播。
thread因为有实体影子,用户浏览主题列表时,需要查询thread集合,这些使用复制是都可以实现,关键是:用户浏览主题时,他想知道这个主题更新信息,最后回帖是谁等等,如果使用thread的复制,那么所有thread都要通知到,显然不切实际。所以,thread只能特殊使用共享。
一个thread实例被缓存后,可以被这个主题下所有帖子Message共享,当有新帖更新时,更新thread状态,所有帖子对象引用的是同一个thread,他们就都知道,当前这个主题中有更新,如果更新的根贴RootMessage,就用新的根贴替代thread中原来的。
所有重构都集中到thread的工厂中,工厂不但要保证thread和RootMessage是一致的;而且必须保证thread是单例被共享的。
当一个对象需要隆重工厂仪式创建,说明需要被重点关注,那它是实体,如果还具有共享性,那么具有聚合根实体可能性很大。

下篇