DDD的一次小实践,在线学习。

用户学习用例场景
1、系统显示课程列表。
2、用户选择其中一个课程查看内容。
3、用户决定学习该课程。
4、系统根据用户之前的学习该课程记录,决定用户学习哪一个章节,并显示给用户看。
5、用户学完该章节之后,系统列出对应的测试题。
6、用户回答测试题,系统判断是否正确。
7、系统根据课程设置的规则,判断是否有效学习,如果有效则记录用户学习时间和章节,如果不是有效学习则提醒用户如何有效学习。
8、用例结束。

局部图1


局部图1


局部图2

说明:软件模型关注重点是用户学习交互过程,没有包括课程、章节、测试等对象的创建,修改,删除等内容,因为这部分就目前业务需求来说,除了CURD,没有涉及的领域知识。

聚合根:课程、节点、学习记录。

困惑点:课程是否该包涵节点Id。思前想后,我这里包涵了节点Id,虽然课程和节点都是聚合根,这样一个聚合根对象拥有一群另外一聚合根对象Id似乎很别扭。我这么做可以找到领域内的解释,就像学生之于班级,班级名册只要记录学生名称,而不要学生本人。

难点:聚合根对应的仓库如何优雅的处理聚合的实体和值对象状态的改变,直白的说就是如何处理聚合实体和值对象的CRUD,特别是有些值对象的删除和修改。比如章节的仓库如何处理小测试实体的新增和修改等操作。似乎除了引入事件,还真没有其他优雅的方法。当然,我用php的魔法方法实现AOP,也可以比较好的处理这类通用的垂直操作。


[该贴被showerxp于2013-08-09 17:46修改过]


[该贴被showerxp于2013-08-09 17:47修改过]
[该贴被showerxp于2013-08-09 17:49修改过]
[该贴被showerxp于2013-08-09 17:49修改过]

章节包含课程,要不课程里要有数组,如果业务允许,单向关联的好处理些。
章节需要课程,但课程可以跳过某个章节的。

聚合根被引用没问题,只要不是内部的就行。最好就引用一个标识就可以了。
还有上面的图里的学习,不是个实体,好像只起到关联的作用,不应存在。

不知道这个应用的关注点是什么,具体的业务核心是什么。先找出核心的业务,其他的可以先放一放。

章节也就是图中的节点,包涵的是课程“Id”。同样课程包涵一连串的章节“Id”。
我认为,一个聚合根绝对不能引用另一个聚合根(包括任何形式的依赖关系)。但是可以引用或者持有另一个聚合根的Id。我上面就业务前面已经解释了:一个班级可以有学生的花名册(学生Id),一个学生清楚的知道自己在哪个班级(班级Id)。如果一个聚合根被另一个聚合根引用——也就是持有,那会出现什么问题呢?比如这么个业务:学校开运动会,小明代表他所在班级(班级Id)参加100米跑。如果班级持有小明,会出现班级站在跑道上参加比赛的情况。这是荒谬的!因为小明作为一个“人”有跑的行为,小明的班级是没有跑的行为,如果班级引用的小明,那么班级也间接的有“跑”的行为,从而可以参加跑步比赛了。
因此,我把学习当做一个service,大家在“学习”这个舞台实现章节和课程的交互。就好像“比赛”也是一个舞台一样,各个班级的代表选手在这个舞台实现他们之间的交互。

具体的业务流程已经在用例场景中写明白了,这些就是核心业务。现在用我设计的这些类实现了。
[该贴被showerxp于2013-08-12 22:31修改过]

班级==》学生==》 课程==》章节==》测试==》学习记录(结果)

public class Class{
id,name
method()
}
public class ClassId{
id,name
}
public class Student{
id,name,classId
method()
}
班级不一定要包含学生。
如果需要,再加就可以了。

一个聚合根绝对不能引用另一个聚合根(包括任何形式的依赖关系)??
这谁说的,没听说过。也不能够呀。只听说除了聚合根,其他的不可直接引用呀。

班级有小明的行为,好奇怪的说法。不知道你怎么写的,代码帖出来看看。

我前面说的意思是:如果班级作为聚合根对象持有小明这个聚合根对象,就会出现班级拥有小明的行为,比如小明有“跑”这个行为,那么持有小明的班级也间接具有“跑”的行为。

所以——一个聚合根绝对不能引用另一个聚合根(包括任何形式的依赖关系)。这个观点是我想出来的,理由如上,当然你可以不必理会我的结论,但是可以想一想我的理由是否正确。

当然,不经意中我自己也违反了我定的“铁律”。类图中,“学习记录”也是聚合根,却和课程有依赖关系,应当将“课程”类中的“获取未学节点ID”的参数修改为“(已学节点Id集合)”。

嗯,好久没有来这里了。大家还可以看这个帖子http://www.jdon.com/45318
使用依赖注入实现聚合根之间调用的逻辑悖论

他们说的是DI与DDD之间的悖论。所以提出最好通过事件的方式来调用,但不是绝对的,
这要分具体情况的,
对象之间如果是平等的弱关联,这样就便于更好模块化,通过事件的方式,实现松耦合,一辈子最好不见面。
如果是聚合或者组合,直接点没问题,同一个屋檐下,抬头不见低头见。

至于其他的问题,最好写出代码来,看看有什么更好的实践,我不相信爱情,只相信事实。

发现这个坛子里的兄弟更喜欢讨论理论,不屑于这种点滴的小实践。想起了南北朝时期的文人有个通性——清谈。

2013-08-09 17:44 "@showerxp
"的内容
聚合根:课程、节点、学习记录。 ...

楼主等得有点不耐烦了,呵呵,我上来冒个泡,我认为这个用例场景下核心应该是“学习”,我们需要找出一个聚合根能够突出“学习”这个核心,表达学生学习课程这个主要活动的核心,好像这里的“学习记录”应该是聚合根。

我认为聚合根是方向性问题,如果聚合根找得不同,就代表方向不同,一个向南一个向北,带来细枝末节差别很多。

仅供参考。

banq 说的好像有道理。
把study 看成 order
studyrecord 看成orderdetail
chapter 看成 product

也许会有奇效,也说不定。

我这学习记录已经是聚合根了。这东西领域概念中不好归纳到课程、章节中,所以独立出来就是一个聚合根。

一直有个问题困惑着我。
我恪守聚合根不能相互持有的目的就是做隔离,但是始终可能会碰到一个问题。具体到本案,我认为课程聚合根可以拥有章节聚合根Id而不是章节本身,就像班级可以有学生花名册而不是学生本人一样。这样导致课程还是要知道章节具体的数据结构,从而破坏了隔离。
那么有什么办法避免这种一个聚合根需要获取另一个聚合根的关键信息。

2013-09-05 09:19 "@showerxp
"的内容
有什么办法避免这种一个聚合根需要获取另一个聚合根的关键信息 ...

办法很多,通过仓储获得,或者“另一个聚合根”将关键信息以值对象形式放出。

现行做法也是,通过课程仓库直接访问章节的Id。我的困惑也就是在这里,这样不就是课程聚合根代码编写者还需要了解章节聚合根的数据结构?

另:昨天你的回复我到今天还看不到,想了一下不对,清除了浏览器缓存才看到。

2013-09-06 08:56 "@showerxp
"的内容
现行做法也是,通过课程仓库直接访问章节的Id。我的困惑也就是在这里,这样不就是课程聚合根代码编写者还需要了解章节聚合根的数据结构? ...

课程只是包含了一些章节的ID吧,这不算是了解章节聚合根的数据结构啊!
就像订单聚合订单项,每个订单项会持有一些商品信息,如商品ID,商品价格,当然订单项还会有商品数量的信息。

那此时订单项持有了商品的一部分拷贝信息(值对象),按照楼主的理解,是否也算是了解了商品的数据结构?