大家看看我设计的这个关于图书馆借书还书的模型属于贫血还是充血?

之前大家也都对图书馆借书还书的例子讨论很多了。所以业务需求描述我就不多说了。直接贴代码吧。
下面代码的实现基于我自己开发的一个EventSourcing框架,继承自EventSourcing<T>类的表明是一个事件源。


/// <summary>
/// 图书馆账号
/// </summary>
public class Account : EventSource<Guid>
{
public Account() { }
public Account(string number, string owner)
: base(Guid.NewGuid())
{
Assert.IsNotNullOrWhiteSpace(number);
Assert.IsNotNullOrWhiteSpace(owner);
ApplyEvent(new AccountCreated(Id, number, owner));
}

public string Number { get; private set; }
public string Owner { get; private set; }

private void OnAccountCreated(AccountCreated evnt)
{
Number = evnt.Number;
Owner = evnt.Owner;
}
}
/// <summary>
/// 账号创建事件
/// </summary>
public class AccountCreated : IEvent
{
public Guid Id { get; private set; }
public string Number { get; private set; }
public string Owner { get; private set; }

public AccountCreated(Guid id, string number, string owner)
{
Id = id;
Number = number;
Owner = owner;
}
}



/// <summary>
/// 图书事件源
/// </summary>
public class Book : EventSource<Guid>
{
public Book() { }
public Book(BookInfo bookInfo)
: base(Guid.NewGuid())
{
Assert.IsValid(bookInfo);
ApplyEvent(new BookCreated(Id, bookInfo));
}

public BookInfo BookInfo { get; private set; }

private void OnBookCreated(BookCreated evnt)
{
BookInfo = evnt.BookInfo;
}
}
/// <summary>
/// 图书描述信息,值对象
/// </summary>
public class BookInfo
{
public string Name { get; private set; }
public string Author { get; private set; }
public string Publisher { get; private set; }
public string ISBN { get; private set; }
public string Description { get; private set; }

protected BookInfo() { }
public BookInfo(string name, string isbn, string author, string publisher, string description)
{
Name = name;
ISBN = isbn;
Author = author;
Publisher = publisher;
Description = description;
}
}
/// <summary>
/// 图书创建事件
/// </summary>
public class BookCreated : IEvent
{
public Guid Id { get; private set; }
public BookInfo BookInfo { get; private set; }

public BookCreated(Guid id, BookInfo bookInfo)
{
Id = id;
BookInfo = bookInfo;
}
}



/// <summary>
/// 图书馆事件源
/// </summary>
public class Library : EventSource<Guid>
{
private IList<BookStoreItem> _bookStoreItems = new List<BookStoreItem>();

public Library() { }
public Library(string name)
: base(Guid.NewGuid())
{
Assert.IsNotNullOrWhiteSpace(name);
ApplyEvent(new LibraryCreated(Id, name));
}

public string Name { get; private set; }
public IEnumerable<BookStoreItem> BookStoreItems { get { return _bookStoreItems; } }

public BookStoreItem GetBookStoreItem(Guid bookId)
{
return _bookStoreItems.SingleOrDefault(x => x.BookId == bookId);
}

public void AddBook(Book book, int count)
{
Assert.IsValid(book);
Assert.Greater(count, 0);
var bookStoreItem = _bookStoreItems.SingleOrDefault(x => x.BookId == book.Id);
if (bookStoreItem == null)
{
ApplyEvent(new NewBookStored(Id, book.Id, count));
}
else
{
ApplyEvent(new BookStoreItemCountUpdated(Id, book.Id, bookStoreItem.Count + count));
}
}
public void LendBook(Book book, int count)
{
Assert.IsValid(book);
Assert.Greater(count, 0);

var bookStoreItem = _bookStoreItems.SingleOrDefault(x => x.BookId == book.Id);
Assert.IsNotNull(bookStoreItem);
Assert.GreaterOrEqual(bookStoreItem.Count, count);

ApplyEvent(new BookStoreItemCountUpdated(Id, book.Id, bookStoreItem.Count - count));
}
public void ReceiveBook(Book book, int count)
{
Assert.IsValid(book);
Assert.Greater(count, 0);

var bookStoreItem = _bookStoreItems.SingleOrDefault(x => x.BookId == book.Id);
Assert.IsNotNull(bookStoreItem);

ApplyEvent(new BookStoreItemCountUpdated(Id, book.Id, bookStoreItem.Count + count));
}

private void OnLibraryCreated(LibraryCreated evnt)
{
Name = evnt.Name;
}
private void OnNewBookStored(NewBookStored evnt)
{
_bookStoreItems.Add(new BookStoreItem(evnt.BookId, Id, evnt.Count));
}
private void OnBookStoreItemCountUpdated(BookStoreItemCountUpdated evnt)
{
_bookStoreItems.Single(x => x.BookId == evnt.BookId).SetCount(evnt.Count);
}
}
/// <summary>
/// 图书馆中描述一本书在图书馆的藏书信息,目前只记录一本书在图书馆当前还有多少本,
/// 实际生活中可能还会记录这本书在图书馆的位置等其他藏书信息
/// </summary>
public class BookStoreItem
{
public Guid BookId { get; private set; }
public Guid LibraryId { get; private set; }
public int Count { get; private set; }

protected BookStoreItem() { }
public BookStoreItem(Guid bookId, Guid libraryId, int count)
{
BookId = bookId;
LibraryId = libraryId;
Count = count;
}

internal void SetCount(int count)
{
Assert.GreaterOrEqual(count, 0);
Count = count;
}

public override bool Equals(object obj)
{
BookStoreItem item = obj as BookStoreItem;
if (item == null)
{
return false;
}
if (item.BookId == BookId)
{
return true;
}
return false;
}
public override int GetHashCode()
{
return BookId.GetHashCode();
}
}
/// <summary>
/// 图书馆创建事件
/// </summary>
public class LibraryCreated : IEvent
{
public Guid Id { get; private set; }
public string Name { get; private set; }

public LibraryCreated(Guid id, string name)
{
Id = id;
Name = name;
}
}
/// <summary>
/// 图书新入库事件
/// </summary>
public class NewBookStored : IEvent
{
public Guid LibraryId { get; private set; }
public Guid BookId { get; private set; }
public int Count { get; private set; }

public NewBookStored(Guid libraryId, Guid bookId, int count)
{
LibraryId = libraryId;
BookId = bookId;
Count = count;
}
}
/// <summary>
/// 图书藏书数量更改事件
/// </summary>
public class BookStoreItemCountUpdated : IEvent
{
public Guid LibraryId { get; private set; }
public Guid BookId { get; private set; }
public int Count { get; private set; }

public BookStoreItemCountUpdated(Guid libraryId, Guid bookId, int count)
{
LibraryId = libraryId;
BookId = bookId;
Count = count;
}
}



/// <summary>
/// 一个与图书相关的处理事件,目前的处理事件的类型有借书和还书;
/// 一个处理事件描述了一本书在某个图书馆被某个账号在某个时刻被借出或还入
/// </summary>
public class HandlingEvent : EventSource<Guid>
{
public HandlingEvent() { }
public HandlingEvent(Book book, Account account, Library library, HandlingType handlingType)
: base(Guid.NewGuid())
{
Assert.IsValid(book);
Assert.IsValid(account);
Assert.IsValid(library);
ApplyEvent(new HandlingEventCreated(Id, book.Id, account.Id, library.Id, handlingType, DateTime.Now));
}

public Guid BookId { get; private set; }
public Guid AccountId { get; private set; }
public Guid LibraryId { get; private set; }
public HandlingType HandlingType { get; private set; }
public DateTime Time { get; private set; }

private void OnHandlingEventCreated(HandlingEventCreated evnt)
{
BookId = evnt.BookId;
AccountId = evnt.AccountId;
LibraryId = evnt.LibraryId;
HandlingType = evnt.HandlingType;
Time = evnt.Time;
}
}
/// <summary>
/// 处理事件枚举定义
/// </summary>
public enum HandlingType
{
Borrow = 1,
Return
}
/// <summary>
/// 一个图书处理事件的创建事件
/// </summary>
public class HandlingEventCreated : IEvent
{
public Guid Id { get; private set; }
public Guid BookId { get; private set; }
public Guid AccountId { get; private set; }
public Guid LibraryId { get; private set; }
public HandlingType HandlingType { get; private set; }
public DateTime Time { get; private set; }

public HandlingEventCreated(Guid id, Guid bookId, Guid accountId, Guid libraryId, HandlingType handlingType, DateTime time)
{
Id = id;
BookId = bookId;
AccountId = accountId;
LibraryId = libraryId;
HandlingType = handlingType;
Time = time;
}
}



/// <summary>
/// 图书服务接口定义,对外提供Facade面向功能的服务,内务实现通过协调领域对象完成业务逻辑;
/// 目前通过加Transaction标记完成事务支持;所以,这里的服务同时完成了应用层服务于领域层服务的双重职责;
/// </summary>
public interface IBookService
{
Book CreateBook(BookInfo bookInfo);
void AddBookToLibrary(Guid bookId, int count, Guid libraryId);
void BorrowBook(Guid bookId, Guid accountId, Guid libraryId, int count);
void ReturnBook(Guid bookId, Guid accountId, Guid libraryId, int count);
}
/// <summary>
/// 图书服务实现类
/// </summary>
[Transactional]
public class BookService : IBookService
{
private IRepository _repository;

public BookService(IRepository repository)
{
_repository = repository;
}

[Transaction]
Book IBookService.CreateBook(BookInfo bookInfo)
{
var book = new Book(bookInfo);
_repository.Add(book);
return book;
}
[Transaction]
void IBookService.AddBookToLibrary(Guid bookId, int count, Guid libraryId)
{
var book = _repository.GetById<Book>(bookId);
var libarary = _repository.GetById<Library>(libraryId);
libarary.AddBook(book, count);
}
[Transaction]
void IBookService.BorrowBook(Guid bookId, Guid accountId, Guid libraryId, int count)
{
var book = _repository.GetById<Book>(bookId);
var library = _repository.GetById<Library>(libraryId);
var account = _repository.GetById<Account>(accountId);

library.LendBook(book, count);
_repository.Add(new HandlingEvent(book, account, library, HandlingType.Borrow));
}
[Transaction]
void IBookService.ReturnBook(Guid bookId, Guid accountId, Guid libraryId, int count)
{
var book = _repository.GetById<Book>(bookId);
var library = _repository.GetById<Library>(libraryId);
var account = _repository.GetById<Account>(accountId);

library.ReceiveBook(book, count);
_repository.Add(new HandlingEvent(book, account, library, HandlingType.Return));
}
}



/// <summary>
/// 图书馆服务接口定义,目前只提供一个方法,用于创建一个图书馆
/// </summary>
public interface ILibraryService
{
Library Create(string name);
}
/// <summary>
/// 图书馆服务实现类
/// </summary>
[Transactional]
public class LibraryService : ILibraryService
{
private IRepository _repository;

public LibraryService(IRepository repository)
{
_repository = repository;
}

[Transaction]
Library ILibraryService.Create(string name)
{
var library = new Library(name);
_repository.Add(library);
return library;
}
}
/// <summary>
/// 账号服务接口定义,目前只提供新建图书馆账号的功能
/// </summary>
public interface IAccountService
{
Account Create(string number, string owner);
}
/// <summary>
/// 账号服务实现类
/// </summary>
[Transactional]
public class AccountService : IAccountService
{
private IRepository _repository;

public AccountService(IRepository repository)
{
_repository = repository;
}

[Transaction]
Account IAccountService.Create(string number, string owner)
{
var account = new Account(number, owner);
_repository.Add(account);
return account;
}
}


/// <summary>
/// 单元测试类,用于测试图书入库、借书,以及还书的功能
/// </summary>
public class BookBorrowAndReturnTest : TestBase
{
[Test]
[Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod]
public void BookBorrowAndReturnTest1()
{
var accountService = DependencyResolver.Resolve<IAccountService>();
var libraryService = DependencyResolver.Resolve<ILibraryService>();
var bookService = DependencyResolver.Resolve<IBookService>();
var repository = DependencyResolver.Resolve<IRepository>();

//创建借书账号
var account = accountService.Create(RandomString(), RandomString());

//创建三本书
var book1Info = new BookInfo(
"Java Programming", "ISBN-001", "John", "Publisher1", null);
var book2Info = new BookInfo(
".Net Programming", "ISBN-002", "Jim", "Publisher2", null);
var book3Info = new BookInfo(
"Mono Develop", "ISBN-003", "Richer", "Publisher3", null);
var book1 = bookService.CreateBook(book1Info);
var book2 = bookService.CreateBook(book2Info);
var book3 = bookService.CreateBook(book3Info);

//创建图书馆
var library = libraryService.Create(RandomString());

//图书入库
bookService.AddBookToLibrary(book1.Id, 5, library.Id);
bookService.AddBookToLibrary(book2.Id, 5, library.Id);
bookService.AddBookToLibrary(book3.Id, 5, library.Id);

//借书
bookService.BorrowBook(book1.Id, account.Id, library.Id, 3);
bookService.BorrowBook(book3.Id, account.Id, library.Id, 2);

//还书
bookService.ReturnBook(book1.Id, account.Id, library.Id, 1);
bookService.ReturnBook(book1.Id, account.Id, library.Id, 1);
bookService.ReturnBook(book3.Id, account.Id, library.Id, 1);

Evict(library);

//Assert图书馆剩余书本是否正确
library = repository.GetById<Library>(library.Id);
Assert.AreEqual(4, library.BookStoreItems.Single(x => x.BookId == book1.Id).Count);
Assert.AreEqual(5, library.BookStoreItems.Single(x => x.BookId == book2.Id).Count);
Assert.AreEqual(4, library.BookStoreItems.Single(x => x.BookId == book3.Id).Count);
}
}

[该贴被tangxuehua于2012-09-17 11:52修改过]

可能眼花,没看到BOOK实体,只看到bookinfo值对象,还有一个BOOK是继承eventsource,如果它是实体,不应该和事件源直接耦合,因为事件不只是创建,还有其他涉及各种场景的不同事件,应该是角色直接和事件耦合。

事件驱动和service结合在一起,实体比如BOOK或图书馆这些数据模型是被操作,被强奸,而不是实体去指挥驱动事件和服务。真正ddd+event source是突出以聚合根为中心,你这些代码是以服务或事件为中心,只能是event sourcing系统,没有ddd, 这是我个人对你这些代码的认识。

好!非常期盼能看到banq写的针对这个例子的真正的DDD的实现,另外也非常希望能看到banq是如何使用JdonFramework的,以及如何让领域对象处于核心地位,非常期盼。相信大家也和我有同样的感觉,我现在只想用成套的模型设计来说事,总是理论一大堆,没有产出,不能信服于人。大家把自己认为好的模型设计拿出来,至于谁好,我相信后人看到自会公论的,呵呵。

2012-09-17 13:22 "@tangxuehua"的内容
非常希望能看到banq是如何使用Jdonframework的,以及如何让领域对象处于核心地位,非常期盼 ...

讨论需要理性,可不能赌气哦,没有谁高谁低,只是在分享讨论中大家共同进步。

jdonframework案例都是以DDD为核心,这里有一个现场案例Robot,案例开发说明见:
Robot案例开发说明
源码下载见
https://github.com/banq/jdonframework/tree/master/examples

等我有空再写这个图书馆案例,应该可以从Robot案例推导出图书馆案例怎么写。只是想抛砖引玉,不想流芳百世,让人验证考验,我做我想做的。呵呵。

那好吧!我实际也不是想和你赌气,而是希望能看到您的解决方案而已。
因为您总是说的很高深,而我的代码不是DDD,所以我非常期盼你能写出DDD+DCI+Event Sourcing+JdonFramework的例子出来。

我强调的是任何设计和代码都要“自然”,如果你写出来的确实是如此,大家自会信服的。

2012-09-17 13:41 "@tangxuehua"的内容
我强调的是任何设计和代码都要“自然”,如果你写出来的确实是如此,大家自会信服的 ...

呵呵,其实我追求的不是让大家信服,而是让你这样的高手让大家信服,这也是我非常欣赏你的作品,但是又如此犀利指出与我理想中的不足,如果一个比较好的思想不只是我一个人实现,还有众多你这样高手都能实践,这才能普及这样好的思想,才能让更多人受益。

这些好的思想很多不是我原创,我只是做了点落地工作,希望筑巢引凤,让中国人设计水平提高起来。届时也是我退休之时,呵呵。
[该贴被banq于2012-09-17 13:48修改过]

晚上看了banq提供的关于机器人的例子,该例子结合了DDD+DCI+Domain Event,但是没有用到Event Sourcing

要是我之前没学过DCI的话,估计一定看不懂,呵呵。

下面是我对该例子的一些理解:
1. 在DCI架构下,DDD中model可以非常瘦身,只需要有描述model自己的固有属性即可,这就是DCI中的data;
2. 场景相关的交互行为由role定义,通过roleManager.assign(model, someRole)来让对象动态赋予某个角色所赋予的职责,但是通过model.ActAs<SomeRole>()这样的写法不是更清楚吗?这样连roleManager都可以不需要;
3. model不是被保存,而是主动扮演角色通知别人保存自己;有意思的是你提到由于可能保存操作比较耗时,所以采用了异步的domain event来通过发送消息的方式来让repository保存model;
4. model进入场景(context),扮演相应角色,然后角色之间行驶交互逻辑;然后context有上层界面创建并启动context中的交互行为;

不知我的理解对不对?

下面是我的几个问题:
1. 如果要引入Event Sourcing,那ES应该是应用在model上还是role上?我想应该是model上吧?那如果model采用es,那model中的所有修改数据的行为就会产生相关事件吧,并且这些事件应该不是你robot机器人中的domain event,对吧?那你能解释一下event sourcing中的event 与domain event 的区别和联系吗?
2. DCI中的role会有与该角色相关的状态吗?如果有,那角色的状态如何持久化,如何重建,特别是在采用ES架构的情况下;
3. 场景的交互结果如何表示,也用model来表示吗?如果用model表示,那保存时也用你机器人例子中类似的方法吗?如果用了es,应该不能采用这样的方法吧。

ES显式是没有用,因为这需要巨大存储库,实际你只要把Domain Events作为事件源保存起来就实现ES了。具体参考jdon framework 为什么没有用Event Source 呢说明了。

推荐你看看这个javascript的Node.js应用案例:采用cqrsnode框架开发的DDD CQRS例子,也是围绕领域模型为核心,Javascript和Java等基本都能实现,相信.NET也可以实现。

简单回答一下你的几个问题:
1. 如果引入ES,当然是在Role上,如果是在Model上,不是和你现在代码一样,两种耦合在一起了?总结公式如下:

Data Model => Role/Context <=事件交互行为

2.角色与状态无关,状态是Model内部的字段或属性。

3.交互事件的结果也是落实在Model内部字段和属性上,持久化保存也属于模型扮演角色的一个功能,这个角色称为负责保存的角色,什么是角色?有职责的角色,什么叫有职责?就是应该做什么事情,比如你担任家长角色,就有负责家庭的职责,你担任公民角色,就应该负担公民的职责。

ES只是属于角色应该实施的一种交互行为,也属于DCI的I交互行为。

什么是事件?事件和行为方法有什么区别?
事件是涉及两个平行独立对象以上的行为,而方法行为没有这个规定,可以是对象自己的方法行为,也可以是调用其他对象的方法行为,后者称为交互行为,也称为发送事件或消息。

参考:
Eric Evans关于技术如何影响DDD的会话

[该贴被banq于2012-09-19 09:10修改过]

针对你的回复我有产生了另外一个问题,我问题总是比较多,呵呵。

ES是作用在role上,能在具体点吗?我的疑问是,如果domain event应该由role驱动发出并持久化,这点我没异议。那我们重建的应该是聚合根(DCI中的data)还是role呢?肯定不是role吧,因为role是无状态的;那就是聚合根了,但是奇怪的是聚合根根本没发出过事件,它有怎么会“认识”它之前所扮演的某个角色所发出来的事件呢?事件应该只有role才认识。当然,你会说,聚合根扮演角色后,实际上聚合根与角色是一个东西,不能割裂开来看。这个我赞同,但这里体现出了不对称性。即发出事件的是role,而利用事件重建的是聚合根;这里的根本问题是,我们重建对象时,重建的是聚合根,而不是“聚合根扮演某个角色后的东西”。

希望能清楚回答一下你对这个问题的理解!