1. 程式人生 > >一個簡單例子:貧血模型or領域模型

一個簡單例子:貧血模型or領域模型

來源於:http://www.iteye.com/topic/283668

最近taowen同學連續發起了兩起關於貧血模型和領域模型的討論,引起了大家的廣泛熱烈的討論,但是討論(或者說是爭論)的結果到底怎樣,我想值得商榷。問題是大家對貧血模型和領域模型都有自己的看法,如果沒有對此達到概念上的共識,那麼討論的結果應該可想而知,討論的收穫也是有的,至少知道了分歧的存在。為了使問題具有確定性,我想從一個簡單例子著手,用我對貧血模型和領域模型的概念來分別實現例子。至於我的理解對與否,大家可以做評判,至少有個可以評判的標準在這。

一個例子


我要舉的是一個銀行轉帳的例子,又是一個被用濫了的例子。但即使這個例子也不是自己想出來的,而是剽竊的<<POJOs in Action>>中的例子,原諒我可憐的想像力

。當錢從一個帳戶轉到另一個帳戶時,轉帳的金額不能超過第一個帳戶的存款餘額,餘額總數不能變,錢只是從一個賬戶流向另一個帳戶,因此它們必須在一個事務內完成,每次事務成功完成都要記錄此次轉帳事務,這是所有的規則。

貧血模型


我們首先用貧血模型來實現。所謂貧血模型就是模型物件之間存在完整的關聯(可能存在多餘的關聯),但是物件除了get和set方外外幾乎就沒有其它的方法,整個物件充當的就是一個數據容器,用C語言的話來說就是一個結構體,所有的業務方法都在一個無狀態的Service類中實現,Service類僅僅包含一些行為。這是Java Web程式採用的最常用開發模型,你可能採用的就是這種方法,雖然可能不知道它有個&ldquo;貧血模型&rdquo;的稱號,這要多虧Martin Flower(這個傢伙慣會發明術語!)。

包結構


在討論具體的實現之前,我們先來看來貧血模型的包結構,以便對此有個大概的瞭解。
 

貧血模型的實現一般包括如下包:

  • dao:負責持久化邏輯
  • model:包含資料物件,是service操縱的物件
  • service:放置所有的服務類,其中包含了所有的業務邏輯
  • facade:提供對UI層訪問的入口

程式碼實現


先看model包的兩個類,Account和TransferTransaction物件,分別代表帳戶和一次轉賬事務。由於它們不包含業務邏輯,就是一個普通的Java Bean,下面的程式碼省略了get和set方法。

Java程式碼 複製程式碼 收藏程式碼
  1. publicclass Account {   
  2. private String accountId;   
  3. private BigDecimal balance;   
  4. public Account() {}   
  5. public Account(String accountId, BigDecimal balance) {   
  6. this.accountId = accountId;   
  7. this.balance = balance;   
  8.     }   
  9. // getter and setter ....
  10. }  
public class Account {
	private String accountId;
	private BigDecimal balance;

	public Account() {}
	public Account(String accountId, BigDecimal balance) {
		this.accountId = accountId;
		this.balance = balance;
	}
	// getter and setter ....

}
Java程式碼 複製程式碼 收藏程式碼
  1. publicclass TransferTransaction {   
  2. private Date timestamp;   
  3. private String fromAccountId;   
  4. private String toAccountId;   
  5. private BigDecimal amount;     
  6. public TransferTransaction() {}   
  7. public TransferTransaction(String fromAccountId, String toAccountId, BigDecimal amount, Date timestamp) {   
  8. this.fromAccountId = fromAccountId;   
  9. this.toAccountId = toAccountId;   
  10. this.amount = amount;   
  11. this.timestamp = timestamp;   
  12.     }   
  13. // getter and setter ....
  14. }  
public class TransferTransaction {
	private Date timestamp;
	private String fromAccountId;
	private String toAccountId;
	private BigDecimal amount;	

	public TransferTransaction() {}

	public TransferTransaction(String fromAccountId, String toAccountId, BigDecimal amount, Date timestamp) {
		this.fromAccountId = fromAccountId;
		this.toAccountId = toAccountId;
		this.amount = amount;
		this.timestamp = timestamp;
	}

	// getter and setter ....
}


這兩個類沒什麼可說的,它們就是一些資料容器。接下來看service包中TransferService介面和它的實現TransferServiceImpl。TransferService定義了轉賬服務的介面,TransferServiceImpl則提供了轉賬服務的實現。

Java程式碼 複製程式碼 收藏程式碼
  1. publicinterface TransferService {   
  2.     TransferTransaction transfer(String fromAccountId, String toAccountId, BigDecimal amount)    
  3. throws AccountNotExistedException, AccountUnderflowException;   
  4. }  
public interface TransferService {
	TransferTransaction transfer(String fromAccountId, String toAccountId, BigDecimal amount) 
			throws AccountNotExistedException, AccountUnderflowException;
}
Java程式碼 複製程式碼 收藏程式碼
  1. publicclass TransferServiceImpl implements TransferService {   
  2. private AccountDAO accountDAO;   
  3. private TransferTransactionDAO transferTransactionDAO;   
  4. public TransferServiceImpl(AccountDAO accountDAO,    
  5.             TransferTransactionDAO transferTransactionDAO) {   
  6. this.accountDAO = accountDAO;   
  7. this.transferTransactionDAO = transferTransactionDAO;   
  8.     }   
  9. public TransferTransaction transfer(String fromAccountId, String toAccountId,   
  10.             BigDecimal amount) throws AccountNotExistedException, AccountUnderflowException {      
  11.     Validate.isTrue(amount.compareTo(BigDecimal.ZERO) > 0);         
  12.         Account fromAccount = accountDAO.findAccount(fromAccountId);   
  13. if (fromAccount == nullthrownew AccountNotExistedException(fromAccountId);   
  14. if (fromAccount.getBalance().compareTo(amount) < 0) {   
  15. thrownew AccountUnderflowException(fromAccount, amount);   
  16.         }          
  17.         Account toAccount = accountDAO.findAccount(toAccountId);   
  18. if (toAccount == nullthrownew AccountNotExistedException(toAccountId);   
  19.         fromAccount.setBalance(fromAccount.getBalance().subtract(amount));   
  20.         toAccount.setBalance(toAccount.getBalance().add(amount));                  
  21.         accountDAO.updateAccount(fromAccount);      // 對Hibernate來說這不是必須的
  22.         accountDAO.updateAccount(toAccount);        // 對Hibernate來說這不是必須的
  23. return transferTransactionDAO.create(fromAccountId, toAccountId, amount);   
  24.     }   
  25. }  
public class TransferServiceImpl implements TransferService {
	private AccountDAO accountDAO;
	private TransferTransactionDAO transferTransactionDAO;

	public TransferServiceImpl(AccountDAO accountDAO, 
			TransferTransactionDAO transferTransactionDAO) {
		this.accountDAO = accountDAO;
		this.transferTransactionDAO = transferTransactionDAO;

	}

	public TransferTransaction transfer(String fromAccountId, String toAccountId,
			BigDecimal amount) throws AccountNotExistedException, AccountUnderflowException {	
	Validate.isTrue(amount.compareTo(BigDecimal.ZERO) > 0);		

		Account fromAccount = accountDAO.findAccount(fromAccountId);
		if (fromAccount == null) throw new AccountNotExistedException(fromAccountId);
		if (fromAccount.getBalance().compareTo(amount) < 0) {
			throw new AccountUnderflowException(fromAccount, amount);
		}		

		Account toAccount = accountDAO.findAccount(toAccountId);
		if (toAccount == null) throw new AccountNotExistedException(toAccountId);
		fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
		toAccount.setBalance(toAccount.getBalance().add(amount));				

		accountDAO.updateAccount(fromAccount);		// 對Hibernate來說這不是必須的
		accountDAO.updateAccount(toAccount);		// 對Hibernate來說這不是必須的
		return transferTransactionDAO.create(fromAccountId, toAccountId, amount);
	}
}


TransferServiceImpl類使用了AccountDAO和TranferTransactionDAO,它的transfer方法負責整個轉帳操作,它首先判斷轉帳的金額必須大於0,然後判斷fromAccountId和toAccountId是一個存在的Account的accountId,如果不存在拋AccountNotExsitedException。接著判斷轉帳的金額是否大於fromAccount的餘額,如果是則拋AccountUnderflowException。接著分別呼叫fromAccount和toAccount的setBalance來更新它們的餘額。最後儲存到資料庫並記錄交易。TransferServiceImpl負責所有的業務邏輯,驗證是否超額提取並更新帳戶餘額。一切並不複雜,對於這個例子來說,貧血模型工作得非常好!這是因為這個例子相當簡單,業務邏輯也不復雜,一旦業務邏輯變得複雜,TransferServiceImpl就會膨脹。

優缺點

貧血模型的優點是很明顯的:

  1. 被許多程式設計師所掌握,許多教材採用的是這種模型,對於初學者,這種模型很自然,甚至被很多人認為是java中最正統的模型。
  2. 它非常簡單,對於並不複雜的業務(轉帳業務),它工作得很好,開發起來非常迅速。它似乎也不需要對領域的充分了解,只要給出要實現功能的每一個步驟,就能實現它。
  3. 事務邊界相當清楚,一般來說service的每個方法都可以看成一個事務,因為通常Service的每個方法對應著一個用例。(在這個例子中我使用了facade作為事務邊界,後面我要講這個是多餘的)


其缺點為也是很明顯的:

  1. 所有的業務都在service中處理,當業越來越複雜時,service會變得越來越龐大,最終難以理解和維護。
  2. 將所有的業務放在無狀態的service中實際上是一個過程化的設計,它在組織複雜的業務存在天然的劣勢,隨著業務的複雜,業務會在service中多個方法間重複。
  3. 當新增一個新的UI時,很多業務邏輯得重新寫。例如,當要提供Web Service的介面時,原先為Web介面提供的service就很難重用,導致重複的業務邏輯(在貧血模型的分層圖中可以看得更清楚),如何保持業務邏輯一致是很大的挑戰。


領域模型


接下來看看領域驅動模型,與貧血模型相反,領域模型要承擔關鍵業務邏輯,業務邏輯在多個領域物件之間分配,而Service只是完成一些不適合放在模型中的業務邏輯,它是非常薄的一層,它指揮多個模型物件來完成業務功能。

包結構

領域模型的實現一般包含如下包:

  • infrastructure: 代表基礎設施層,一般負責物件的持久化。
  • domain:代表領域層。domain包中包括兩個子包,分別是model和service。model中包含模型物件,Repository(DAO)介面。它負責關鍵業務邏輯。service包為一系列的領域服務,之所以需要service,按照DDD的觀點,是因為領域中的某些概念本質是一些行為,並且不便放入某個模型物件中。比如轉帳操作,它是一個行為,並且它涉及三個物件,fromAccount,toAccount和TransferTransaction,將它放入任一個物件中都不好。
  • application: 代表應用層,它的主要提供對UI層的統一訪問介面,並作為事務界限。


程式碼實現

現在來看實現,照例先看model中的物件:

Java程式碼 複製程式碼 收藏程式碼
  1. publicclass Account {   
  2. private String accountId;   
  3. private BigDecimal balance;   
  4. private OverdraftPolicy overdraftPolicy = NoOverdraftPolicy.INSTANCE;   
  5. public Account() {}   
  6. public Account(String accountId, BigDecimal balance) {   
  7.         Validate.notEmpty(accountId);   
  8.         Validate.isTrue(balance == null || balance.compareTo(BigDecimal.ZERO) >= 0);   
  9. this.accountId = accountId;   
  10. this.balance = balance == null ? BigDecimal.ZERO : balance;   
  11.     }   
  12. public String getAccountId() {   
  13. return accountId;   
  14.     }   
  15. public BigDecimal getBalance() {   
  16. return balance;   
  17.     }   
  18. publicvoid debit(BigDecimal amount) throws AccountUnderflowException {   
  19.         Validate.isTrue(amount.compareTo(BigDecimal.ZERO) > 0);   
  20. if (!overdraftPolicy.isAllowed(this, amount)) {   
  21. thrownew AccountUnderflowException(this, amount);   
  22.         }   
  23.         balance = balance.subtract(amount);   
  24.     }   
  25. publicvoid credit(BigDecimal amount) {   
  26.         Validate.isTrue(amount.compareTo(BigDecimal.ZERO) > 0);   
  27.         balance = balance.add(amount);   
  28.     }   
  29. }  
public class Account {
	private String accountId;
	private BigDecimal balance;
	
	private OverdraftPolicy overdraftPolicy = NoOverdraftPolicy.INSTANCE;
	
	public Account() {}
	
	public Account(String accountId, BigDecimal balance) {
		Validate.notEmpty(accountId);
		Validate.isTrue(balance == null || balance.compareTo(BigDecimal.ZERO) >= 0);
		
		this.accountId = accountId;
		this.balance = balance == null ? BigDecimal.ZERO : balance;
	}
	
	public String getAccountId() {
		return accountId;
	}

	public BigDecimal getBalance() {
		return balance;
	}
	
	public void debit(BigDecimal amount) throws AccountUnderflowException {
		Validate.isTrue(amount.compareTo(BigDecimal.ZERO) > 0);
		
		if (!overdraftPolicy.isAllowed(this, amount)) {
			throw new AccountUnderflowException(this, amount);
		}
		balance = balance.subtract(amount);
	}
	
	public void credit(BigDecimal amount) {
		Validate.isTrue(amount.compareTo(BigDecimal.ZERO) > 0);
		
		balance = balance.add(amount);
	}
	
}


與貧血模型的區別在於Account類中包含業務方法(credit,debit),注意沒有set方法,對Account的更新是通過業務方法來更新的。由於“不允許從帳戶取出大於存款餘額的資金”是一條重要規則,將它放在一個單獨的介面OverdraftPolicy中,也提供了靈活性,當業務規則變化時,只需要改變這個實現就可以了。

TransferServiceImpl類:

Java程式碼 複製程式碼 收藏程式碼
  1. publicclass TransferServiceImpl implements TransferService {   
  2. private AccountRepository accountRepository;   
  3. private TransferTransactionRepository transferTransactionRepository;   
  4. public TransferServiceImpl(AccountRepository accountRepository,    
  5.             TransferTransactionRepository transferTransactionRepository) {   
  6. this.accountRepository = accountRepository;   
  7. this.transferTransactionRepository = transferTransactionRepository;   
  8.     }   
  9. public TransferTransaction transfer(String fromAccountId, String toAccountId,   
  10.             BigDecimal amount) throws AccountNotExistedException, AccountUnderflowException {   
  11.         Account fromAccount = accountRepository.findAccount(fromAccountId);   
  12. if (fromAccount == nullthrownew AccountNotExistedException(fromAccountId);   
  13.         Account toAccount = accountRepository.findAccount(toAccountId);   
  14. if (toAccount == nullthrownew AccountNotExistedException(toAccountId);   
  15.         fromAccount.debit(amount);   
  16.         toAccount.credit(amount);   
  17.         accountRepository.updateAccount(fromAccount);   // 對Hibernate來說這不是必須的
  18.         accountRepository.updateAccount(toAccount);     // 對Hibernate來說這不是必須的
  19. return transferTransactionRepository.create(fromAccountId, toAccountId, amount);   
  20.     }   
  21. }  
public class TransferServiceImpl implements TransferService {
	private AccountRepository accountRepository;
	private TransferTransactionRepository transferTransactionRepository;
	
	public TransferServiceImpl(AccountRepository accountRepository, 
			TransferTransactionRepository transferTransactionRepository) {
		this.accountRepository = accountRepository;
		this.transferTransactionRepository = transferTransactionRepository;
	}
	
	public TransferTransaction transfer(String fromAccountId, String toAccountId,
			BigDecimal amount) throws AccountNotExistedException, AccountUnderflowException {
		Account fromAccount = accountRepository.findAccount(fromAccountId);
		if (fromAccount == null) throw new AccountNotExistedException(fromAccountId);
		Account toAccount = accountRepository.findAccount(toAccountId);
		if (toAccount == null) throw new AccountNotExistedException(toAccountId);

		fromAccount.debit(amount);
		toAccount.credit(amount);
		
		accountRepository.updateAccount(fromAccount);	// 對Hibernate來說這不是必須的
		accountRepository.updateAccount(toAccount);		// 對Hibernate來說這不是必須的
		return transferTransactionRepository.create(fromAccountId, toAccountId, amount);
	}
	
}

與貧血模型中的TransferServiceImpl相比,最主要的改變在於業務邏輯被移走了,由Account類來實現。對於這樣一個簡單的例子,領域模型沒有太多優勢,但是仍然可以看到程式碼的實現要簡單一些。當業務變得複雜之後,領域模型的優勢就體現出來了。

優缺點

其優點是:

  1. 領域模型採用OO設計,通過將職責分配到相應的模型物件或Service,可以很好的組織業務邏輯,當業務變得複雜時,領域模型顯出巨大的優勢。
  2. 當需要多個UI介面時,領域模型可以重用,並且業務邏輯只在領域層中出現,這使得很容易對多個UI介面保持業務邏輯的一致(從領域模型的分層圖可以看得更清楚)。

其缺點是:

  1. 對程式設計師的要求較高,初學者對這種將職責分配到多個協作物件中的方式感到極不適應。
  2. 領域驅動建模要求對領域模型完整而透徹的瞭解,只給出一個用例的實現步驟是無法得到領域模型的,這需要和領域專家的充分討論。錯誤的領域模型對專案的危害非常之大,而實現一個好的領域模型非常困難。
  3. 對於簡單的軟體,使用領域模型,顯得有些殺雞用牛刀了。

我的看法

這部分我將提出一些可能存在爭議的問題並提出自己的看法。

軟體分層


理解軟體分層、明晰每層的職責對於理解領域模型以及程式碼實現是有好處的。軟體一般分為四層,分別為表示層,應用層,領域層和基礎設施層。軟體領域中另外一個著名的分層是TCP/IP分層,分為應用層,運輸層,網際層和網路介面層。我發現它們之間存在對應關係,見下表:

TCP/IP分層 軟體分層
表示層 負責向用戶顯示資訊。
應用層 負責處理特定的應用程式細節。如FTP,SMTP等協議。 應用層 定義軟體可以完成的工作,指揮領域層的物件來解決問題。它不負責業務邏輯,是很薄的一層。
運輸層 兩臺主機上的應用程式提供端到端的通訊。主要包括TCP,UDP協議。 領域層 負責業務邏輯,是業務軟體的核心。
網際層 處理分組在網路中的活動,例如分組的選路。主要包括IP協議。
網路介面層 作業系統中的裝置驅動程式和計算機中對應的網路介面卡。它們一起處理與電纜(或其他任何傳輸媒介)的物理介面細節。 基礎設施層 為上層提供通用技術能力,如訊息傳送,資料持久化等。

對於TCP/IP來說,運輸層和網際層是最核心的,這也是TCP/IP名字的由來,就像領域層也是軟體最核心的一層。可以看出領域模型的包結構與軟體分層是一致的。在軟體分層中,表示層、領域層和基礎設施層都容易理解,難理解的是應用層,很容易和領域層中Service混淆。領域Service屬於領域層,它需要承擔部分業務概念,並且這個業務概念不易放入模型物件中。應用層服務不承擔任何業務邏輯和業務概念,它只是呼叫領域層中的物件(服務和模型)來完成自己的功能。應用層為表示層提供介面,當UI介面改變一般也會導致應用層介面改變,也可能當UI介面很相似時應用層介面不用改變,但是領域層(包括領域服務)不能變動。例如一個應用同時提供Web介面和Web Service介面時,兩者的應用層介面一般不同,這是因為Web Service的介面一般要粗一些。可以和TCP/IP的層模型進行類比,開發一個FTP程式和MSN聊天程式,它們的應用層不同,但是可以同樣利用TCP/IP協議,TCP/IP協議不用變。與軟體分層不同的是,當同樣開發一個FTP程式時,如果只是UI介面不同,一個是命令列程式,一個是圖形介面,應用層不用變(利用的都是FTP服務)。下圖給出領域模型中的分層:

Repository介面屬於領域層

可能有人會將Repository介面,相當於貧血模型中的DAO介面,歸於基礎設施層,畢竟在貧血模型中DAO是和它的實現放在一起。這就涉及Repository 介面到底和誰比較密切?應該和domain層比較密切,因為Repository介面是由domain層來定義的。用TCP/IP來類比,網際層支援標準乙太網、令牌環等網路介面,支援介面是在網際層中定義的,沒有在網際層定義的網路介面是不能被網際層訪問的。那麼為什麼在貧血模型中DAO的介面沒有放在model包中,這是因為貧血模型中DAO的介面是由service來定義的,但是為什麼DAO介面也沒有放在service包中,我無法解釋,按照我的觀點DAO介面放在service包中要更好一些,將DAO介面放在dao包或許有名稱上對應的考慮。對於領域模型,將Repository介面放入infrastructure包中會引入包的迴圈依賴,Repository依賴Domain,Domain依賴Repository。然而對於貧血模型,將DAO介面放入dao包中則不會引入包迴圈依賴,只有service對DAO和model的依賴,而沒有反方向的依賴,這也導致service包很不穩定,service又正是放置業務邏輯的地方。JDepend這個工具可以檢測包的依賴關係。

貧血模型中Facade有何用?

我以前的做一個專案使用的就是貧血模型,使用了service和facade,當我們討論service和facade有什麼區別時,很少有人清楚,最終結果facade就是一個空殼,它除了將方法實現委託給相應的service方法,不做任何事,它們的介面中的方法都一樣。Facade應該是主要充當遠端訪問的門面,這在EJB時代相當普遍,自從Rod Johson叫嚷without EJB之後,大家對EJB的熱情降了很多,對許多使用貧血模型的應用程式來說,facade是沒有必要的。貧血模型中的service在本質上屬於應用層的東西。當然如果確實需要提供遠端訪問,那麼遠端Facade(或許叫做Remote Service更好)也是很有用的,但是它仍然屬於應用層,只不過在技術層面上將它的實現委託給對應的Service。下圖是貧血模型的分層:

貧血模型分層

從上面的分層可以看出貧血模型實際上相當於取消掉了領域層,因為領域層並沒有包含業務邏輯。

DAO到底有沒有必要?

貧血模型中的DAO或領域模型中的Repository到底有沒有必要?有人認為DAO或者說Repository是充血模型的大敵,對此我無論如何也不贊同。DAO或Repository是負責持久化邏輯的,如果取消掉DAO或Repository,將持久化邏輯直接寫入到model物件中,勢必造成model物件承擔不必要的職責。雖然現在的ORM框架已經做得很好了,持久化邏輯還是需要大量的程式碼,持久化邏輯的摻入會使model中的業務邏輯變得模糊。允許去掉DAO的一個必要條件就是Java的的持久化框架必須足夠先進,持久化邏輯的引入不會干擾業務邏輯,我認為這在很長一段時間內將無法做到。在rails中能夠將DAO去掉的原因就是rail中實現持久化邏輯的程式碼很簡潔直觀,這也與ruby的表達能力強有關係。DAO的另外一個好處隔離資料庫,這可以支援多個數據庫,甚至可以支援檔案儲存。基於DAO的這些優點,我認為,即使將來Java的持久化框架做得足夠優秀,使用DAO將持久化邏輯從業務邏輯中分離開來還是十分必要的,況且它們本身就應該分離。

結束語

在這篇文章裡,我使用了一個轉帳例子來描述領域模型和貧血模型的不同,實現程式碼可以從附件中下載,我推薦你看下附件程式碼,這會對領域模型和貧血模型有個更清楚的認識。我談到了軟體的分層,以及貧血模型和領域模型的實現又是怎樣對應到這些層上去的,最後是對DAO(或Repository)的討論。以上只是我個人觀點,如有不同意見歡迎指出。