前言:
最近公司架构师一直在组织关于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接口保持业务逻辑的一致(从领域模型的分层图可以看得更清楚)。
其缺点是:
- 对程序员的要求较高,初学者对这种将职责分配到多个协作对象中的方式感到极不适应。
- 领域驱动建模要求对领域模型完整而透彻的了解,只给出一个用例的实现步骤是无法得到领域模型的,这需要和领域专家的充分讨论。错误的领域模型对项目的危害非常之大,而实现一个好的领域模型非常困难。
- 对于简单的软件,使用领域模型,显得有些杀鸡用牛刀了。