锁定老帖子 主题:一个简单例子:贫血模型or领域模型
该帖已经被评为精华帖
|
|||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
作者 | 正文 | ||||||||||||||||||||
发表时间:2008-12-01
最后修改:2008-12-01
最近taowen同学连续发起了两起关于贫血模型和领域模型的讨论,引起了大家的广泛热烈的讨论,但是讨论(或者说是争论)的结果到底怎样,我想值得商榷。问题是大家对贫血模型和领域模型都有自己的看法,如果没有对此达到概念上的共识,那么讨论的结果应该可想而知,讨论的收获也是有的,至少知道了分歧的存在。为了使问题具有确定性,我想从一个简单例子着手,用我对贫血模型和领域模型的概念来分别实现例子。至于我的理解对与否,大家可以做评判,至少有个可以评判的标准在这。
贫血模型
包结构
代码实现
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 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); } }
优缺点
贫血模型的优点是很明显的:
包结构
领域模型的实现一般包含如下包:
代码实现
现在来看实现,照例先看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 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类来实现。对于这样一个简单的例子,领域模型没有太多优势,但是仍然可以看到代码的实现要简单一些。当业务变得复杂之后,领域模型的优势就体现出来了。
优缺点
其优点是:
其缺点是:
我的看法
这部分我将提出一些可能存在争议的问题并提出自己的看法。
软件分层
对于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)的讨论。以上只是我个人观点,如有不同意见欢迎指出。
声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|||||||||||||||||||||
返回顶楼 | |||||||||||||||||||||
发表时间:2008-12-02
其实表示层变了,领域层也得变,很难保持不变的。
|
|||||||||||||||||||||
返回顶楼 | |||||||||||||||||||||
发表时间:2008-12-02
最后修改:2008-12-02
争论的焦点到了DAO上啊,呵呵。确实,实践中很多项目最终实施DDD的结果就是把所有的DAO重命名为Repository。但是我认为DAO和Repository很像,但是不是一个东西,因为它们出发点不同。
为什么要有DAO? 因为之前,很早之前,我们对于框架中立性还很受用。DAO给了我们可以随时把Hibernate换成ibatis的幻觉,所以我们要有一个地方隔离了框架。 而且DAO集中了所有的查询,方便了性能调优人员。同时也鼓励了查询的重用,同样方便了调优。 为什么要有Repository? 在我看来,与其说PublicationRepository,不如说Publications。Repository是一个集合对象,它封装了集合的逻辑。因为它具有封装性,所以它应该负责保持这个集合的状态,比如拒绝一些非法的的修改 class PublicationRepository { public void save(Publication pub) { if (hasSameName(pub)) { throw new InvalidPublicationException(); } dao.save(pub); } } 另外,Repository只应该负责Aggregate Root。对于被Aggregate的对象,应该用Navigation,也就是在关系之间游走来获取。所以不是所有的查询都必须由Repository来完成,比如说: class Contact { private List<ContactNote> contactNotes = new ArrayList<ContactNote>(); public void contactedBy(User accountManager, DateTime time){ ContactNote contactNote = new ContactNote(this, accountManager, time); if (isDuplicated(contactNote)) { throw new InvalidContactNote(); } contactNotes.add(contactNote); } private boolean isDuplicated(ContactNote contactNote) { // 查询contactNotes return xxx; } } 现状是,对象之间的关联不可查询导致了,很多这样的查询必须通过xxxDao,xxxRepository来完成。其实它们都不应该插手。 理想情况下,只有业务开始的时候用repository加载对象,在结束的时候用repository把对象存储回去,中间都是领域对象在互相作用。而DAO,可以以Generic Query Builder的形式存在,不过和它之前被发明出来的意图已经不是一个东西了。 |
|||||||||||||||||||||
返回顶楼 | |||||||||||||||||||||
发表时间:2008-12-02
"与贫血模型的区别在于Account类中包含业务方法(credit,debit),注意没有set方法,对Account的更新是通过业务方法来更新的。" ...这个应该只对这种简单的类有意义吧。
比如包含数十条信息的AccountInfo类,你是想通过一个带数十个参数的register(...)来完成,还是用数十个Set来完成? |
|||||||||||||||||||||
返回顶楼 | |||||||||||||||||||||
发表时间:2008-12-02
第一种方式services就是Fowler说的事务脚本吧
|
|||||||||||||||||||||
返回顶楼 | |||||||||||||||||||||
发表时间:2008-12-02
tomcatacec 写道 其实表示层变了,领域层也得变,很难保持不变的。
一个好的领域模型是应该当表示层变动时,领域层不变。 |
|||||||||||||||||||||
返回顶楼 | |||||||||||||||||||||
发表时间:2008-12-02
最后修改:2008-12-02
taowen 写道 争论的焦点到了DAO上啊,呵呵。确实,实践中很多项目最终实施DDD的结果就是把所有的DAO重命名为Repository。但是我认为DAO和Repository很像,但是不是一个东西,因为它们出发点不同。
为什么要有DAO? 因为之前,很早之前,我们对于框架中立性还很受用。DAO给了我们可以随时把Hibernate换成ibatis的幻觉,所以我们要有一个地方隔离了框架。 而且DAO集中了所有的查询,方便了性能调优人员。同时也鼓励了查询的重用,同样方便了调优。 为什么要有Repository? 在我看来,与其说PublicationRepository,不如说Publications。Repository是一个集合对象,它封装了集合的逻辑。因为它具有封装性,所以它应该负责保持这个集合的状态,比如拒绝一些非法的的修改 class PublicationRepository { public void save(Publication pub) { if (hasSameName(pub)) { throw new InvalidPublicationException(); } dao.save(pub); } } 非常赞同你关于DAO和Repository的观点,DAO原本的作用就是隔离数据库的影响,没有业务逻辑。而Repository更抽象,从概念上来说是一个可以全局访问的集合,从这个意义上来讲对你所举PublicationRepository,使用add(Publication pub)作为方法签名要更好一些。Repository也负责保持完整对象的完整性,PublicationRepository的例子也说明了这一点,另外一个例子,但从数据库重建一个对象时,由于外部原因,对象已经变得不完整,将它恢复为一个完整的对象或者直接抛异常也是Repostory的责任,它可以将这种保证对象完整性的责任委托给别的对象(如Factory)。将Repository和DAO联合起来用应该很有用,谢谢你的提醒! 至于 引用 class Contact { private List<ContactNote> contactNotes = new ArrayList<ContactNote>(); public void contactedBy(User accountManager, DateTime time){ ContactNote contactNote = new ContactNote(this, accountManager, time); if (isDuplicated(contactNote)) { throw new InvalidContactNote(); } contactNotes.add(contactNote); } private boolean isDuplicated(ContactNote contactNote) { // 查询contactNotes return xxx; } } 你说isDuplicated需要通过Repository来查询contactNotes,难道不能通过关联的contactNotes来检测是否duplicated吗?如果不能,那么ContactNote本身可能就是一个聚合根,因为这时contactNotes不具备局部身份唯一,即它是否只需要在Contact对象的内部具备唯一的身份就可以了,而是需要全局身份唯一,在所有的contactNotes中它需要具备身份唯一性。 |
|||||||||||||||||||||
返回顶楼 | |||||||||||||||||||||
发表时间:2008-12-02
最后修改:2008-12-02
越来越觉得DAO是没必要的
通常情况下DAO代码非常少,如果是hibernate,很多时候就一句代码 return getHibernateTemplate().xxxxxx.xxxxx; 为了一句这种代码的封装加一层有点觉得得不偿失,而且对HibernateTemplate再封装之后,代码更少。 多加一层增加了系统的复杂度,无论是维护还是调试都会麻烦一些。 省去了DAO层之后,我们发现,业务Bean更像一个可重用的组件,与其他层没什么依赖。 |
|||||||||||||||||||||
返回顶楼 | |||||||||||||||||||||
发表时间:2008-12-02
ray_linn 写道 "与贫血模型的区别在于Account类中包含业务方法(credit,debit),注意没有set方法,对Account的更新是通过业务方法来更新的。" ...这个应该只对这种简单的类有意义吧。
比如包含数十条信息的AccountInfo类,你是想通过一个带数十个参数的register(...)来完成,还是用数十个Set来完成? 或许我表达的不清楚,我不是不要set方法,而是说所有的方法都必须保证对象的完整性,在DDD中约束是一个很重要的概念。如果Account需要确实直接改变金额,那么set方法就是必需的,但是在set方法内部进行某种约束检测,例如金额不能小于0,这个在现实中不太可能,或许你需要只是一个清零的操作,那么就请只用一个clear方法,这样可以只暴露需要的功能。 当两个属性之间有着某种紧密的关联,那么提供方法来同时设置这两个属性是必要的,而不是两个分别的set方法。 |
|||||||||||||||||||||
返回顶楼 | |||||||||||||||||||||
发表时间:2008-12-02
bloodrate 写道 第一种方式services就是Fowler说的事务脚本吧
是的。 |
|||||||||||||||||||||
返回顶楼 | |||||||||||||||||||||