银行转账事务脚本实现方式
//两个账号间,转账金额计算逻辑全部在MoneyTransferService中,Account仅仅是数据载体
public class MoneyTransferServiceTransactionScriptImpl implements MoneyTransferService {
private AccountDao accountDao; private BankingTransactionRepository bankingTransactionRepository; @Override public BankingTransaction transfer(String fromAccountId, String toAccountId, double amount) { Account fromAccount = accountDao.findById(fromAccountId); Account toAccount = accountDao.findById(toAccountId); double newBalance = fromAccount.getBalance() - amount; //是否支持透支 switch (fromAccount.getOverdraftPolicy()) { case NEVER: if (newBalance < 0) { throw new DebitException("Insufficient funds"); } break; case ALLOWED: if (newBalance < -limit) { throw new DebitException( "Overdraft limit (of " + limit + ") exceeded: " + newBalance); } break; } fromAccount.setBalance(newBalance); toAccount.setBalance(toAccount.getBalance() + amount); BankingTransaction moneyTransferTransaction = new MoneyTranferTransaction(fromAccountId, toAccountId, amount); bankingTransactionRepository.addTransaction(moneyTransferTransaction); return moneyTransferTransaction; } }
银行转账DDD实现
public class Account { // @Id private String id; private double balance; private OverdraftPolicy overdraftPolicy; . . . public double balance() { return balance; } public void debit(double amount) { this.overdraftPolicy.preDebit(this, amount); this.balance = this.balance - amount; this.overdraftPolicy.postDebit(this, amount); } public void credit(double amount) { this.balance = this.balance + amount; }
//构造器
public Account() {
//构造器中根据XX创建透支策略
if(){
this.overdraftPolicy = new NoOverdraftAllowed();
}else{
this.overdraftPolicy = new XXXX();
}
}
}
透支策略OverdraftPolicy
不仅仅是一个Enum了,而是被抽象成包含了业务规则并采用了策略模式的对象。
public interface OverdraftPolicy{ void preDebit(Account account, double amount); void postDebit(Account account, double amount); } public class NoOverdraftAllowed implements OverdraftPolicy { public void preDebit(Account account, double amount) { double newBalance = account.balance() - amount; if (newBalance < 0) { throw new DebitException("Insufficient funds"); } } public void postDebit(Account account, double amount) {
} }
public class LimitedOverdraft implements OverdraftPolicy { private double limit; public void preDebit(Account account, double amount) { double newBalance = account.balance() - amount; if (newBalance < -limit) { throw new DebitException("Overdraft limit (of " + limit + ") exceeded: " + newBalance); } } public void postDebit(Account account, double amount) {
} }
Domain Service 只需要调用相关的Entity对象完成业务
public class MoneyTransferServiceDomainModelImpl implements MoneyTransferService { private AccountRepository accountRepository; private BankingTransactionRepository bankingTransactionRepository; @Override public BankingTransaction transfer(String fromAccountId, String toAccountId, double amount) { Account fromAccount = accountRepository.findById(fromAccountId); Account toAccount = accountRepository.findById(toAccountId); fromAccount.debit(amount); toAccount.credit(amount); BankingTransaction moneyTransferTransaction = new MoneyTranferTransaction(fromAccountId, toAccountId, amount); bankingTransactionRepository.addTransaction(moneyTransferTransaction); return moneyTransferTransaction; } }
对比事务脚本,使用领域模型优势
面向对象
- 封装:Account 账户相关操作,转出/传入操作封装在Account对象内,
- 多态:策略模式的OveraftPolicy,提高代码拓展性
业务语义显性化
- 显性化:将隐式业务逻辑从if...else中抽离出来,用通用语言去写代码,扩展,变成显性概念。举例说明,比如”透支策略“是账户Account的一种属性。使用事务脚本,其含义
完全淹没在代码中,没有凸显出来
- 通用语言:“一个团队,一种语言”,将模型作为语言的支柱。确保团队在内部的所有交流中,代码中,画图,写东西,特别是讲话的时候都要使用这种语言。例如账号,转账,透支策略,这些都是非常重要的领域概念,如果这些命名都和我们日常讨论以及PRD中的描述保持一致,将会极大提升代码的可读性,减少认知成本。
领域服务
有些领域动作,是一些动词,但是却不属于任何对象。它们代表了领域中的一个重要行为,所以不能忽略它们或简单把它们合并到某个实体或者值对象中。当这样的行为从领域中被识别出来,最佳方式是将它们声明成一个服务。它的作用是为了领域提供相应的功能。
Service通常以一个活动来命名,而不是Entity来命名。以转账为例,转账的这个行为就是一个领域概念,但是是发生在两个账号之间,归属于账号Entity并不合适,因为一个账号Entity没必要去关联他转账的账号Entity。这种情况下使用MoneyTransferDomainService更合适。
识别领域服务,主要看是否满足以下三个特征
- 服务执行的操作代表了一个领域概念,这个领域概念无法自然的归属于一个实体或者值对象
- 被执行的操作涉及到领域中的其他对象
- 操作是无状态的