领域驱动设计实践:还是图书馆借书的例子

第一次在这里发帖,不太懂这里的排版。。。原文在此

去年开始博客园和Jdon有一场DDD的讨论,是关于如何给一个图书馆的应用系统建模。大概是在讨论几个经典的Use Case:办卡、持卡借书和还书。
讨论最开始由博客园的张逸大牛发起(链接在此),给出了一个比较完整的建模。一方面从功能上实现了不少逾期罚款之类的功能,另一方面这个建模也涉及到了很多DDD的要点,比如聚合的划分什么的。
然后Jdon有两篇文章给出了回应(一、二),下面讨论的质量也比较高。针对于这个系统的建模来说,前者给出了一个思路是以借书卡为中心,借书行为由Card对象负责,然后还有一个观点是借书条目这个东西应该是个什么,什么时候被删除,用四色原型来看,大概就是在界定这个MI的范围。后者结合四色原型给出了比较完整的建模代码,但是我是不太同意这套模型的,主要两个方面,一个是和四色原型对应的是否准确。二是动作作为类(例如该设计的BorrowBook和BorrowedBookReturn)是传统设计中比较少见的,感觉接受起来有些困难。
小生接触DDD和四色原型时间都比较短,这里也斗胆贴出自己的一个初步设计,请走过路过不吝赐教。
首先给出类图

图一:完整的类图
这个设计借鉴了一些DCI的想法,操作的还是人(IPerson),但是需要在一定的场景中有一定的Role(IRole)才能处理。比如IReader作为一个role,有regist的能力,那么regist的示意流程是这样的
1: void Regist()
2: {
3: ILibrary library = null;//a library
4: IPerson person = null;//a person
5:
6: var reader = person.ActAs<IReader>();
7: reader.Regist(library);
8: }
在.Net中的DCI,其实是可以用扩展方法实现的,只要using某一个命名空间就可以把Regist方法“注入”到person对象,但是用命名空间当做context我认为还是不妥的。可能比较理想的还是用AOP来实现。java领域里已经有了Qi4j和AspectJ的实现,.net感觉要加油了呢。。。
回到这个模型的设计上来,刚才看到的是IReader这个role,我的设计里这个role是指到图书馆来的人(当然除了staff),而不管他有没有办过卡。他可以有regist(办卡)和search(查找书)两个行为。

图二:Reader是一个role
系统中另一个role是ICardHolder,他持有borrow这个行为。我还是认为这个行为不应该是由卡来承担。这个方法没有要求传入一个card参数,系统会从cardholder的cards中挑选card.Library = book.Library的一张。如果这个系统是一个网上借书的系统且一个cardholder可以拥有同一个library的多张card,那么可能要有一个重载方法,允许cardholder选择一张card。

图三:card和cardholder
这里我有个两个疑问,一是IReader和ICardHolder是否应该实现IPerson?二是ICardHolder是否应该实现IReader?好像实现的话一方面情理上更说得过去,二来允许ICardHolder做search和再一次regist也可能更符合需求?
ICardHolder和ICard在我的设计中是一对多的。这一方面因为我希望系统能同时支持多个Library,ICardHolder可能同时有多个Library的card,另一方面我不知道Library是否需要一个人只能有一张卡?如果只能一对一的话可能还需要一个ILibraryCard做过渡。
注意到Borrow方法要求传入参数为IInStockBook。这是我的设计中两个核心MI之一,另一个是IBorrowedBook

图四:book,in stock book和out of stock book
两者都实现IBook,但是又是不同的状态(一本书不能同时处于在库和借出两种状态),有其固定的Duration,所以我觉得它们应该是MI。
获取HoldingBooks和BorrowHistory还得是ICard的职责,在数据库中必然是关联到card上的。
其中IRequestable是指“可以得到手的东西”(这个名字不太好),系统中有两样,一个是card一个是borrowedBook。后面将会看到这是实现上的一个概念,混进了模型里比较不爽,同样的还有IEntity。
关于还书,好像没有很明确的执行这个操作的对象。无论是不是CardHolder本人来还书,应该都可以完成还书这个流程。甚至根本见不到还书的人,书就被放在还书的架子上了。(即使要交逾期罚款,还书人还是可以扔下书就跑,所以逾期罚款这个事儿只能先记在card里。。。)硬要说的话,可以说是一个scanner来触发的这个动作,但是scanner明显只应该有scan一个行为。所以我无奈把它放在了IBorrowedBook里了。
IBook应该是实体上的书,而BookInfo只是概念上的“一本书”,应该算是Thing和Description。

图五:book及其description
实现上的话,我是这样设计的

图六:request的处理
一个cardHolder及其borrow方法可能是这样的
1: public class CardHolder : ICardHolder, IRequestApplicant
2: {
3: public ICollection<ICard> Cards { get; set; }
4: public IRequestPublisher Publisher { get; set; }
5:
6: public void Borrow(IInStockBook book)
7: {
8: var request = new BorrowRequest<IBorrowedBook>(this, book)
9: {
10: Callback = response =>
11: {
12: if (response.IsApprovaled) { }//todo
13:
14: }
15: };
16: Publisher.Publish(request);
17: }
18: }
最终一个借书的流程可能是这样的
1: void SearchThenBorrowBook()
2: {
3: IPerson person = null;//a person
4: var bookInfo = new BookInfo
5: {
6: Author = new PersonInfo
7: {
8: Name = "Jeffery Richter"
9: },
10: Name = "CLR Via C#"
11: };
12: var reader = person.ActAs<IReader>();
13: var books = reader.Search(bookInfo).OfType<IInStockBook>();
14: if (books.Count() > 0)
15: {
16: var cardHolder = person.ActAs<ICardHolder>();
17: var bookToBorrow =
18: books.FirstOrDefault(book => cardHolder.Cards.Select(c => c.Library).Contains(book.Library));
19: if (bookToBorrow != null)
20: {
21: cardHolder.Borrow(bookToBorrow);
22: }
23: }
24: }
以上就是我对这个系统的一个想法,还没有完整的实现。斗胆先把一部分贴出来请多多指教。

首先,你这套类图建模思路和我们四色原型和DDD是很不同的,注意到你都使用的是接口,比较注重动作。

从第一印象来看,这张类图有点类似蜘蛛网呢?没有突出重点,这是一个小的图书借阅系统,如果再大点,估计这个类图还要非常大,能否给出重点?模型图虽然是用类图表达,但和类图主要区别就是模型图有主次,有边界。

2011年03月25日 15:13 "
banq"的内容
首先,你这套类图建模思路和我们四色原型和DDD是很不同的,注意到你都使用的是接口,比较注重动作。

从第一印象来看,这张类图有点类似蜘蛛网呢?没有突出重点,这是一个小的图书借阅系统,如果再大点,估计这个类图还要非常大,能否给出重点?模型图虽 ...


使用接口是为了能在没有实现的情况下快速写出实例代码,看能不能实现Use Case。。。真正实现的时候再考察是直接用类还是接口,比如除了IRole、IReader和ICardHolder都可以用类
重点的话大概算是文中图四吧,那个图中没有管理员,按照我的想法管理员按照权限和职责应该分别实现IRequestDispatcher和IRequestHandler
总之我刚刚开始学习,非常希望各位大侠们不吝赐教~~~多谢了

这个例子既有大量的接口(只有方法没有属性)又有大量只有属性没有方法的对象。这样看起来就非常迷糊。避免这种情况有个准则就是——高内聚、低耦合。

另外,我认为,从客观事件来看,可以抽象出“书架”这一事物。添加新书就是入库,销毁旧书就是出库,借书就是书架下一本书,还书就是书架上一本书。
[该贴被showerxp于2011-04-10 15:50修改过]