转:一个简单例子:贫血模型or领域模型

贫血模型

我们首先用贫血模型来实现。所谓贫血模型就是模型对象之间存在完整的关联(可能存在多余的关联),但是对象除了get和set方外外几乎就没有其它的方法,整个对象充当的就是一个数据容器,用C语言的话来说就是一个结构体,所有的业务方法都在一个无状态的Service类中实现,Service类仅仅包含一些行为。这是Java Web程序采用的最常用开发模型,你可能采用的就是这种方法,虽然可能不知道它有个“贫血模型”的称号,这要多亏Martin Flower(这个家伙惯会发明术语!)。

包结构

在讨论具体的实现之前,我们先来看来贫血模型的包结构,以便对此有个大概的了解。
  

贫血模型的实现一般包括如下包:

  • dao:负责持久化逻辑
  • model:包含数据对象,是service操纵的对象
  • service:放置所有的服务类,其中包含了所有的业务逻辑
  • facade:提供对UI层访问的入口

代码实现

先看model包的两个类,Account和TransferTransaction对象,分别代表帐户和一次转账事务。由于它们不包含业务逻辑,就是一个普通的Java Bean,下面的代码省略了get和set方法。

  1. public class Account {
  2. private String accountId;
  3. private BigDecimal balance;
  4. public Account() {}
  5. public Account(String accountId, BigDecimal balance) {
  6. this.accountId = accountId;
  7. this.balance = balance;
  8. }
  9. // getter and setter ....
  10. }
  1. public class TransferTransaction {
  2. private Date timestamp;
  3. private String fromAccountId;
  4. private String toAccountId;
  5. private BigDecimal amount;
  6. public TransferTransaction() {}
  7. public TransferTransaction(String fromAccountId, String toAccountId, BigDecimal amount, Date timestamp) {
  8. this.fromAccountId = fromAccountId;
  9. this.toAccountId = toAccountId;
  10. this.amount = amount;
  11. this.timestamp = timestamp;
  12. }
  13. // getter and setter ....
  14. }

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

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

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

优缺点

贫血模型的优点是很明显的:

  1. 被许多程序员所掌握,许多教材采用的是这种模型,对于初学者,这种模型很自然,甚至被很多人认为是java中最正统的模型。
  2. 它非常简单,对于并不复杂的业务(转帐业务),它工作得很好,开发起来非常迅速。它似乎也不需要对领域的充分了解,只要给出要实现功能的每一个步骤,就能实现它。
  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中的对象:

  1. public class Account {
  2. private String accountId;
  3. private BigDecimal balance;
  4. private OverdraftPolicy overdraftPolicy = NoOverdraftPolicy.INSTANCE;
  5. public Account() {}
  6. public Account(String accountId, BigDecimal balance) {
  7. Validate.notEmpty(accountId);
  8. Validate.isTrue(balance == null || balance.compareTo(BigDecimal.ZERO) >= 0);
  9. this.accountId = accountId;
  10. this.balance = balance == null ? BigDecimal.ZERO : balance;
  11. }
  12. public String getAccountId() {
  13. return accountId;
  14. }
  15. public BigDecimal getBalance() {
  16. return balance;
  17. }
  18. public void debit(BigDecimal amount) throws AccountUnderflowException {
  19. Validate.isTrue(amount.compareTo(BigDecimal.ZERO) > 0);
  20. if (!overdraftPolicy.isAllowed(this, amount)) {
  21. throw new AccountUnderflowException(this, amount);
  22. }
  23. balance = balance.subtract(amount);
  24. }
  25. public void credit(BigDecimal amount) {
  26. Validate.isTrue(amount.compareTo(BigDecimal.ZERO) > 0);
  27. balance = balance.add(amount);
  28. }
  29. }

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

TransferServiceImpl类:

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

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

优缺点

其优点是:

  1. 领域模型采用OO设计,通过将职责分配到相应的模型对象或Service,可以很好的组织业务逻辑,当业务变得复杂时,领域模型显出巨大的优势。
  2. 当需要多个UI接口时,领域模型可以重用,并且业务逻辑只在领域层中出现,这使得很容易对多个UI接口保持业务逻辑的一致(从领域模型的分层图可以看得更清楚)。

其缺点是:

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

我的看法

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

软件分层

理解软件分层、明晰每层的职责对于理解领域模型以及代码实现是有好处的。软件一般分为四层,分别为表示层,应用层,领域层和基础设施层。软件领域中另外一个著名的分层是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)的讨论。以上只是我个人观点,如有不同意见欢迎指出。

一个简单例子:贫血模型or领域模型的更多相关文章

  1. (转)Java中使用正则表达式的一个简单例子及常用正则分享

    转自:http://www.jb51.net/article/67724.htm 这篇文章主要介绍了Java中使用正则表达式的一个简单例子及常用正则分享,本文用一个验证Email的例子讲解JAVA中如 ...

  2. C语言多线程的一个简单例子

    多线程的一个简单例子: #include <stdio.h> #include <stdlib.h> #include <string.h> #include &l ...

  3. quartz---的一个简单例子

    quartz---的一个简单例子 首先建立一个maven项目.jar工程即可.(提示:我前面有如何建立一个maven工程的总结以及maven环境的配置.) 1.建立好后点击到app中运行,--> ...

  4. 用socket.io实现websocket的一个简单例子

    socket.io 是基于 webSocket 构建的跨浏览器的实时应用. 逛博客发现几个比较好的 一.用socket.io实现websocket的一个简单例子 http://biyeah.iteye ...

  5. 2,turicreate入门 - 一个简单的回归模型

    turicreate入门系列文章目录 1,turicreate入门 - jupyter & turicreate安装 2,turicreate入门 - 一个简单的回归模型 3,turicrea ...

  6. 词法分析程序 LEX和VC6整合使用的一个简单例子

    词法分析的理论知识不少,包括了正规式.正规文法.它们之间的转换以及确定的有穷自动机和不确定的有穷自动机等等... 要自己写一个词法分析器也不会很难,只要给出了最简的有穷自动机,就能很方便实现了,用if ...

  7. spring集合类型的setter注入的一个简单例子

    在项目中我们有时候会为集合类型设定一些默认的值,使用spring后,我们可以通过配置文件的配置,用setter方式为对象的集合属性提供一些默认值,下面就是一个简单的例子. 首先我们创建了一个名为Col ...

  8. netsh interface portproxy的一个简单例子

    netsh interface portproxy的微软帮助文档地址: https://technet.microsoft.com/zh-cn/library/cc776297(WS.10).aspx ...

  9. js 排列 组合 的一个简单例子

    最近工作项目需要用到js排列组合,于是就写了一个简单的demo. 前几天在网上找到一个写全排列A(n,n)的code感觉还可以,于是贴出来了, 排列的实现方式: 全排列主要用到的是递归和数组的插入 比 ...

随机推荐

  1. VSS迁移备忘

    今天早上服务器down掉了,没办法,只能把vss数据文件目录一并压缩,拷贝到本机.然后准备利用本机做服务端.下面是操作步骤: 1.将拷贝下来的文件夹设置为共享. 2.打开Microsoft Visua ...

  2. ant exec

    http://ant.apache.org/manual/Tasks/exec.html Exec Description Executes a system command. When the os ...

  3. Nutch搜索引擎(第2期)_ Solr简介及安装

    1.Solr简介 Solr是一个高性能,采用Java5开发,基于Lucene的全文搜索服务器.同时对其进行了扩展,提供了比Lucene更为丰富的查询语言,同时实现了可配置.可扩展并对查询性能进行了优化 ...

  4. 20145208《Java程序设计》第2周学习总结

    2015208 <Java程序设计>第2周学习总结 教材学习内容总结 本章内容主要讲了类型.变量与运算符的一些知识,也讲了一些基本的流程语法. 类型 基本类型 整数:short整数(占2字 ...

  5. 【Moqui业务逻辑翻译系列】Shipment Receiver Receives Shipment with Packing Slip but no PO

    Shipment Receiver receives shipment. It has invoice tucked into it. Receiver records vendor name, ve ...

  6. ListView 和 Adapter用法

    一个ListView通常有两个职责. (1)将数据填充到布局. (2)处理用户的选择点击等操作. 第一点很好理解,ListView就是实现这个功能的.第二点也不难做到,在后面的学习中读者会发现,这非常 ...

  7. 第一次作业---安卓开发工具Android studio发展演变

    Android studio2013年由谷歌推出,用于安卓端的开发,我所使用的版本为2015年5月推出的1.3.2. 1.安装.配置.作为麻瓜的我,刚刚接触Android studio时在安装方面走了 ...

  8. Gson解析Json格式数据

    //数据定义:=========================================== class User{ String name; String password; String ...

  9. attempted to assign id from null one-to-one

    one-to-one在hibernate中可以用来作为两张表之间的主键关联,这也是hibernate中主键关联的一种用法,这样在一张表中的ID,在生成另外一张表的同时回自动插入到相应的ID字段中去,相 ...

  10. A星寻路算法

    A星寻路算法 1.准备一个close关闭列表(存放已被检索的点),一个open开启列表(存放未被检索的点),一个当前点的对象cur 2.将cur设成开始点 3.从cur起,将cur点放入close表中 ...