贫血模型or领域模型

引:http://www.javaeye.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. public class Account {   
  2.     private String accountId;   
  3.     private BigDecimal balance;   
  4.   
  5.     public Account() {}   
  6.     public Account(String accountId, BigDecimal balance) {   
  7.         this.accountId = accountId;   
  8.         this.balance = balance;   
  9.     }   
  10.     // getter and setter ....   
  11.   
  12. }  

 

Java代码 复制代码
  1. public class TransferTransaction {   
  2.     private Date timestamp;   
  3.     private String fromAccountId;   
  4.     private String toAccountId;   
  5.     private BigDecimal amount;     
  6.   
  7.     public TransferTransaction() {}   
  8.   
  9.     public TransferTransaction(String fromAccountId, String toAccountId, BigDecimal amount, Date timestamp) {   
  10.         this.fromAccountId = fromAccountId;   
  11.         this.toAccountId = toAccountId;   
  12.         this.amount = amount;   
  13.         this.timestamp = timestamp;   
  14.     }   
  15.   
  16.     // getter and setter ....   
  17. }  


这两个类没什么可说的,它们就是一些数据容器。接下来看service包中TransferService接口和它的实现TransferServiceImpl。TransferService定义了转账服务的接口,TransferServiceImpl则提供了转账服务的实现。

Java代码 复制代码
  1. public interface TransferService {   
  2.     TransferTransaction transfer(String fromAccountId, String toAccountId, BigDecimal amount)    
  3.             throws AccountNotExistedException, AccountUnderflowException;   
  4. }  

 

Java代码 复制代码
  1. public class TransferServiceImpl implements TransferService {   
  2.     private AccountDAO accountDAO;   
  3.     private TransferTransactionDAO transferTransactionDAO;   
  4.   
  5.     public TransferServiceImpl(AccountDAO accountDAO,    
  6.             TransferTransactionDAO transferTransactionDAO) {   
  7.         this.accountDAO = accountDAO;   
  8.         this.transferTransactionDAO = transferTransactionDAO;   
  9.   
  10.     }   
  11.   
  12.     public TransferTransaction transfer(String fromAccountId, String toAccountId,   
  13.             BigDecimal amount) throws AccountNotExistedException, AccountUnderflowException {      
  14.     Validate.isTrue(amount.compareTo(BigDecimal.ZERO) > 0);         
  15.   
  16.         Account fromAccount = accountDAO.findAccount(fromAccountId);   
  17.         if (fromAccount == nullthrow new AccountNotExistedException(fromAccountId);   
  18.         if (fromAccount.getBalance().compareTo(amount) < 0) {   
  19.             throw new AccountUnderflowException(fromAccount, amount);   
  20.         }          
  21.   
  22.         Account toAccount = accountDAO.findAccount(toAccountId);   
  23.         if (toAccount == nullthrow new AccountNotExistedException(toAccountId);   
  24.         fromAccount.setBalance(fromAccount.getBalance().subtract(amount));   
  25.         toAccount.setBalance(toAccount.getBalance().add(amount));                  
  26.   
  27.         accountDAO.updateAccount(fromAccount);      // 对Hibernate来说这不是必须的   
  28.         accountDAO.updateAccount(toAccount);        // 对Hibernate来说这不是必须的   
  29.         return transferTransactionDAO.create(fromAccountId, toAccountId, amount);   
  30.     }   
  31. }  


TransferServiceImpl类使用了AccountDAO和TranferTransactionDAO,它的transfer方法负责整个转帐操作,它首先推断转帐的金额必须大于0,然后推断fromAccountId和toAccountId是一个存在的Account的accountId,假设不存在抛AccountNotExsitedException。接着推断转帐的金额是否大于fromAccount的剩余金额,假设是则抛AccountUnderflowException。接着分别调用fromAccount和toAccount的setBalance来更新它们的剩余金额。最后保存到数据库并记录交易。TransferServiceImpl负责全部的业务逻辑,验证是否超额提取并更新帐户剩余金额。一切并不复杂,对于这个样例来说,贫血模型工作得很好!这是由于这个样例相当简单,业务逻辑也不复杂,一旦业务逻辑变得复杂,TransferServiceImpl就会膨胀。

 

优缺点

 

贫血模型的长处是非常明显的:

  1. 被很多程序猿所掌握,很多教材採用的是这样的模型,对于刚開始学习的人,这样的模型非常自然,甚至被非常多人觉得是java中最正统的模型。
  2. 它非常easy,对于并不复杂的业务(转帐业务),它工作得非常好,开发起来非常迅速。它似乎也不须要对领域的充分了解,仅仅要给出要实现功能的每一个步骤,就能实现它。
  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. public class Account {   
  2.     private String accountId;   
  3.     private BigDecimal balance;   
  4.        
  5.     private OverdraftPolicy overdraftPolicy = NoOverdraftPolicy.INSTANCE;   
  6.        
  7.     public Account() {}   
  8.        
  9.     public Account(String accountId, BigDecimal balance) {   
  10.         Validate.notEmpty(accountId);   
  11.         Validate.isTrue(balance == null || balance.compareTo(BigDecimal.ZERO) >= 0);   
  12.            
  13.         this.accountId = accountId;   
  14.         this.balance = balance == null ? BigDecimal.ZERO : balance;   
  15.     }   
  16.        
  17.     public String getAccountId() {   
  18.         return accountId;   
  19.     }   
  20.   
  21.     public BigDecimal getBalance() {   
  22.         return balance;   
  23.     }   
  24.        
  25.     public void debit(BigDecimal amount) throws AccountUnderflowException {   
  26.         Validate.isTrue(amount.compareTo(BigDecimal.ZERO) > 0);   
  27.            
  28.         if (!overdraftPolicy.isAllowed(this, amount)) {   
  29.             throw new AccountUnderflowException(this, amount);   
  30.         }   
  31.         balance = balance.subtract(amount);   
  32.     }   
  33.        
  34.     public void credit(BigDecimal amount) {   
  35.         Validate.isTrue(amount.compareTo(BigDecimal.ZERO) > 0);   
  36.            
  37.         balance = balance.add(amount);   
  38.     }   
  39.        
  40. }  


与贫血模型的差别在于Account类中包括业务方法(credit,debit),注意没有set方法,对Account的更新是通过业务方法来更新的。因为“不同意从帐户取出大于存款剩余金额的资金”是一条重要规则,将它放在一个单独的接口OverdraftPolicy中,也提供了灵活性,当业务规则变化时,仅仅须要改变这个实现就能够了。

TransferServiceImpl类:

Java代码 复制代码
  1. public class TransferServiceImpl implements TransferService {   
  2.     private AccountRepository accountRepository;   
  3.     private TransferTransactionRepository transferTransactionRepository;   
  4.        
  5.     public TransferServiceImpl(AccountRepository accountRepository,    
  6.             TransferTransactionRepository transferTransactionRepository) {   
  7.         this.accountRepository = accountRepository;   
  8.         this.transferTransactionRepository = transferTransactionRepository;   
  9.     }   
  10.        
  11.     public TransferTransaction transfer(String fromAccountId, String toAccountId,   
  12.             BigDecimal amount) throws AccountNotExistedException, AccountUnderflowException {   
  13.         Account fromAccount = accountRepository.findAccount(fromAccountId);   
  14.         if (fromAccount == nullthrow new AccountNotExistedException(fromAccountId);   
  15.         Account toAccount = accountRepository.findAccount(toAccountId);   
  16.         if (toAccount == nullthrow new AccountNotExistedException(toAccountId);   
  17.   
  18.         fromAccount.debit(amount);   
  19.         toAccount.credit(amount);   
  20.            
  21.         accountRepository.updateAccount(fromAccount);   // 对Hibernate来说这不是必须的   
  22.         accountRepository.updateAccount(toAccount);     // 对Hibernate来说这不是必须的   
  23.         return transferTransactionRepository.create(fromAccountId, toAccountId, amount);   
  24.     }   
  25.        
  26. }  

与贫血模型中的TransferServiceImpl相比,最基本的改变在于业务逻辑被移走了,由Account类来实现。对于这样一个简单的样例,领域模型没有太多优势,可是仍然能够看到代码的实现要简单一些。当业务变得复杂之后,领域模型的优势就体现出来了。

 

优缺点

 

其长处是:

  1. 领域模型採用OO设计,通过将职责分配到对应的模型对象或Service,能够非常好的组织业务逻辑,当业务变得复杂时,领域模型显出巨大的优势。
  2. 当须要多个UI接口时,领域模型能够重用,而且业务逻辑仅仅在领域层中出现,这使得非常easy对多个UI接口保持业务逻辑的一致(从领域模型的分层图能够看得更清楚)。

其缺点是:

  1. 对程序猿的要求较高,刚開始学习的人对这样的将职责分配到多个协作对象中的方式感到极不适应。
  2. 领域驱动建模要求对领域模型完整而透彻的了解,仅仅给出一个用例的实现步骤是无法得到领域模型的,这须要和领域专家的充分讨论。错误的领域模型对项目的危害很之大,而实现一个好的领域模型很困难。
  3. 对于简单的软件,使用领域模型,显得有些杀鸡用牛刀了。

 

我的看法

 

 

这部分我将提出一些可能存在争议的问题并提出自己的看法。

 

软件分层


理解软件分层、明晰每层的职责对于理解领域模型以及代码实现是有优点的。软件一般分为四层,分别为表示层,应用层,领域层和基础设施层。软件领域中另外一个著名的分层是TCP/IP分层,分为应用层,运输层,网际层和网络接口层。我发现它们之间存在相应关系,见下表:

 

TCP/IP分层 软件分层
表示层 负责向用户显示信息。
应用层 负责处理特定的应用程序细节。如FTP,SMTP等协议。 应用层 定义软件能够完毕的工作,指挥领域层的对象来解决这个问题。它不负责业务逻辑,是非常薄的一层。
运输层 两台主机上的应用程序提供端到端的通信。主要包含TCP,UDP协议。 领域层 负责业务逻辑,是业务软件的核心。
网际层 处理分组在网络中的活动,比如分组的选路。主要包含IP协议。
网络接口层 操作系统中的设备驱动程序和计算机中相应的网络接口卡。它们一起处理与电缆(或其它不论什么传输媒介)的物理接口细节。 基础设施层 为上层提供通用技术能力,如消息发送,数据持久化等。

 

对于TCP/IP来说,运输层和网际层是最核心的,这也是TCP/IP名字的由来,就像领域层也是软件最核心的一层。能够看出领域模型的包结构与软件分层是一致的。在软件分层中,表示层、领域层和基础设施层都easy理解,难理解的是应用层,非常easy和领域层中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)的讨论。以上仅仅是我个人观点,如有不允许见欢迎指出。

 

原文地址:https://www.cnblogs.com/zfyouxi/p/4346611.html