最近taowen同窗連續發起了兩起關於貧血模型和領域模型的討論,引發了你們的普遍熱烈的討論,可是討論(或者說是爭論)的結果到底 怎樣,我想值得商榷。問題是你們對貧血模型和領域模型都有本身的見解,若是沒有對此達到概念上的共識,那麼討論的結果應該可想而知,討論的收穫也是有的, 至少知道了分歧的存在。爲了使問題具備肯定性,我想從一個簡單例子着手,用我對貧血模型和領域模型的概念來分別實現例子。至於個人理解對與否,你們能夠作 評判,至少有個能夠評判的標準在這。
一個例子 java
我要舉的是一個銀行轉賬的例子,又是一個被用濫了的例子。但即便這個例子也不是本身想出來的,而是剽竊的<<POJOs in Action>>中的例子,原諒我可憐的想像力
。當錢從一個賬戶轉到另外一個賬戶時,轉賬的金額不能超過第一個賬戶的存款餘額,餘額總數不能變,錢只是從一個帳戶流向另外一個賬戶,所以它們必須在一個事務內完成,每次事務成功完成都要記錄這次轉賬事務,這是全部的規則。程序員
貧血模型 數據庫
咱們首先用貧血模型來實現。所謂貧血模型就是模型對象之間存在完整的關聯(可能存在多餘的關聯),可是對象除了get和set方外外幾乎就沒有其它的方 法,整個對象充當的就是一個數據容器,用C語言的話來講就是一個結構體,全部的業務方法都在一個無狀態的Service類中實現,Service類僅僅包 含一些行爲。這是Java Web程序採用的最經常使用開發模型,你可能採用的就是這種方法,雖然可能不知道它有個“貧血模型”的稱號,這要多 虧Martin Flower(這個傢伙慣會發明術語!)。ruby
包結構 網絡
在討論具體的實現以前,咱們先來看來貧血模型的包結構,以便對此有個大概的瞭解。
貧血模型的實現通常包括以下包:app
- dao:負責持久化邏輯
- model:包含數據對象,是service操縱的對象
- service:放置全部的服務類,其中包含了全部的業務邏輯
- facade:提供對UI層訪問的入口
代碼實現 框架
先看model包的兩個類,Account和TransferTransaction對象,分別表明賬戶和一次轉帳事務。因爲它們不包含業務邏輯,就是一個普通的Java Bean,下面的代碼省略了get和set方法。dom
- public class Account {
- private String accountId;
- private BigDecimal balance;
-
- public Account() {}
- public Account(String accountId, BigDecimal balance) {
- this.accountId = accountId;
- this.balance = balance;
- }
-
-
- }
- 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;
- }
-
-
- }
這兩個類沒什麼可說的,它們就是一些數據容器。接下來看service包中TransferService接口和它的實現 TransferServiceImpl。TransferService定義了轉帳服務的接口,TransferServiceImpl則提供了轉帳服 務的實現。工具
- public interface TransferService {
- TransferTransaction transfer(String fromAccountId, String toAccountId, BigDecimal amount)
- throws AccountNotExistedException, AccountUnderflowException;
- }
- 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);
- accountDAO.updateAccount(toAccount);
- return transferTransactionDAO.create(fromAccountId, toAccountId, amount);
- }
- }
TransferServiceImpl類使用了AccountDAO和TranferTransactionDAO,它的transfer方法負責整個 轉賬操做,它首先判斷轉賬的金額必須大於0,而後判斷fromAccountId和toAccountId是一個存在的Account的 accountId,若是不存在拋AccountNotExsitedException。接着判斷轉賬的金額是否大於fromAccount的餘額,如 果是則拋AccountUnderflowException。接着分別調用fromAccount和toAccount的setBalance來更新它 們的餘額。最後保存到數據庫並記錄交易。TransferServiceImpl負責全部的業務邏輯,驗證是否超額提取並更新賬戶餘額。一切並不複雜,對 於這個例子來講,貧血模型工做得很是好!這是由於這個例子至關簡單,業務邏輯也不復雜,一旦業務邏輯變得複雜,TransferServiceImpl就 會膨脹。this
優缺點
貧血模型的優勢是很明顯的:
- 被許多程序員所掌握,許多教材採用的是這種模型,對於初學者,這種模型很天然,甚至被不少人認爲是java中最正統的模型。
- 它很是簡單,對於並不複雜的業務(轉賬業務),它工做得很好,開發起來很是迅速。它彷佛也不須要對領域的充分了解,只要給出要實現功能的每個步驟,就能實現它。
- 事務邊界至關清楚,通常來講service的每一個方法均可以當作一個事務,由於一般Service的每一個方法對應着一個用例。(在這個例子中我使用了facade做爲事務邊界,後面我要講這個是多餘的)
其缺點爲也是很明顯的:
- 全部的業務都在service中處理,當業愈來愈複雜時,service會變得愈來愈龐大,最終難以理解和維護。
- 將全部的業務放在無狀態的service中其實是一個過程化的設計,它在組織複雜的業務存在自然的劣勢,隨着業務的複雜,業務會在service中多個方法間重複。
- 當添加一個新的UI時,不少業務邏輯得從新寫。例如,當要提供Web Service的接口時,原先爲Web界面提供的service就很難重用,致使重複的業務邏輯(在貧血模型的分層圖中能夠看得更清楚),如何保持業務邏輯一致是很大的挑戰。
領域模型
接下來看看領域驅動模型,與貧血模型相反,領域模型要承擔關鍵業務邏輯,業務邏輯在多個領域對象之間分配,而Service只是完成一些不適合放在模型中的業務邏輯,它是很是薄的一層,它指揮多個模型對象來完成業務功能。
包結構
領域模型的實現通常包含以下包:
- infrastructure: 表明基礎設施層,通常負責對象的持久化。
- domain:表明領域層。domain包中包括兩個子包,分別是model和service。model中包含模型對 象,Repository(DAO)接口。它負責關鍵業務邏輯。service包爲一系列的領域服務,之因此須要service,按照DDD的觀點,是由於領域中的某些概念本質是一些行爲,而且不便放入某個模型對象中。好比轉賬操做,它是一個行爲,而且它涉及三個對 象,fromAccount,toAccount和TransferTransaction,將它放入任一個對象中都很差。
- application: 表明應用層,它的主要提供對UI層的統一訪問接口,並做爲事務界限。
代碼實現
如今來看實現,照例先看model中的對象:
- 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類:
- 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);
- accountRepository.updateAccount(toAccount);
- return transferTransactionRepository.create(fromAccountId, toAccountId, amount);
- }
-
- }
與貧血模型中的TransferServiceImpl相比,最主要的改變在於業務邏輯被移走了,由Account類來實現。對於這樣一個簡單的例子,領域模型沒有太多優點,可是仍然能夠看到代碼的實現要簡單一些。當業務變得複雜以後,領域模型的優點就體現出來了。
優缺點
其優勢是:
- 領域模型採用OO設計,經過將職責分配到相應的模型對象或Service,能夠很好的組織業務邏輯,當業務變得複雜時,領域模型顯出巨大的優點。
- 當須要多個UI接口時,領域模型能夠重用,而且業務邏輯只在領域層中出現,這使得很容易對多個UI接口保持業務邏輯的一致(從領域模型的分層圖能夠看得更清楚)。
其缺點是:
- 對程序員的要求較高,初學者對這種將職責分配到多個協做對象中的方式感到極不適應。
- 領域驅動建模要求對領域模型完整而透徹的瞭解,只給出一個用例的實現步驟是沒法獲得領域模型的,這須要和領域專家的充分討論。錯誤的領域模型對項目的危害很是之大,而實現一個好的領域模型很是困難。
- 對於簡單的軟件,使用領域模型,顯得有些殺雞用牛刀了。
個人見解
這部分我將提出一些可能存在爭議的問題並提出本身的見解。
軟件分層
理解軟件分層、明晰每層的職責對於理解領域模型以及代碼實現是有好處的。軟件通常分爲四層,分別爲表示層,應用層,領域層和基礎設施層。軟件領域中另一個著名的分層是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)的討論。以 上只是我我的觀點,若有不一樣意見歡迎指出。