【DDD】貧血模型和領域模型區別

前言:

最近公司架構師一直在組織關於DDD的培訓,也正在研讀《領域驅動設計》一書,也在新項目中逐步實踐,但是感覺領域驅動很抽象,其實好多項目在做的時候,我發現雖然整體的架構設計,都把領域模型單獨提出來作爲一個單獨的模塊去編寫、實現,但是具體的實現內容還是相對較爲簡單,比如 存放一些pojo,在manger層做一些關於事務的處理,但是核心業務還是寫在和service層,這樣編寫出來的模型,是典型的貧血模型。導致的問題是當業務越來越複雜,service層的邏輯越來越多,則越來越難讀懂,今日發現一文,詳細的闡述了貧血模型和領域模型的區別,俗話說,talk is cheap,show me the code,更容易幫助大家理解DDD所帶來的的好處。

一個例子

我要舉的是一個銀行轉帳的例子,又是一個被用濫了的例子。但即使這個例子也不是自己想出來的,而是剽竊的《POJOs in Action》中的例子,原諒我可憐的想像力 。當錢從一個帳戶轉到另一個帳戶時,轉帳的金額不能超過第一個帳戶的存款餘額,餘額總數不能變,錢只是從一個賬戶流向另一個帳戶,因此它們必須在一個事務內完成,每次事務成功完成都要記錄此次轉帳事務,這是所有的規則。

貧血模型

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

包結構

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

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

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

代碼實現

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

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 ....   
  
}  
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 ....  
  
}  
 
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 ....   
}  
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則提供了轉賬服 務的實現。

public   interface  TransferService {  
    TransferTransaction transfer(String fromAccountId, String toAccountId, BigDecimal amount)   
            throws  AccountNotExistedException, AccountUnderflowException;  
}
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);      // 對Hibernate來說這不是必須的   
        accountDAO.updateAccount(toAccount);        // 對Hibernate來說這不是必須的   
        return  transferTransactionDAO.create(fromAccountId, toAccountId, amount);  
    }  
}  
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就 會膨脹。

優缺點

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

  • 被許多程序員所掌握,許多教材採用的是這種模型,對於初學者,這種模型很自然,甚至被很多人認爲是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);  
    }  
      
}  
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);   // 對Hibernate來說這不是必須的   
        accountRepository.updateAccount(toAccount);     // 對Hibernate來說這不是必須的   
        return  transferTransactionRepository.create(fromAccountId, toAccountId, amount);  
    }  
      
}
 
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類來實現。對於這樣一個簡單的例子,領域模型沒有太多優勢,但是仍然可以看到代碼的實現要簡單一些。當業務變得複雜之後,領域模型的優勢就體現出來了。

優缺點

其優點是:

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

其缺點是:

  • 對程序員的要求較高,初學者對這種將職責分配到多個協作對象中的方式感到極不適應。
  • 領域驅動建模要求對領域模型完整而透徹的瞭解,只給出一個用例的實現步驟是無法得到領域模型的,這需要和領域專家的充分討論。錯誤的領域模型對項目的危害非常之大,而實現一個好的領域模型非常困難。
  • 對於簡單的軟件,使用領域模型,顯得有些殺雞用牛刀了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章