领域驱动设计系列贫血模型和充血模型

面向过程的设计方式(贫血模型)

假设现在有一个银行支付系统项目,其中的一个重要的业务用例是账户转账业务。系统使用迭代的方式进行开发,在1.0版本中,该用例的功能需求非常简单,事件流描述如下:

主事件流:

  • 用户登录银行的在线支付系统
  • 选择用户在该银行注册的网上银行账户
  • 选择需要转账的目标账户,输入转账金额,申请转账
  • 银行系统检查转出账户的金额是否足够
  • 从转出账户中扣除转出金额(debit),更新转出账户的余额
  • 把转出金额加入到转入账户中(credit),更新转入账户的余额

    备选事件流:

  • 如果转出账户中的余额不足,转账失败,返回错误信息
  • 设计方案如下(忽略展示层部分):

1.设计一个账户交易服务接口AccountingService,设计一个服务方法transfer(),并提供一个具体实现类AccountingServiceImpl,所有账户交易业务的业务逻辑都置于该服务类中
2.提供一个AccountInfo和一个Account,前者是一个用于与展示层交换账户数据的账户数据传输对象,后者是一个账户实体(相当于一个EntityBean),这两个对象都是普通的JavaBean,具有相关属性和简单的get/set方法。
下面是AccountingServiceImpl.transfer()方法的实现逻辑(伪代码):

public class AccountingServiceImpl implements AccountingService {
    public void transfer(Long srcAccountId, Long destAccountId, BigDecimal amount) throws AccountingServiceException {
        Account srcAccount = accountRepository.getAccount(srcAccountId);
        Account destAccount = accountRepository.getAccount(destAccountId);
        if(srcAccount.getBalance().compareTo(amount)

可以看到,由于1.0版本的功能需求非常简单,按面向过程的设计方式,把所有业务代码置于​​AccountingServiceImpl​​中完全没有问题,这时候,新需求来了,在1.0.1版本中,需要为账户转账业务增加如下功能,在转账时,首先需要判断账户是否可用,然后,账户的余额还要分成两部分:冻结部分和活跃部分,处于冻结部分的金额不能用于任何交易业务,我们来看看变更后的代码:

public class AccountingServiceImpl implements AccountingService {
    public void transfer(Long srcAccountId,Long destAccountId,BigDecimal amount) throws AccountingServiceException {
        Account srcAccount = accountRepository.getAccount(srcAccountId);
        Account destAccount = accountRepository.getAccount(destAccountId);
        if(!srcAccount.isActive() || !destAccount.isActive())
            throw new AccountingServiceException(AccountingService.ACCOUNT_IS_NOT_AVAILABLE);
        BigDecimal availableAmount = srcAccount.getBalance().substract(srcAccount.getFrozenAmount());
        if(availableAmount.compareTo(amount)

可以看到,情况变得稍微复杂了,这时候,1.0.2的需求又来了,需要在每次交易成功后,创建一个交易明细账,于是,我们又必须在​​transfer()​​方面里面增加创建并持久化交易明细账的业务逻辑:

AccountTransactionDetails details= new AccountTransactionDetails(…);
accountRepository.save(details);

业务需求不断复杂化:账户每笔转账的最大额度需要由其信用指数确定、需要根据银行的手续费策略计算并扣除一定的手续费用……,随着业务的复杂化,transfer()方法的逻辑变得越来越复杂,逐渐形成了上文所述的成百上千行代码。有经验的程序员可能会做出类此“方法抽取”的重构,把转账业务按逻辑划分成若干块:判断余额是否足够、判断账户的信用指数以确定每笔最大转账金额、根据银行的手续费策略计算手续费、记录交易明细账……,从而使代码更加结构化。这是一个好的开始,但还是显然不足。

假设某一天,系统需求增加一个新的模块,为系统增加一个网上商城,让银行用户可以进行在线购物,而在线购物也存在着很多与账户贷记借记业务相同或相似的业务逻辑:判断余额是否足够、对账户进行借贷操作(credit/debit)以改变余额、收取手续费用、产生交易明细账……

面对这种情况,有两种解决办法:

  • 把AccountingServiceImpl中的相同逻辑拷贝到OnlineShoppingServiceImplementation中​​
  • 让OnlineShoppingServiceImpl调用AccountingServiceImpl的相同服务​​

显然,第二种方法比第一种方法更好,结构更清晰,维护更容易。但问题在于,这样就会形成网上商城服务模块与账户收支服务模块的不必要的依赖关系,系统的耦合度高了,如果系统为了更灵活的伸缩性,让每个大业务模块独立进行部署,还需要因为两者的依赖关系建立分布式调用,这无疑增加了设计、开发和运维的成本

有经验的设计人员可能会发现第三种解决办法:把相同的业务逻辑抽取成一个新的服务,作为公共服务同时供上述两个业务模块使用。这就是笔者将会马上讨论的方案——使用领域驱动设计

面向对象的领域驱动设计方式(充血模型)

领域驱动设计的一个重要的概念是领域模型,首先,我们根据业务领域抽象出以下核心业务对象模型:
领域驱动设计系列贫血模型和充血模型插图

  • Account:账户,是整个系统的最核心的业务对象,它包括以下属性:对象标识、账户号、是否有效标识、余额、冻结金额、账户交易明细集合、账户信用等级。
  • AccountTransactionDetails:账户交易明细,它从属于账户,每个账户有多个交易明细,它包括以下属性:对象标识、所属账户、交易类型、交易发生金额、交易发生时间。
  • AccountCreditDegree:账户信用等级,它用于限制账户的每笔交易发生金额,包含以下属性:对象标识、对应账户、信用指数。
  • BankTransactionFeeCalculator:银行交易手续费用计算器,它包含一个常量:每笔交易的手续费上限。

我们知道,领域对象除了具有自身的属性和状态之外,它的一个很重要的标志是,它具有属于自己职责范围之内的行为,这些行为封装了其领域内的领域业务逻辑。于是,我们进行进一步的建模,根据业务需求为领域对象设计业务方法:
领域驱动设计系列贫血模型和充血模型插图1
根据职责单一的原则,我们把功能需求中描述的功能合理的分配到不同的领域对象中:

  • Account:
    • credit:向银行账户存入金额,贷记
    • debit:从银行账户划出金额,借记
    • transferTo:把固定金额转入指定账户
    • createTransactionDetails:创建交易明细账
    • updateCreditIndex:更新账户的信用指数
  • AccountCreditDegree:
    • getMaxTransactionAmount:获取所属账户的每笔交易最大金额
    • BankTransactionFeeCalculator:
    • calculateTransactionFee:根据交易信息计算该笔交易的手续费

经过这样的设计,前例中所有放置在服务对象的业务逻辑被分别划入不同的负责相关职责的领域对象当中,下面的时序图描述了AccountingServiceImpl的转账业务的实现逻辑(为了简化逻辑,忽略掉事物、持久化等逻辑):
领域驱动设计系列贫血模型和充血模型插图2
再看看AccountingServiceImpl.transfer()的实现逻辑:

public class AccountingServiceImpl implements AccountingService {
    public void transfer(Long srcAccountId,Long destAccountId,BigDecimal amount) throws AccountDomainException {
        Account srcAccount = accountRepository.getAccount(srcAccountId);
        Account destAccount = accountRepository.getAccount(destAccountId);
        srcAccount.transferTo(destAccount,amount);
    }
}

我们可以看到,上例那些复杂的业务逻辑:判断余额是否足够、判断账户是否可用、改变账户余额、计算手续费、判断交易额度、产生交易明细账……,都不再存在于​​AccountingServiceImplementation​​​的​​transfer​​​方法中,它们被委派给负责这些业务的领域对象的业务方法中去,Account中有两个方法被声明为protected,因为他们是在debit和credit方法被调用时,由这两个方法调用的,对于​​AccountingServiceImpl​​来说,由于产生交易明细(createTransactionDetails)和更新账户信用指数(updateCreditIndex)都不属于其职责范围,它不需要也无权使用这些逻辑。

使用领域驱动设计至少会带来下述优点:

  • 业务逻辑被合理的分散到不同的领域对象中,代码结构更加清晰,可读性,可维护性更高。
  • 对象职责更加单一,内聚度更高。
  • 复杂的业务模型可以通过领域建模(UML是一种主要方式)清晰的表达,开发人员甚至可以在不读源码的情况下就能了解业务和系统结构,这有利于对现存的系统进行维护和迭代开发。

再看看如果这时需要加入网上商城的一个新的模块,开发人员需要怎么去做,还记得上面提过的第三种方案吗?就是把账户贷记和借记的相关业务抽取到成一个公共服务,同时供银行在线支付系统和网上商城系统服务,其实这个公共的服务,本质上就是这些具有领域逻辑的领域对象:​​Account、AccountCreditDegree……​​,由此我们又可以发现领域驱动设计的另一大优点:

  • 系统高度模块化,代码重用度高,不会出现太多的重复逻辑。

文章来源于互联网:领域驱动设计系列贫血模型和充血模型

THE END
分享
二维码