关于借书场景的领域建模,我从以下几个方面进行分析:
分析模型静态结构
我分析一个领域模型的静态结构的思路一般是:先找出我们需要关心的对象,对于借书这个场景,我们关心的有:
1. Account(账号):Id(账号唯一标识,自动生成), Number(卡号), Owner(账号当前拥有者用户信息), BorrowedBooks(账号当前借到的书)
2. Book(书本):Id(唯一标识,自动生成),BookInfo(值对象,包含书本基本信息),Count(表示当前库存数量)
3. BorrowHistory(借书历史、借书日志):AccountId(借书账号),BookId(书本Id),Count(数量,表示借了几本),BorrowTime(借书时间)
4. BorrowedBook(借到的书):BookId(书本Id),Count(书本数量)
通过上面的分析,那么模型的静态结构就很容易画出来了。
按面向过程的思维实现逻辑
这种分析思路是最容易的,因为我们不用考虑对象之间如何交互,我们只需要考虑场景结束后,每个对象会发生什么变化即可。所以,按照这个思路,我们得出借书这个场景发生后有以下几个对象会发生变化:
- Book的Count属性会变化(减少,因为书本被借出);
- Account的BorrowedBooks属性会增多(因为借到书);
- BorrowHistory会被创建,因为发生了一次借书的操作;
上面这3步在经典DDD中,我们通常会设计一个领域服务来完成,比如叫BorrowBookService
可以发现,其实面向过程的分析思路是一种面向结果的分析方法;我们只需要考虑一个交互过程的结果改变了哪些对象的什么状态即可,而对象之间到底如何交互的我们不用显式的建模出来;所以这种建模方法相对简单,因为我们考虑的东西比下面第这种方式要少一样东西,那就是对象之间的交互。
按面向对象的思维实现逻辑
也许有人会说,上面的面向过程的建模思路不是真正的OO,因为对象之间没有交互,对象只是一个data,只是一个对数据的封装而已。
那么,如果要让对象之间体现出交互,那我们该如何分析呢?我觉得最关键的是要把握一点:我们分析的时候要时刻按照“谁通知谁做什么事情,或谁被通知做什么事情”这个思路来分析;
好,那按照这个思路,那么对上面的对象:Account,Book,BorrowedHistory,我们如何来分析呢?
首先假设我们已经设计好了这个软件,然后有一个界面显示在屏幕上,然后用户用它的卡号(account.Number)登陆了系统,然后用户通过查询选择了几本书,然后点击“借书”按钮。整个借书场景就是从这个“借书”按钮开始启动。另外,整个借书场景的参与者信息有:accountId,bookId,count,表示哪个账号对哪本书借了几本。
好,那么,“借书”按钮被点击后,应该有一个对象被激活,先不管该对象是什么,我们只要知道该对象会做一件事情,就是:
var borrower = repository.load<Account>(accountId); //将借书人账号load到内存 var book = repository.load<Book>(bookId); //将书本对象load到内存 borrower.BorrowBook(book, count); //启动账号的借书行为
接下来borrower.BorrowBook方法内部会发生什么呢?想想现实世界怎么发生的就知道了,你去图书馆借书,你肯定告诉管理员说“我要借这几本书”,这句话潜在的意思是,请你把这几本书借给我,谢谢,呵呵。
那就很明白了,应该有一个对象,如图书馆管理员(administrator),他有借出书(LendBook)的职责行为,那么上面的BorrowBook方法内看起来就是如下这样:
borrower.BorrowBook(book, count) { //通知图书馆管理员把指定的书借给我count本,this就是我,呵呵 administrator.LendBook(this, book, count); //管理员把书借出来后,更新账号自己的当前借到的书的信息 var borrowedBook = _borrowedBooks.SingleOrDefault(x => x.BookId == book.Id); if (borrowedBook == null) { borrowedBooks.Add(new BorrowedBook(book, count)); } else { borrowedBook.AddBookCount(count); } }
接下来我们可以考虑administrator.LendBook这个行为做了什么?
administrator.LendBook(account, book, count) { //通知书本减少其库存数量 book.DecreaseCount(count); //方法内部需要检查库存数量是否足够,如果不够需要抛异常; //这里应该要记录借书记录了,因为译本书是否被借出的衡量标准是图书馆管理员说了算的,当他 //用扫描仪对该本书进行了扫描并确认后,就表示该本书确定被借出去了,所以我们可以在这里做 //创建借书记录的逻辑。 var borrowHistory = new BorrowHistory(account, book, count, DateTime.Now); //下面理想情况下我们不希望在这里保存borrowHistory,但是如果光是new一个BorrowHistory对象出来, //是没办法被持久化出来的,必须通过某种方式通知框架保存new出来的这个对象, //如果用经典的ddd,那如何保存borrowHistory呢?后面我会谈到一些关于这个的思考。 }
好,上面的分析我想应该很清晰地表达了对象之间如何交互,从而完成整个借书场景。但是,上面提到“借书”按钮被点击后,应该有一个对象被激活。那么这个对象会是什么呢?
我觉得你可以设计一个BorrowBookService领域服务,也可以设计一个BorrowBookContext场景类,它有一个Interaction(交互的意思)方法:
borrowBookContext.Interaction(Guid accountId, Guid bookId, int count) { var borrower = repository.load<Account>(accountId); //将借书人账号load到内存 var book = repository.load<Book>(bookId); //将书本对象load到内存 borrower.BorrowBook(book, count); //启动账号的借书行为 }
所以,整个交互的过程就是:
- 软件使用者(user)通知系统(system)我要借书;
- 系统于是创建一个BorrowBookContext场景对象,并通知该场景对象启动交互过程(Interaction);
- borrowBookContext通知仓储(repository)将account,book这两个对象从内存激活,通过load方法实现;
- 然后通知借书账号执行其借书行为,其实此时借书账号是扮演了IBorrower角色(即借书人的角色),所以严格来讲,BorrowBook这个行为是属于IBorrower这个角色的;
- 然后borrower通知administrator把书借出来;
- 然后administrator通知book减少其余额;
- 然后administrator创建借书记录,即产生借书日志;
从上可以明显的看出,每一个交互都是对象a通知对象b做什么,这是关键;上面的分析和代码充分体现了“对象交互”,也许这样的代码才是更OO吧,呵呵。
为了大家更好的理解这种OO的方式,我特地写了一个完整的例子。源代码下载地址:https://files.cnblogs.com/netfocus/BookLibraryExample.rar
最后一些补充
- 因为,现实生活中,你去图书馆借书,那执行借出书的那个管理员(administrator)代表的就是图书馆。甚至我们可以这样想,假设现在有一个自动借书机或借书网站,你插入你的借书卡(网站用户登录),然后输入要借的书,然后点击确定,然后书本就自动从借书机里吐出来了,呵呵。如果是网站,那就是会自动邮寄过来(当然还要输入寄送地址,呵呵)。所以,从这个分析可以知道,其实图书馆管理员不重要,它其实代表的是图书馆,而图书馆本质上就是提供借书服务。当然,因为我们上面只考虑的借书的场景,我们有没有想过books这个集合放在哪个对象上比较合适呢?我觉得很显而易见把,那就是图书馆,即library.Books,图书馆维护了所有的书本信息;所以,从整体来看,图书馆也有状态。
- 有时我们认为产生借书日志不是核心领域逻辑,因为并不是所有的图书借阅系统都需要记录借书记录,那这样的话,我们可以在应用层(也就是我上面的BorrowBookContext中)生成借书记录;
- 虽然按照OO的思路去领域建模出来的结果看起来很舒服,但实际上不是很实用,我个人认为属于中看不中用的设计,呵呵。因为这样的设计虽然做到了对象与对象之间的交互,但实际上当我们在面对并发和数据一致性时,都会引入事务。像上面的分析,我们知道一次借书,至少会影响3个聚合根的修改或新增,那意味着一次事务会跨3个聚合根。一旦引入事务,那在当用户访问量大,并发高的情况下,系统可用性是很差的;所以,国外DDD专家才推荐,一次事务只更新一个聚合根,那如果要遵守这样的规定,那如何实现上面的一次要修改3个聚合根的需求呢?呵呵,为了解决这个问题,我们需要通过saga了,就是类似于一个流程管理器的东西。引入saga,相当于实现了聚合根与聚合根之间的异步通信,而不是直接调用聚合根的方法通知其做事情;上面的设计的最大问题就是都是某个聚合根直接调用另一个聚合根的方法通知其做事情。实际上每个聚合根都自己内部维护了其一致性,聚合根之间完全可以通过异步的方式实现交互。saga就是用来实现聚合根之间异步交互的一种技术。saga就是将方法调用修改为:publish-subscribe,以及command的模式,呵呵。学习Saga的一个例子可以看看这篇文章:http://msdn.microsoft.com/en-us/library/jj591569.aspx