【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接口保持业务逻辑的一致(从领域模型的分层图可以看得更清楚)。

其缺点是:

  • 对程序员的要求较高,初学者对这种将职责分配到多个协作对象中的方式感到极不适应。
  • 领域驱动建模要求对领域模型完整而透彻的了解,只给出一个用例的实现步骤是无法得到领域模型的,这需要和领域专家的充分讨论。错误的领域模型对项目的危害非常之大,而实现一个好的领域模型非常困难。
  • 对于简单的软件,使用领域模型,显得有些杀鸡用牛刀了。

你可能感兴趣的:(微服务,DDD,领域模型,贫血模型)