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

时间:2022-09-09 17:50:25

去年开始博客园和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: }

以上就是我对这个系统的一个想法,还没有完整的实现。斗胆先把一部分贴出来请多多指教。