`
lifethinker
  • 浏览: 72093 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

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

    博客分类:
  • java
阅读更多

最近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方法。

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 ....
}


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

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);
	}
}


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中的对象:

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);
	}
	
}

与贫血模型中的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)的讨论。以上只是我个人观点,如有不同意见欢迎指出。

 

分享到:
评论
26 楼 fnet 2008-12-05  
teclogid 写道
fnet 写道
terranhao 写道
新手问一下,不用DAO,假如我要条件查询怎么解决?
比如select * from entity as e where e.name=:name and e.id=:id
写在service里面?



放在Service里面也就一句连写

如果我多个service都用这个查询呢?如果这个查询某天需要修改呢?修改所有service?bad smell。



用设计模式解决,不用我提醒了吧
25 楼 qufulin 2008-12-04  
现在面对对象数据库正在兴起, 如果持久化使用oo数据库, DAO是不是就可以扔掉了?
24 楼 czx566 2008-12-04  
我也说一句:

    面向对象不是万能的,计算机处理任何有个事情,其实都有一个过程调用。   
    那么在利用充血模型实现时,那么一个业务中的过程调用职责就是service层存在的意义, 而被过程调用的单元元素的职责 都应该放入到领域模型中。

    这样是最理想的了。


最后谢谢楼主,受益良多!
23 楼 lujiawu12 2008-12-04  
看的有这种感觉:
      对领域模型有种似曾相识的感觉,在哪里呢 ?puremvc
     我对这个也做了少许学习,在flex方面。发现他们的观点很相似,比如业务处理不仅仅只放在action中 ,同样在module数据层方面也做与之相关的业务处理。在view方面也做它的相应处理。刚开始就觉得这种模式非常不错。
    
22 楼 lujiawu12 2008-12-04  
好像我目前做的 都是 “贫血模型”(自己还不怎么知道这么一个概念,汗!!!)
关于:
1.所有的业务都在service中处理,当业越来越复杂时,service会变得越来越庞大,  最终难以理解和维护。
2.将所有的业务放在无状态的service中实际上是一个过程化的设计,它在组织复杂的业务存在天然的劣势,随着业务的复杂,业务会在service中多个方法间重复。
3.当添加一个新的UI时,很多业务逻辑得重新写。
例如,当要提供Web Service的接口时,原先为Web界面提供的service就很难重用,导致重复的业务逻辑(在贫血模型的分层图中可以看得更清楚),如何保持业务逻辑一致是很大的挑战。
    这几点提出的太完美了。特别是1,2点,我特别有这种感觉。(面向过程)

21 楼 cherryQQ 2008-12-04  
看过帖子,受益啊,学啦
20 楼 lifethinker 2008-12-04  
<div class='quote_title'>pf_miles 写道</div>
<div class='quote_div'>to 楼主:<br/><br/>关于贫血模型的缺点:
<div class='quote_title'>引用</div>
<div class='quote_div'>“# 将所有的业务放在无状态的service中实际上是一个过程化的设计,它在组织复杂的业务存在天然的劣<br/><br/>势,随着业务的复杂,业务会在service中多个方法间重复。<br/># 当添加一个新的UI时,很多业务逻辑得重新写。例如,当要提供Web Service的接口时,原先为Web界面<br/><br/>提供的service就很难重用,导致重复的业务逻辑(在贫血模型的分层图中可以看得更清楚),如何保持<br/><br/>业务逻辑一致是很大的挑战。”</div>
<br/><br/>这两点我不太赞同,说“业务会在service中多个方法间重复”这可以通过即时的重构解决,如抽取private方法,甚至在“多个service之间重复”时可以抽出包含共用逻辑的类;<br/><br/>第二句话“service就很难重用”我觉得也说不过去,贫血模型中的service是无状态的,可以纯粹看做一段组织好的代码,别的什么地方要用这个逻辑,直接将这段代码inline调用即可,不必担心任何状态相关的问题;总之要在webUI和远程接口之间共用某个service是肯定可以的。<br/><br/>service里的业务逻辑代码会越来越长我赞同,并且正亲身体验中;尤其对于变化很快的某个行业的逻辑;<br/>但是别忘了,上了一定规模的系统,目前还是很少采用领域模型的方式来组织的,原因如楼主描述的“领域模型的缺点”,其实别看小小的几句话,里面涵盖的风险是很大的,特别对于大型系统;或许还因为目前的大型系统一般建立地比较早,而采用了早已成熟的贫血模型,由数据库驱动而来的系统显然贫血模型更加自然,开发人员也好找。<br/><br/></div>
<p>第一个问题,重构能够解决部分问题,但不能解决全部问题。我承认对于这个简单的转账例子,如果添加取钱和存钱服务,一个优秀的程序员完全能够提取方法重构来消除代码上的重复,但是请注意,这只是技术上的手段,不是领域的驱动。我曾经参加的一个项目,当我查看现有代码时,发现里面很多重复代码,由于看完《重构》不久,十分想练手,SDM也赞同,于是让我做code review。刚开始很兴奋,但是当消除几个明显的代码重复之后,剩下来的工作越来越举步维艰,很多逻辑相互交织在一起。虽然明显看到其中存在重复,但是却很难使用提取方法来重构,好几次还不得不回复成原来的代码,因为重构后的方法和原来的代码存在细微的不一致。提取后的方法很少具有业务意义,它只是技术上的手段,因此也并有因为重复代码的消除而获得更可理解的代码。对于这种过程化的类,我发现几乎仅有的重构手段就是提取方法,不管是提取到当前类,还是提取到一个外部的工具类。最终的结果是,我的重构实质上并没有带来实质性的好处。我认为,<strong><span style='color: #ff6600;'>重构需要有领域模型的引导,否则它可能走错方向</span></strong><span style='color: #000000;'>。</span></p>
<p> </p>
<p>第二个问题,你认为WebUI和WebService接口的Service代码能够重用吗?两者接口的粒度不同,这会导致它们使用的DTO完全不同,要想重用几乎是不可能的。关于我谈到的领域模型的缺点,我坦率的承认实施领域模型有很大的风险,尤其当项目中没有一个人真正懂得领域模型时候。</p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<div class='quote_title'>pf_miles 写道</div>
<div class='quote_div'>to taowen:<br/><br/>引用<br/>“另外,Repository只应该负责Aggregate Root。对于被Aggregate的对象,应该用Navigation,也就是在<br/><br/>关系之间游走来获取。所以不是所有的查询都必须由Repository来完成”<br/><br/><br/>这么说来repository只用来取root?从概念上讲不错,可是要知道往往一个root下面会有成百上千个node,这么巨大的一坨关系树我们用 repository取出来然后在“关系之间游走”...是否有点滑稽?呃...或许我们能想到引入延迟加载,取出来的root下的二级node都用 stub来占位;不过这又为污染我们纯洁的领域模型创造了良机...<br/>我想你肯定解决过这类问题,能否多介绍一点?</div>
<p> 我来回答这个问题吧,对于Aggregate Root的概念你可能理解有问题,详细介绍还是请参看Eric Evans的《DDD》,我这里只举一个简单的例子,订单的例子(又被用滥了)。关于Order, LineItem,Product的关系,这里不用说了吧(我讨厌画图),对于Order我们需要单独跟踪它,因为客户有可能去查询某个订单(根据订单号),因此它是一个聚合根。但是对LineItem,我们一般不需要跟踪它,凡是当我们谈论一个LineItem时它必定要和一个Order联系,也就是说有个Order的上下文在,因此LineItem不是一个聚合根,它属性Order这个聚合根。同样我们也需要跟踪Product,我们需要查询所有的Product,需要获得单个Product的信息,因此Product也是一个聚合根。在这里有两个聚合根,Order和Product。按照你的观点(我揣测的),可能认为只有Order这一个聚合根,这是错误的。</p>
19 楼 coolnight 2008-12-04  

我同意 pf_miles 的回复,
对于楼主的“所谓的”明显的优点和缺点, 我觉得有待商榷。

引用

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

   1. 被许多程序员所掌握,许多教材采用的是这种模型,对于初学者,这种模型很自然,甚至被很多人认为是java中最正统的模型。
   2. 它非常简单,对于并不复杂的业务(转帐业务),它工作得很好,开发起来非常迅速。它似乎也不需要对领域的充分了解,只要给出要实现功能的每一个步骤,就能实现它。
   3. 事务边界相当清楚,一般来说service的每个方法都可以看成一个事务,因为通常Service的每个方法对应着一个用例。(在这个例子中我使用了facade作为事务边界,后面我要讲这个是多余的)

其缺点为也是很明显的:
   1. 所有的业务都在service中处理,当业越来越复杂时,service会变得越来越庞大,最终难以理解和维护。
   2. 将所有的业务放在无状态的service中实际上是一个过程化的设计,它在组织复杂的业务存在天然的劣势,随着业务的复杂,业务会在service中多个方法间重复。
   3. 当添加一个新的UI时,很多业务逻辑得重新写。例如,当要提供Web Service的接口时,原先为Web界面提供的service就很难重用,导致重复的业务逻辑(在贫血模型的分层图中可以看得更清楚),如何保持业务逻辑一致是很大的挑战。


缺点1:
    我的观点是:
        a) Service层可以而且必须经过良好的组织的以达到清晰、简洁、复用的效果!
        b) 不经过良好组织的Service层,不管用贫血的、还是充血的模型, 都是难以理解和维护的!
缺点2:
    过程化的设计在组织复杂的业务存在天然的劣势? 何以见得? OO到最终的程序不都是在过程化的运行吗?
    随着业务的复杂, 业务会在service中多个方法间重复?? 这又是哪门子的道理? 难道贫血模型中就不能用
封装不能复用了? 还是在java写东西呀!
    业务逻辑的复杂会导致Service的膨胀,但是那是必然的,因为业务逻辑复杂了。 Service可能从一个变成多个,
从一个类中简单的几个方法变成一个类中有十来个方法, 但是这些在rich模型下,一样无法避免,我们只能通过
良好的组织各个类来使它们容易理解和维护!

缺点3:
    当添加新的UI时,业务逻辑得重新写? 为什么?
    我的观点:
    业务逻辑如果需要重写, 那就意味着业务逻辑发生了改变! 只有当业务逻辑发生改变的情况下, 才需要重写!
    实际上, 在贫血模型下, 模型给上层(UI层)的接口不仅仅包括Service层, 还包括贫血模型中的那些类似值对象的POJO, 这已经是在业务逻辑不改变的情况下,上层所能拿到的最完整的模型, 只要逻辑不改变, 上层所要
做的就是对这些POJO的操作和对service层的调用,UI改变根本不需要重写domain层!
    这一点上, 看不到rich模型有什么优势, 甚至贫血模型更有优势,因为提供的模型很完整,而rich模型下,封装
之类可能导致一些底层信息的屏蔽, 这可能会在将来的扩展当中带来麻烦。


再看楼主的例子, 所谓业务逻辑被移走了, 只是在 Account类中封装了 credit/debit方法。
在我看来, 首先, 这是很合理的一种封装, 这种封装不应该成为两种模型之间的根本区别, 贫血模型下一样可以
这样封装,可以在贫血的POJO里面,也可以在service层也可以这样封装,适当而已。
其次, 这个封装的credit/debit方法,并不是真正的业务逻辑, 真正的业务逻辑应该是完整的:
       credit/debit + save data !

我觉得楼主的所谓充血模型,只是 贫血模型 + 一些简单封装 !
如果要做真正的充血模型, 那么在Account的credit/debit方法里面请把 save也塞进去!


下面是楼主写的“充血模型的优点:”
引用

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

优缺点

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

其缺点是:
   1. 对程序员的要求较高,初学者对这种将职责分配到多个协作对象中的方式感到极不适应。
   2. 领域驱动建模要求对领域模型完整而透彻的了解,只给出一个用例的实现步骤是无法得到领域模型的,这需要和领域专家的充分讨论。错误的领域模型对项目的危害非常之大,而实现一个好的领域模型非常困难。
   3. 对于简单的软件,使用领域模型,显得有些杀鸡用牛刀了。
18 楼 by5739 2008-12-04  
想请教一下...order和orderitem是1对多的强关联, 我要分页列出orderitem...充血模型是如何处理的?
1. 用Repository find(offset, pagesize)出来?
2. order.getOrderItem(offset, pagesize)出来...如果是这种...要么在load出order的时候把所有orderitem全load出来,要么lazy方式...如果是lazy方式...order又去哪里搞到connection呢?

不是太明白..望赐教..谢谢
17 楼 pf_miles 2008-12-03  
to 楼主:

关于贫血模型的缺点:
引用
“# 将所有的业务放在无状态的service中实际上是一个过程化的设计,它在组织复杂的业务存在天然的劣

势,随着业务的复杂,业务会在service中多个方法间重复。
# 当添加一个新的UI时,很多业务逻辑得重新写。例如,当要提供Web Service的接口时,原先为Web界面

提供的service就很难重用,导致重复的业务逻辑(在贫血模型的分层图中可以看得更清楚),如何保持

业务逻辑一致是很大的挑战。”


这两点我不太赞同,说“业务会在service中多个方法间重复”这可以通过即时的重构解决,如抽取private方法,甚至在“多个service之间重复”时可以抽出包含共用逻辑的类;

第二句话“service就很难重用”我觉得也说不过去,贫血模型中的service是无状态的,可以纯粹看做一段组织好的代码,别的什么地方要用这个逻辑,直接将这段代码inline调用即可,不必担心任何状态相关的问题;总之要在webUI和远程接口之间共用某个service是肯定可以的。

service里的业务逻辑代码会越来越长我赞同,并且正亲身体验中;尤其对于变化很快的某个行业的逻辑;
但是别忘了,上了一定规模的系统,目前还是很少采用领域模型的方式来组织的,原因如楼主描述的“领域模型的缺点”,其实别看小小的几句话,里面涵盖的风险是很大的,特别对于大型系统;或许还因为目前的大型系统一般建立地比较早,而采用了早已成熟的贫血模型,由数据库驱动而来的系统显然贫血模型更加自然,开发人员也好找。

to taowen:

引用
“另外,Repository只应该负责Aggregate Root。对于被Aggregate的对象,应该用Navigation,也就是在

关系之间游走来获取。所以不是所有的查询都必须由Repository来完成”


这么说来repository只用来取root?从概念上讲不错,可是要知道往往一个root下面会有成百上千个node,这么巨大的一坨关系树我们用repository取出来然后在“关系之间游走”...是否有点滑稽?呃...或许我们能想到引入延迟加载,取出来的root下的二级node都用stub来占位;不过这又为污染我们纯洁的领域模型创造了良机...
我想你肯定解决过这类问题,能否多介绍一点?

最后,关于DAO是否该有逻辑,我谈谈我遇到过的情况:
最典型的——一个比较复杂的查询页面,这个页面接收n个参数,比如id,keyword,name,startDate,endDate,dateRangeEnabled。。。。等等等等。。
这n个参数之间还有着这样的三角关系:
当用户输入id时,其余输入参数无效,只按id查询;
如果用户没选中“dateRangeEnabled”,输入的startDate和endDate无效,按其它条件查询;
当用户选中“dateRangeEnabled”时,如果同时也输入了startDate和endDate,那么查询这两个时刻组成的区间;如果只输入了startDate或endDate,就只按>startDate或<endDate查询...
............等等等等........

像这样的查询逻辑,其实就是业务逻辑,并且很难组织到model中,看上去最自然的方式其实也就是写在数据层,由于我们用了某种sql映射框架,这种逻辑一般直接和sql语句待在一起,由该框架提供的一点微弱的脚本编程能力通过传入参数的不同来组装生成不同的查询语句,最后执行。其实跟写在DAO里面是一回事,我们只是觉得利用脚本编程来组织要稍微比在DAO中使用java语言组装sql要省一些代码,其实是一样的,在同一个层次中。

那么,我举的这个例子是否说明DAO不可避免地会有“需要掺杂逻辑”的情况呢?
16 楼 sys53 2008-12-03  
serivce{
Dao dao;
...
public List<A> queryA(){
return dao.find("From A as obj Where obj.name='A'");
}
}
15 楼 fnet 2008-12-03  
terranhao 写道
新手问一下,不用DAO,假如我要条件查询怎么解决?
比如select * from entity as e where e.name=:name and e.id=:id
写在service里面?



放在Service里面也就一句连写
14 楼 狂放不羁 2008-12-03  
taowen 写道


为什么要有Repository?
在我看来,与其说PublicationRepository,不如说Publications。Repository是一个集合对象,它封装了集合的逻辑。因为它具有封装性,所以它应该负责保持这个集合的状态,比如拒绝一些非法的的修改
class PublicationRepository {
  public void save(Publication pub) {
   if (hasSameName(pub)) {
     throw new InvalidPublicationException();
   }
   dao.save(pub);
  }
}



严重同意。repository是与DAO有很大区别的。Dao只是数据库操作接口,但是仓库需要保证领域对象的完整性。
13 楼 terranhao 2008-12-03  
新手问一下,不用DAO,假如我要条件查询怎么解决?
比如select * from entity as e where e.name=:name and e.id=:id
写在service里面?
12 楼 wxl0806 2008-12-02  
正在郁闷软件分层结构呢!受益匪浅!谢谢!
11 楼 sys53 2008-12-02  
DAO层当前应该存在,原因如下:
1,必尽还是关系数据库的时代,还有对于数据库访问不同数据库还是有存在差异,用DAO层实现来解决差异,除非ORM足够强大,根本不存在。
2,DAO分担业务层的逻辑(小逻辑),就如domain层实体里不光是setter,getter的原里一样。但这层逻辑业务仅针对DAO对应的domain层相关逻辑,否则建议到service层。
3,service层应该是主要业务逻辑,不关心应用逻是什么,service的逻辑接口应该永远不了,除非业务改变。个人认为我们的业务主逻辑图都在service层,再配罗dao的小逻辑,(千万不要把hibernate的HQL,或者 相关SQL在这里中写逻辑,初学者误区)。
4,DAO可以认为是大海,有无穷的资源,而service是大海上的船,是dao层上按需所取,船下的海域就是不同dao的小业务逻辑。dao与entity应该是强偶合性的,service以上是松偶合。
5,service不光要dao层,还是分布式,远程访问。就是银行转账的扩展。一般应用都是调用银行接口转账,还有相应的业务关联。转账可能不是一个事务的问题了(当然业务层的设计也可以解决,只是简单举例)
10 楼 ray_linn 2008-12-02  
看了你的代码,有个感觉是无论是DAO或者是Resposity,都是domain logic里的噪声。如果Domain logic也能透明的调用DAO或Respoisty,这样的domain模型会更完美,而且更关注业务。
9 楼 lifethinker 2008-12-02  
bloodrate 写道
第一种方式services就是Fowler说的事务脚本吧

是的。
8 楼 lifethinker 2008-12-02  
ray_linn 写道
"与贫血模型的区别在于Account类中包含业务方法(credit,debit),注意没有set方法,对Account的更新是通过业务方法来更新的。" ...这个应该只对这种简单的类有意义吧。

比如包含数十条信息的AccountInfo类,你是想通过一个带数十个参数的register(...)来完成,还是用数十个Set来完成?


或许我表达的不清楚,我不是不要set方法,而是说所有的方法都必须保证对象的完整性,在DDD中约束是一个很重要的概念。如果Account需要确实直接改变金额,那么set方法就是必需的,但是在set方法内部进行某种约束检测,例如金额不能小于0,这个在现实中不太可能,或许你需要只是一个清零的操作,那么就请只用一个clear方法,这样可以只暴露需要的功能。

当两个属性之间有着某种紧密的关联,那么提供方法来同时设置这两个属性是必要的,而不是两个分别的set方法。
7 楼 fnet 2008-12-02  
越来越觉得DAO是没必要的
通常情况下DAO代码非常少,如果是hibernate,很多时候就一句代码
return getHibernateTemplate().xxxxxx.xxxxx;
为了一句这种代码的封装加一层有点觉得得不偿失,而且对HibernateTemplate再封装之后,代码更少。
多加一层增加了系统的复杂度,无论是维护还是调试都会麻烦一些。
省去了DAO层之后,我们发现,业务Bean更像一个可重用的组件,与其他层没什么依赖。




相关推荐

Global site tag (gtag.js) - Google Analytics