领域驱动设计之我见

开这个新帖子的诱因,是《寻找答案之DDD》的发贴者要我写的图书管理系统的原型代码。 因为这个案例足够小,而且在学校,我们一般都有借书的经历,对图书管理系统所要表达的领域有充分的认识,可以判断设计出来的领域模型是否优雅,是否接近客观领域的本质。

《寻找答案之DDD》中引用了敏捷之道的张逸先生使用DDD方法设计的图书管理系统的领域模型,张逸先生是位验丰富的程序员,以前看过他的一篇文章《什么是好的代码》,里头有一个观点,好的代码中每个方法最好不要超过5行。这个量化的指标,要完成挺难的(完全做到可能也没有必要),我只能尽力而为。MVC和DCI的提出者Trygve Reenskaug提倡用可读性极佳的代码直接捕获用户心中的模型,我采纳了这个观点,同时在编写原型时借鉴了四色原型及DCI的部分思想。

这里先陈述我的一些思考和观点,可能与一些经典著的定义有所出入,随后再将代码贴出来。

1)领域是客观的,不以人的意志为转移,但人之于领域具有能动性,可以认识和改造它。
2)领域模型是主观的,体现了程序员对领域的认识,是程序员心中对领域的素描。
3)用户需求是主观的,体现了用户对领域的认识,是用户心中对领域的素描。
4)领域模型需要捕捉和包容用户需求。领域模型与用户需求的关系十分重要,下面展开来讲。

“用户需求”是对领域的“素描”,用户的需求来自对领域的“诉求”,这些诉求往往是深刻的,因为其来源于用户对领域长期观察和使用的经验,比起我们程序员,一般更完整、更真实地接近领域的本质。我们对“用户需求进行素描”,就是“借鉴用户的宝贵经验”,可以更快、更好地素描客观领域,这可以说是一条认识(未知)领域的捷径。但是当用户需求不明朗或不清晰时,我们需要超越“用户需求”,对领域进行深入的摸索,去寻求更清晰的视角,对领域进行刻画。

此外,“用户需求”不能等同于“用户”,捕捉“用户心中的模型”也不能等同于“以用户为核心模型”,这是不同的概念,不能忽略其差异。《老子》书中有个观点:有之以为利,无之以为用。在这里,有之利,即建立领域模型;无之用,即包容用户需求。举些例子,一个杯子要装满一杯水,我们在制作杯子时,制作的是空杯子,即要把水倒出来,之后才能装下水;再比如,一座房子要住人,我们在建造房子时,建造的房子是空的,唯有空的才能容乃人的居住。我们建立领域模型时也要将用户置于模型之外,这样才能包容用户的需求。

在图书馆管理系统的领域,借书人(Reader)即用户,建模时应该将其置之于外(借书人在系统中的镜像可以考虑放在应用层实现),借书卡(Card)是该领域一个重要的模型,借书人可以使用它进行借书。我的设计与张逸先生的设计差异很大,考虑到其具有丰富的经验,所以我不能断言我的设计是否更接近于领域,下面我将代码原型贴出来,因为只是用来示意和讨论的,所以有些实现做了简化。欢迎各路神仙进行评断、深入探讨,如果张逸先生本人也可以来到这里参与讨论,最好不过了。

推荐其博客,内容极其丰富。
http://www.agiledon.com/post/2010/06/107.html

// 书的一些详细信息,在四色原型中是DESC


package domain;

public class BookDetail {
private String author;
private String publisher;

public BookDetail(String author, String publisher) {
this.author = author;
this.publisher = publisher;
}

public String getAuthor() { return author; }
public String getPublisher() { return publisher; }

public void setAuthor(String author) { this.author = author; }
public void setPublisher(String publisher) { this.publisher = publisher; }
}

// 书本身,在四色原型中是PPT之Thing


package domain;

public class Book {
protected int count;
private String name;
private String serialNo;
private BookDetail detail;

public Book(int count, String name, String serialNo, BookDetail detail) {
this.count = count;
this.detail = detail;
this.name = name;
this.serialNo = serialNo;
}

public String getSerialNo() { return serialNo; }
public String getName() { return name; }
public int getCount() { return count; }
public BookDetail getDetail() { return detail; }

public void setSerialNo(String serialNo) { this.serialNo = serialNo; }
public void setName(String name) { this.name = name; }
public void setDetail(BookDetail detail) { this.detail = detail; }
public void setCount(int count) { this.count = count; }
}

// 所借之书,在四色原型中是Role


package domain;

import java.util.Date;

public class BorrowedBook {
private String username;
private Book book;
private Date borrowedTime;
private Date returnedTime;

public BorrowedBook(Book book) {
this.book = book;
this.borrowedTime = new Date();
this.returnedTime = new Date();
}

@Override
public String toString() {
return book.getName();
}

public String getUsername() { return username;}
public Book getBook() { return book; }
public Date getBorrowedTime() { return borrowedTime; }
public Date getReturnedTime() { return returnedTime; }

public void setUsername(String username) { this.username = username; }
public void setBook(Book book) { this.book = book; }
public void setBorrowedTime(Date borrowedTime) { this.borrowedTime = borrowedTime; }
public void setReturnedTime(Date returnedTime) { this.returnedTime = returnedTime; }
}

// 借书卡,在四色原型中是Role,也是PPT之Thing


package domain;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Card {
private String username;
private CardType cardType;
private List<BorrowedBook> borrowedBooks = Collections
.synchronizedList(new ArrayList<BorrowedBook>());

protected Card(String username) {
this.username = username;
}

public BorrowedBook borrowBook(Book book) {
BorrowedBook borrowedBook = new BorrowedBook(book);
borrowedBook.setUsername(username);
borrowedBooks.add(borrowedBook);
return borrowedBook;
}

public Book returnBorrowedBook(BorrowedBook borrowedBook) {
Book book = borrowedBook.getBook();
borrowedBooks.remove(borrowedBook);
return book;
}

public List<BorrowedBook> borrowBooks(List<Book> books) {
BorrowedBook borrowedBook;
for (Book book : books) {
borrowedBook = new BorrowedBook(book);
borrowedBook.setUsername(username);
borrowedBooks.add(borrowedBook);
}
return borrowedBooks;
}

public List<Book> returnBorrowedBooks(List<BorrowedBook> borrowedBooks) {
List<Book> books = null;
if (this.borrowedBooks.removeAll(borrowedBooks)) {
books = new ArrayList<Book>();
for (BorrowedBook borrowedBook : borrowedBooks) {
books.add(borrowedBook.getBook());
}
}
return books;
}

public void showInfo() {
StringBuffer buf = new StringBuffer();
buf.append(username + " borrows:");

if (borrowedBooks.isEmpty()) {
buf.append(
" nothing.");
System.out.println(buf.toString());
return;
}
for (BorrowedBook borrowedBook : borrowedBooks) {
buf.append(
" " + borrowedBook.getBook().getName());
}
System.out.println(buf.toString()+
".");
}

public String getUsername() { return username; }
public CardType getCardType() { return cardType; }
public List<BorrowedBook> getBorrowedBooks() { return borrowedBooks; }

public void setUsername(String username) { this.username = username; }
public void setCardType(CardType cardType) { this.cardType = cardType; }
public void setBorrowedBooks(List<BorrowedBook> borrowedBooks) { this.borrowedBooks = borrowedBooks; }
}

// 借书卡的类型,在四色原型中是DESC


package domain;

public enum CardType {
TEARCHER, STUDENT;
}

// 创建借书卡,保证其唯一性。


package domain;

import java.util.HashMap;
import java.util.Map;

public class CardFactory {
private static Map<String, Card> cards = new HashMap<String, Card>();

private CardFactory() {
}

public static synchronized Card getCard(String username) {
if (!cards.containsKey(username)) {
cards.put(username, new Card(username));
}
return cards.get(username);
}
}

// 图书馆,在四色原型中是Role,也是PPT之place


package domain;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class Library {
public static Map<String, Book> books = Collections
.synchronizedMap(new HashMap<String, Book>());

static {
books.put("Jdon Framework", new Book(10, "Jdon Framework", "001",
new BookDetail(
"Banq", "Jdon")));
books.put(
"Spring Framework", new Book(10, "Spring Framework", "002",
new BookDetail(
"Rod Jonhson", "Spring")));
books.put(
"Guice Framework", new Book(10, "Guice Framework", "003",
new BookDetail(
"Bob Lee", "Guice")));
}

public static Map<String, Book> getBooks() {
return books;
}

public static void setBooks(Map<String, Book> books) {
Library.books = books;
}

public static Book takeBook(String name) {
if (!hasStock(name)) return null;
books.get(name).count--;
return books.get(name);
}

public static void putBook(Book book) {
if (!hasStock(book)) {
books.put(book.getName(), book);
}
books.get(book.getName()).count++;
}

public static boolean hasStock(String bookName) {
return getCount(bookName) > 0 ? true : false;
}
public static boolean hasStock(Book book) {
return hasStock(book.getName());
}
public static int getCount(String bookName) {
return books.get(bookName).count;
}
public static int getCount(Book book) {
return getCount(book.getName());
}

public static void showInfo() {
StringBuffer buf = new StringBuffer();
buf.append(
"The library has:");
if (Library.getBooks().isEmpty()) {
buf.append(
" nothing.");
System.out.println(buf.toString());
return;
}
for (Book book : Library.getBooks().values()) {
buf.append(
" " + book.getName() + "(" +book.getCount()+ ")");
}
System.out.println(buf.toString()+
".");
}
}

// 持卡借书, 在四色原型中是MI


package domain;

import java.util.ArrayList;
import java.util.List;

public class BorrowBook {
public List<BorrowedBook> borrowBooks(Card card, List<String> bookNames) {
if (bookNames.isEmpty()) return null;

List<BorrowedBook> borrowedBooks = new ArrayList<BorrowedBook>();
for (String bookName : bookNames) {
borrowedBooks.add(borrowBook(card, bookName));
}
return borrowedBooks;
}

public BorrowedBook borrowBook(Card card, String bookName) {
if (!BorrowingTerms.canBorrowBook(card, bookName)) return null;
Book book = Library.takeBook(bookName);
return card.borrowBook(book);
}
}

// 无卡还书,在四色原型中是MI。
// 注意其命名为BorrowedBookReturn,而不是ReturnBorrowedBook
// 这样做是为了在视觉上将这些类名摆放在一起


package domain;

import java.util.List;

public class BorrowedBookReturn {

public void returnBorrowedBooks(List<BorrowedBook> borrowedBooks) {
if (borrowedBooks.isEmpty())
return;
for (BorrowedBook borrowedBook : borrowedBooks) {
returnBorrowedBook(borrowedBook);
}
}

public void returnBorrowedBook(BorrowedBook borrowedBook) {
Card card = CardFactory.getCard(borrowedBook.getUsername());
Book book = card.returnBorrowedBook(borrowedBook);
Library.putBook(book);
}
}

// 这是借书条款,在四色原型中是Role,也可以作为MI。
// 这里实现得比较集中,是看作MI,可进行细化分散设计。


package domain;

import java.util.List;

public class BorrowingTerms {
private static final int limitOfBooks = 5;
private static final long limitOfDays = 30;
private static final int limitOfStock = 1;
private static final double unitOfPenalty = 0.1;

// 卡是否可以借书
public static boolean checkCard(Card card) {
int borrowedBooks;
borrowedBooks = card.getBorrowedBooks().size();
return (borrowedBooks < limitOfBooks) ? true : false;
}

// 书是否超期
public static boolean checkBorrowedBook(BorrowedBook borrowedBook) {
return (exceededDays(borrowedBook) == 0) ? true : false;
}

// 图书馆是否还有额外的库存
public static boolean checkLibrary(String bookName) {
int stockBooks = Library.getCount(bookName);
return (stockBooks > limitOfStock) ? true : false;
}

// 超期的天数
public static long exceededDays(BorrowedBook borrowedBook) {
long borrowedDays, exceededDays;
borrowedDays = (borrowedBook.getReturnedTime().getTime() - borrowedBook
.getBorrowedTime().getTime())
/ (24 * 60 * 60 * 1000);
exceededDays = (borrowedDays - limitOfDays);
return (exceededDays > 0) ? exceededDays : 0;
}

// 一本书超期的罚款金额,1本书1天0.1元
public static double getPenalty(BorrowedBook borrowedBook) {
return exceededDays(borrowedBook) * unitOfPenalty;
}

// 多本书超期的罚款金额
public double getPenalty(List<BorrowedBook> borrowedBooks) {
double penalty = 0;
for (BorrowedBook borrowedBook : borrowedBooks) {
if (BorrowingTerms.checkBorrowedBook(borrowedBook)) {
penalty += BorrowingTerms.getPenalty(borrowedBook);
}
}
return penalty;
}
// 是否可以借书,可定制
public static boolean canBorrowBook(Card card, String bookName) {
if (!BorrowingTerms.checkCard(card))
return false;
if (!BorrowingTerms.checkLibrary(bookName))
return false;
for (BorrowedBook borrowedBook : card.getBorrowedBooks()) {
if (!BorrowingTerms.checkBorrowedBook(borrowedBook))
return false;
}
return true;
}
}

[该贴被jdon007于2011-01-30 18:25修改过]

// 借阅者,在领域之外,可以考虑在应用层做一个镜像。
// 这里只是示范作用,出于简单,没有使用借书条款。
// 附件是源码


package application;

import java.util.ArrayList;
import java.util.List;

import domain.BorrowBook;
import domain.BorrowedBook;
import domain.BorrowedBookReturn;
import domain.Card;
import domain.CardFactory;
import domain.Library;

public class Reader {
public static void main(String[] args) {
// 0 图书馆里找书
List<String> bookNames = new ArrayList<String>();
bookNames.add(
"Jdon Framework");
bookNames.add(
"Spring Framework");
bookNames.add(
"Guice Framework");

// 1 持卡借书
Card bill = CardFactory.getCard(
"Bill Gates");
// 1.1) 借书之前
System.out.println(
"0) Before Borrowing Books");
bill.showInfo();
Library.showInfo();
// 1.2)借书
BorrowBook borrowService = new BorrowBook();
List<BorrowedBook> borrowedBooks = borrowService.borrowBooks(bill, bookNames);
// 1.2)借书之后
System.out.println(
"1) After Borrowing Books");
bill.showInfo();
Library.showInfo();

// 2 阅读借来的书,读完时就该还书了。
System.out.println(
"\n" + bill.getUsername() + " is reading books!" + "\n" );

// 3 无卡还书
// 3.1) 还书之前
System.out.println(
"3) Before Returning Books");
bill.showInfo();
Library.showInfo();
// 3.2) 还书
// 对每一本书设定还书时间,如果要检查是否超期或保存借书历史的话。
for (BorrowedBook borrowedBook : borrowedBooks) {
borrowedBook.setReturnedTime(new Date());
}
BorrowedBookReturn returnService = new BorrowedBookReturn();
returnService.returnBorrowedBooks(borrowedBooks);
// 3.3) 还书之后
System.out.println(
"4) After Returning Books");
bill.showInfo();
Library.showInfo();
}
}

[该贴被jdon007于2011-01-29 21:15修改过]
attachment:

domain.rar

代码运行结果:


0) Before Borrowing Books
Bill Gates borrows: nothing.
The library has: Jdon Framework(10) Spring Framework(10) Guice Framework(10).
1) After Borrowing Books
Bill Gates borrows: Jdon Framework Spring Framework Guice Framework.
The library has: Jdon Framework(9) Spring Framework(9) Guice Framework(9).

Bill Gates is reading books!

3) Before Returning Books
Bill Gates borrows: Jdon Framework Spring Framework Guice Framework.
The library has: Jdon Framework(9) Spring Framework(9) Guice Framework(9).
4) After Returning Books
Bill Gates borrows: nothing.
The library has: Jdon Framework(10) Spring Framework(10) Guice Framework(10).


其中,在CardFactory可以设置Card的类型,然后BorrowingTerms可以根据Card的类型制定更详细的借书条款。Reader可以考虑在领域应用层提供,作为用户的镜像,用来“激活”各种活动或场景。

之后,我们可以考虑要持久化些什么,比如图书Book,可用来查看有哪些书可以借;也可以持久化BorrowedBook,可用来查看每个人的借书史。当然,查看这些信息要提供相应的界面,即界面设计。

呵呵,很好的例子,其实我觉得关键是对reader和card的理解,我的理解为:card并不是借书还书,而是一种认证(登录),而真正的借书的是Reader。刷卡借书是两个动作:认证,接着借书(当然其中隐含着登出)。你试着把Card“类名”换成Reader,把Card作为应用层的登录,会发现是一样不一样的理解——只是把Card和Reader互换了而已,在我看来Card只是Reader的一样镜像,见卡如见人。试想若果不用再刷卡了,而是通过DNA来作为唯一识别(假设),那么难道是DNA在借书了?我认为无论卡,还是DNA都是认证而已,与借书还书无关。

DDD一书还有一些词需要磨磨,如“核心领域(的模型)”,还有“核心内”和“核心外”。
[该贴被SpeedVan于2011-01-30 01:59修改过]

1、借书人(Reader)与借书卡(Card)不是镜像,借书人(Reader)是借书卡(Card)的使用者。

2、认证是认证,跟借书卡没有关系。在这里相当于借书人有没有资格拿到卡,也就是如果你不是这个学校的的师生,就拿不到这个学校图书馆的借书卡。

3、借书卡之于借书人,相当于信箱之于收信人,这是不同的概念。

4、可以考虑在应用层做一个镜像,也就是借书人本身的镜像,对其“借书资格”进行审查,此时可以建立一个Accout类作为其在系统中的镜像,至于你如何审查这个Accout, 如指纹识别,身份证识别,手机号码识别,学生证识别,DNA识别,这个都是对Accout(即Reader)借书资格的审查,是该领域核心之外的东西。

5、借书卡,是一个借书的工具,就像信箱,是一个收发邮件的工具。用户User在系统中的镜像Accout,与这并无直接的联系。对“用户进入领域的资格”与“领域本身”是相对独立的。路人甲的房子与房子的房产证(路人甲的镜像)是不同的,日后,房产证可能丢失或转移到路人乙手里,但房子依旧,领域依旧。

6、你一直强调用户在领域模型中存在镜像,但是“建立模型时把用户至于领域之中”与“用户使用领域模型时进入领域之中”是大有不同的,而不是形式上的差异(说法不同而已)。

7、如果领域模型将包含用户的镜像,以人以自我为中心的潜意识和习惯,几乎必然以Reader为核心模型,一切都围绕着Reader转,很难客观地描述领域,比如张逸先生将canBorrowBook的方法放在Reader之中,可是能不能借书不是Reader说了算,是BorrowingTerms规定的。

8、上面的这些解释“看得见”,只是一些具体的描述,“看不见”的“无之以为用”的思想更值得好好领会和运用。毕竟,“看得见”的东西是有形和有限的(直观和感性非常重要,但不能止步于此,虽然我们很多时候也只能止步于此,想再往前走几乎无路可走,因为超出经验的认知之路是极其艰难的),如果换了一种形式,可能就“看不见”了,只有“看见”“看不见”的东西,以后再遇到这种情景,不管其以任何表象出现在我们面前,我们都可能看到真相,不再拘泥于表象或受表象之惑。当然,这种悟性的成长要长期的坚持和努力。

9、这些只是我个人的想法,正确与否,还需要时间和实践的检验。大家如果有不同看法,可以畅所欲言。

1、你认为是使用者,而我认为这是登录方式,使用卡和使用DNA是同一样东西,方式不同而已。一个是卡码,一个是DNA密码,都是唯一的。“借书人(Reader)是借书卡(Card)的使用者”,这里的本质是,我们不是使用卡借书,而是使用卡登录,只是因为行为单一,所以可以把登录和借书合并而已。试想,难道罚款,越境登记,这些是一个系统的话,那么这些都不是人在做事,而是身份证?

2、借书卡是申请而来,不是借书时才确定的,而已一开始就已经在现实判断有没有拥有权——相关手续后得卡,这里相当于注册。如果不是这个学校的老师,那么是在申请时,不能得到卡,而不是借书时不能得到。

3、没有卡就不能用DNA来借书?没有信箱,不能用其他来收信?

4、若果把Reader放到应用层,这是绝对的错误,请注意,account其实是混合了passport和user的概念,所以会被迷惑,登录属应用层,这是对passport而言的,但在领域中是user,我之前就说过帐号和用户的区别。

5、我根本观点是,它本身并不是为借书而存在的,是登录,是因为行为只有借书,所以可以一并启动借书而已,但这当中是两步过程,不是一步。(也可以认为是三步,借书完成后,直接登出)。

6、请你看看DDD中Cargo对Customer的说法。注意:关键一词——实体。

7、注意,是不是你没有考虑过第一人称和第三人称的思维的区别。自我为中心是第一人称思维,那么也请你用一下第三人称思维来思考,把Reader当作实体来看待。“张逸先生将canBorrowBook的方法放在Reader之中”,这里为啥写成canBorrowBook,若果是borrowBook(借书)行为的话,这行为正是那个实体的行为,有何不妥?就如登录了的user能够发表帖子一样。

8、把刷卡只看作借书,这不表象?

若果排除人的话,Cargo中的customer早应去掉了,为啥还存在呢?customer也是聚合根,只是没有其他东西内聚进去而已。我感觉你把领域的核心模型——核心领域模型,理解为领域模型,这两者是不同的,DDD一书中“精炼”一章有说明。Customer最后被归为模型中的能力层里。

其实我所说的Reader和你们所说的Card和Account极为相似的,可以说地位很相似,迟点我也放上我根据我理解的代码。
[该贴被SpeedVan于2011-01-31 01:15修改过]

晕,重复发了,删此回复
[该贴被SpeedVan于2011-01-31 01:13修改过]

呵呵,谢谢楼主,终于有点理解四色原型可。
怪不得有人说MI就是DCI的场景呢。
这个场景有两个一是借书,二是还书。然后书以被借的角色,卡以借的角色参与到借书和还书的场景中来,至于BorrowsItem是借书的业务规则限制显示的提出来。