DDD案例:网上书店

网上书店是采用DDD设计思想构建的一个应用系统示例,实现网上书店的常用功能:包括浏览书籍、挑选书籍、提交订单、查看订单、自动折扣、处理订单、取消订单等。未登录用户可以浏览和挑选书籍;已登录用户可以提交和查看自己相关的订单;管理员可以处理订单。
经过业务抽象,即使是这样一个简单的业务场景也包含了很多领域对象,例如订单、账户、书籍、购物车、购物项、折扣等,通过分析和设计,可以得到如下的设计图:



BookStoreAction负责处理展现层的请求,并把请求转发给业务服务IBookStoreBS,业务服务负责调度上图中显示的领域对象,处理该场景的所有业务。
其中领域对象和现实业务的对应关系为:
Account——账户
Order——订单
Book——书籍
Cart——购物车
Item——订单项
Discount——折扣
与事务脚本的编程模式不同,领域驱动设计不是把业务逻辑放在BS(BusinessService)中,而是由具备属性、行为和状态的领域对象处理。例如Order类,它是一个相对独立的、能够处理自身关联业务的领域对象。在本系统中,我们对Order的描述如下:
订单的实现类是com.ddd.bookstore.model.Order,类中除了联系方式、邮寄地址等基本属性外,还有以下领域相关的行为:
init(...),结算时调用方法,根据当前用户与购物车中的Items初始化订单,供用户修改。
submit(...),提交订单时调用的方法,保存订单。
cancel(...),取消订单,把订单和相关item的状态设置为“已取消”,然后委托Dao进行持久化。
dispose(...),处理订单,首先更新订单项的状态,然后委托Dao持久化订单数据。
reSubmit、setItemsStatus......
通过以上的描述,我们可以看到,Order类基本上覆盖了现实世界中订单这个业务的所有行为和状态,是相对内聚的,这样的特性使其复用性大大增加,即使未来开发新的模块,涉及到订单业务的,可以直接复用Order类。同时在后期维护中,如果我想了解订单的业务,直接读Order的代码就可以了。
从上图中我们还可以清晰的看到各个领域对象之间的关系。Order和Cart都聚合了Item,对应都是1...n,Item聚合了Book,对应关系1...1。Order分别与折扣、账户发生关联和调用等等,整个网上书店的场景就这样描述出来了。
另外,不要忘了BS,除了起到基础设施的作用外(事务管理和服务共享),它还要负责调度和维护领域对象之间的关系。因为总会有些业务逻辑,既不属于这个领域对象,也不属于那个,那这部分业务由谁来处理呢?由BS来处理。例如在管理员处理订单这个场景中,首先需要根据订单信息获取账户,根据账户信息确定折扣率,同时进行余额校验,如果校验通过,就会调用订单对象的dispose方法处理订单,这个场景会涉及到Order、Account、Discount等对象,这样的业务逻辑,应该由BS实现。
IBookStoreDao是数据访问对象,可以被BS调用,用来持久化对象,也可以被领域对象引用,用来持久化自身。
通过以上的描述,我们可以看到,整个设计和实现是优雅、清晰的。业务逻辑没有堆积在BS中,而是分散在BS和各个领域对象中,服务和对象都与现实世界的业务息息相关,无论是对领域专家、开发人员和后期维护人员,都能这种方式中获得自己需要的内容。

2013-01-07 18:19 "@cloudstack"的内容
首先需要根据订单信息获取账户,根据账户信息确定折扣率,同时进行余额校验,如果校验通过,就会调用订单对象的dispose方法处理订单,这个场景会涉及到Order、Account、Discount等对象,这样的业务逻辑,应该由BS实现 ...

我觉得Cart和Account必须位于User领域之内,一个购物车必须属于某个用户,即使这个用户是“游客_123”。Account也必须要有一个唯一的所有者。

如果因为有了充血模型就完全放弃BusinessObject也不太好,服务接口的作用是调用仓储或切面,并对外部提供API。领域之外的业务完全可以放在切面或者BO里实现,不必硬编码到接口里。余额校验的逻辑可能多个接口都会用到,也可能随时变化。

现在接口的作用就仅剩下将SPI转换为API一个用途了,自然也不必调用DAO了。

问题有两个:
1.没有看到聚合根,这是DDD分析的核心。这里Book才是聚合根
2.Order和购物车都是Book参与销售活动产生的结果。不是领域的本质。

根本原因:注重了细节忘记了方向。

推荐你使用四色原型来对这个系统继续宏观把握,或者用故事的方式来简单描述一下这个系统。

建议你使用我总结Jdon分析法来分析这个系统,我引导如下:

这张图代表横向和纵向两个方向,分析一个系统要从这两个方向入手:

首先纵向,用DDD找出聚合根实体,代表这个系统的结构本质,很显然这里是BOOK。

再从横向:用户发出操作命令产生事件,在这个系统中,用户发出挑选或购买书籍BOOK的命令,也就是购书活动,改变了聚合根BOOK的状态。

什么状态呢?
1. 挑选命令致使BOOK放入购物车,
2.购买书籍命令致使BOOK加入了订单。

我帮助你分析到这里,下面我相信你知道如何实现了。


2013-01-08 09:37 "@banq"的内容
1.没有看到聚合根,这是DDD分析的核心。这里Book才是聚合根
2.Order和购物车都是Book参与销售活动产生的结果。不是领域的本质。 ...

个人感觉聚合和聚合根(Aggregate Root)的东西有问题复杂化的倾向,用传统的聚合、组合等概念去描述领域对象之间的关系更容易理解,所以这里就没有描述聚合根

2013-01-08 12:08 "@cloudstack"的内容
感觉聚合和聚合根(Aggregate Root)的东西有问题复杂化的倾向 ...

不是复杂化,是思路不同而已,横看成岭侧成峰,如果你不从DDD角度看,你当然看不到聚合根,也无法知道聚合根是DDD的根本,这我在以前帖子反复强调了。

如果你这个帖子不命名为DDD案例,而是单纯建模案例,也许就无可挑剔了。

如果我们确定BOOK为聚合根,关键是订单是聚合根实体还是值对象的问题,如果我们这个需求中有网上支付和货物跟踪,无疑订单在这些流程中变成实体甚至是聚合根。

在一个简单的网上书店中,也就是没有那么多子领域系统,那么单纯以BOOK为聚合根的这个子领域边界中,我们可以把订单看成是值对象,用户买书的一种关系凭证,是一种活动的结果。
[该贴被banq于2013-01-08 16:02修改过]

2013-01-08 13:51 "@banq"的内容
如果我们确定BOOK为聚合根,关键是订单是聚合根实体还是值对象的问题,如果我们这个需求中有网上支付和货物跟踪,无疑订单在这些流程中变成实体甚至是聚合根。 ...

这个系统涉及到支付和物流跟踪,因此Order和Book都是聚合根,Cart和Account为实体,其它为ValueObject:




[该贴被thinkjava于2013-01-09 12:00修改过]

从技术和业务方面谈谈我的看法
1、DAO元素可以用Repository替换掉,可有四个聚合对象
AccountRepository/OrderRepository/CartRepository/BookRepository,完成相关对象的重建与持久化工作。
2、IBookStoreService这个粒度太大,更象是应用层的服务,
领域层可以用象IOrderPlaceService、IOrderProcessService
ICartPickService等替代。
3、Order与Cart共用Item,OrderItem与CartItem虽然都指向同一样东西但其业务含义完全不一样。
4、看了你的需求,其实上订单应该是潜在的两种生成方式,一个是有购物车生成订单,一个是管理员创建订单,所以Order.init()这个方法不是很合适,最好有领域服务来完成。

5、Track这个太抽象,可以用OrderHistory与一系列的OrderEvent重构,当然也可以响应的引入一些事件传递机制(EventBUS等)

2013-01-09 11:59 "@thinkjava"的内容
这个系统涉及到支付和物流跟踪,因此Order和Book都是聚合根 ...

既然这个系统涉及支付和物流,那么就要根据其内部聚合进行聚合分界,就像人以群分,物以类聚一样,你已经认识到Order和Book都是聚合根,但是没有认识到这两个根是不同群中的头。聚合根实则是聚合的显式表现,聚合是隐式的。

比如:两个算法公式就代表两个聚合体。
1+1=3;
2+2=4;
这两个公式分别代表不同意义,你不应该写成:
1+1=3 2+2=4
你就是写在一行,别人一眼也看出这是两个公式,为什么?因为公式算法自己的聚合性,体现在"+"和"="这些逻辑概念上。

事物分析方法之道:找出内聚性强的结构,以包含这个结构为边界进行切分。

这个道理其实西方科学分析根本思想,如果研究过中医中国文化的人就发现中国人思维方法与其不太一样。而所谓全盘西化的正规教学课程没有提出这种区别,中国人就是学到大学生,也可能不能掌握西方科学分析精髓。这是目前中国知识分子的悲哀吧。

好了,以上观点仅代表个人,不针对任何人,下面回归正题,我帮助楼主根据聚合进行了系统切分,也就是划分子领域,如下:



2013-01-10 08:24 "@banq"的内容
既然这个系统涉及支付和物流,那么就要根据其内部聚合进行聚合分界,就像人以群分,物以类聚一样,你已经认识到Order和Book都是聚合根,但是没有认识到这两个根是不同群中的头。聚合根实则是聚合的显式表现,聚合是隐式的。 ...

看了Banq老师划分的这张聚合体图片,有一个疑问。

聚合根是实体吗?以上图的聚合根Book来看,Book代表的是“书籍”的概念,页不是某一本书(实体对象)。

2013-01-10 08:24 "@banq"的内容
事物分析方法之道:找出内聚性强的结构,以包含这个结构为边界进行切分。 ...

感谢banq老师,这就是DDD中的聚合,边界概念,边界外部对象只能通过根去访问聚合对象群,这种思想可以保证聚合的不变性,如何定位聚合根,我的想法就是:第一:首先是实体;第二:需要在其上应用一切事件(Event)的需要;第三:根存在,其它对象存在,根不存在,其它对象全部消除

2013-01-09 23:38 "@clonalman"的内容
1、DAO元素可以用Repository替换掉,可有四个聚合对象
AccountRepository/OrderRepository/CartRepository/BookRepository,完成相关对象的重建与持久化工作。
2、IBookStoreService这个粒度太大,更象是应用层的服务,
领域层可以用象IOrderPlaceService、IOrderProcessService
ICartPickService等替代。

由于Account和Cart不是聚合根,因此不能有关于他们两个的Repository
DDD的四层架构要求应用层不能参与业务逻辑,业务逻辑全集中在领域层,IBookStoreService这里是用来处理一些业务逻辑的,是DDD中的服务

2013-01-10 08:24 "@banq"的内容

既然这个系统涉及支付和物流,那么就要根据其内部聚合进行聚合分界,就像人以群分,物以类聚一样,你已经认识到Order和Book都是聚合根,但是没有认识到这两个根是不同群中的头。聚合根实则是聚合的显式表现,聚合是隐式的。 ...

2013-01-10 11:34 "@thinkjava"的内容
由于Account和Cart不是聚合根,因此不能有关于他们两个的RepositoryDDD的四层架构要求应用层不能参与业务逻辑,业务逻辑全集中在领域层,IBookStoreService这里是用来处理一些业务逻辑的,是DDD中的服务 ...

1、分聚合根是分系统、子系统、模块、子模块的继续,都是依业务而定,而非设计上看起来是否好看(Evans书里有例子,中看不中用模型),购物车、帐户如果不是独立的聚合根,先考虑其生命周期会被置于哪个聚合根下,仔细推敲一下。
(判定聚合根没有固定的模式,如果一个聚合根在特定业务条件下能含盖一个对象全部过程,完全可以将该对象置于聚合根下,随着业务的深入,发现含盖不了,随时可以分离出来成为一个独立的聚合根)
2、IBookStoreService命名太够笼统太宽泛,领域服务需要明确指向,有特定的业务含义,建议IBookStoreService不要放在领域层


[该贴被clonalman于2013-01-10 22:44修改过]

2013-01-10 22:32 "@clonalman"的内容
2、IBookStoreService命名太够笼统太宽泛,领域服务需要明确指向,有特定的业务含义,建议IBookStoreService不要放在领域层 ...

命名是有些笼统,实际应用时,需要根据具体业务具体定义,但Service是属于具有业务逻辑的领域层服务,他是领域模型的一种,因此其居于领域层,不属于其它层,可以这么理解吗?
[该贴被thinkjava于2013-01-11 10:19修改过]