一、贫血模型
1.1 概念
以MVC的entity或domain为例,这种只包含数据不包含逻辑业务的类就叫做贫血模型(Anemic Domain Model)。贫血模型将数据与操作分离,破坏了面向对象的封装特性,是一种典型的面向过程编程。
1.2 场景
- 常用的基于CRUD的MVC三层架构。
1.3 优点
- 基于贫血模型的传统的开发模式,比较适合业务比较简单的系统开发
- 贫血模型要比充血模型更加有简单、易上手。因为充血模型是一种面向对象的编程风格。我们从一开始就要设计好针对数据要暴露哪些操作,定义哪些业务逻辑。而不是像贫血模型那样,我们只需要定义数据,之后有什么功能开发需求,我们就在 Service 层定义什么操作,不需要事先做太多设计。
二、充血模型
2.1 概念
2.1.1 领域驱动设计
领域驱动设计(Domain Driven Design,简称 DDD)主要是用来指导如何解耦业务系统,划分业务模块,定义业务领域模型及其交互,注意:做好领域驱动设计的关键是对业务的熟悉程度,如果对业务不熟悉即使再熟悉领域驱动设计的概念也无法很好的设计出合理的领域设计。
2.1.2 充血模型
在贫血模型中,数据和业务逻辑被分割到不同的类中。充血模型(Rich Domain Model)正好相反,数据和对应的业务逻辑被封装到同一个类中。因此,这充血模型满足面向对象的封装特性,是典型的面向对象编程风格。
2.2 场景
- 包含各种利息计算模型、还款模型等复杂业务的金融系统。
2.3 优点
- 基于充血模型的 DDD 开发模式,更适合业务复杂的系统开发,
三、实战
3.1 需求介绍
需求:为每个用户开设一个系统内的虚拟钱包账户,支持用户充值、提现、支付、冻结、透支、转赠、查询账户余额、查询交易流水等操作。主要功能包括:充值、提现、支付、查询余额、查询交易流水;
- 充值:用户通过三方支付渠道,把自己银行卡账户内的钱,充值到虚拟钱包账号中。这整个过程,我们可以分解为三个主要的操作流程:第一个操作是从用户的银行卡账户转账到应用的公共银行卡账户;第二个操作是将用户的充值金额加到虚拟钱包余额上;第三个操作是记录刚刚这笔交易流水。
- 支付:用户用钱包内的余额,支付购买应用内的商品。实际上,支付的过程就是一个转账的过程,从用户的虚拟钱包账户划钱到商家的虚拟钱包账户上。除此之外,我们也需要记录这笔支付的交易流水信息。
- 提现:用户还可以将虚拟钱包中的余额,提现到自己的银行卡中。这个过程实际上就是扣减用户虚拟钱包中的余额,并且触发真正的银行转账操作,从应用的公共银行账户转钱到用户的银行账户。同样,我们也需要记录这笔提现的交易流水信息。
- 查询余额:看一下虚拟钱包中的余额数字
- 查询交易流水:只支持三种类型的交易流水:充值、支付、提现。在用户充值、支付、提现的时候,我们会记录相应的交易信息。在需要查询的时候,我们只需要将之前记录的交易流水,按照时间、类型等条件过滤之后,显示出来即可。
3.2 设计
我们可以把整个钱包系统的业务划分为两部分,其中一部分单纯跟应用内的虚拟钱包账户打交道,另一部分单纯跟银行账户打交道。我们基于这样一个业务划分,给系统解耦,将整个钱包系统拆分为两个子系统:虚拟钱包系统和三方支付系统。
3.2.1 实战公共部分
//Controller层 @RestController("/VirtualWallect") public class VirtualWallectController { @Autowired private VirtualWallectService service; /** * 查询余额 * * @param wallectId * @return */ @RequestMapping(value = "/GetBalance", method = {RequestMethod.GET, RequestMethod.POST}) public BigDecimal getBalance(Long wallectId) { return service.getBalance(wallectId); } /** * 出账 * * @param wallectId * @param amount */ @PostMapping(value = "/Debit") public void debit(Long wallectId, BigDecimal amount) throws NoSufficientBalanceException, InsufficientBalanceException { service.debit(wallectId, amount); } /** * 入账 * * @param wallectId * @param amount */ @PostMapping(value = "/Crebit") public void credit(Long wallectId, BigDecimal amount) throws InvalidAmountException { service.credit(wallectId, amount); } /** * 转账 * * @param fromWallectId * @param toWallectId * @param amount */ @PostMapping(value = "/Tranfer") public void tranfer(Long fromWallectId, Long toWallectId, BigDecimal amount) throws NoSufficientBalanceException, InsufficientBalanceException, InvalidAmountException { service.tranfer(fromWallectId, toWallectId, amount); } } public enum TransactionType { DEBIT, CREDIT, TRANSFER; } //service层 public interface VirtualWallectService { /** * 查询余额 * * @param wallectId * @return */ BigDecimal getBalance(Long wallectId); /** * 出账 * * @param wallectId * @param amount */ void debit(Long wallectId, BigDecimal amount) throws NoSufficientBalanceException, InsufficientBalanceException; /** * 入账 * * @param wallectId * @param amount */ void credit(Long wallectId, BigDecimal amount) throws InvalidAmountException; /** * 转账 * * @param fromWallectId * @param toWallectId * @param amount */ void tranfer(Long fromWallectId, Long toWallectId, BigDecimal amount) throws NoSufficientBalanceException, InsufficientBalanceException, InvalidAmountException; } //TODO Service实现类和BO部分 // DAO层 public interface VirtualWalletRepository { BigDecimal getBanlance(Long wallectId); VirtualWalletEntity getWallectEntity(Long wallectId); void updateBalance(Long wallectId, BigDecimal subtract); } public interface VirtualWalletTransactionRepository { void saveTransaction(VirtualWalletTransactionEntity transactionEntity); }
3.3 基于贫血模型实现MVC
3.3.1 开发模式
基于贫血模型的开发模式:大部分都是 SQL 驱动(SQL-Driven)的开发模式。我们接到一个后端接口的开发需求的时候,就去看接口需要的数据对应到数据库中,需要哪张表或者哪几张表,然后思考如何编写 SQL 语句来获取数据。之后就是定义 Entity、BO、VO,然后模板式地往对应的 Repository、Service、Controller 类中添加代码。业务逻辑包裹在一个大的 SQL 语句中,而 Service 层可以做的事情很少。SQL 都是针对特定的业务功能编写的,复用性差。
3.3.2 实战
/** * AnemiaVirtualWallectServiceImpl类 * * 基于贫血模型的设计:方法和数据分离 * */ @Service("AnemiaVirtualWallectServiceImpl") public class AnemiaVirtualWallectServiceImpl implements VirtualWallectService { @Autowired private VirtualWalletRepository walletRepo; @Autowired private VirtualWalletTransactionRepository transactionRepo; @Override public BigDecimal getBalance(Long wallectId) { return walletRepo.getBanlance(wallectId); } @Transactional @Override public void debit(Long wallectId, BigDecimal amount) throws NoSufficientBalanceException { VirtualWalletEntity entity = walletRepo.getWallectEntity(wallectId); BigDecimal balance = entity.getBalance(); if (balance.compareTo(amount) < 0) { throw new NoSufficientBalanceException("Failed to debit, the reason is the virtual wallet balance is less than amount."); } VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactionEntity(); transactionEntity.setAmount(amount); transactionEntity.setCreateTime(System.currentTimeMillis()); transactionEntity.setType(TransactionType.DEBIT); transactionEntity.setFromWalletId(wallectId); transactionRepo.saveTransaction(transactionEntity); walletRepo.updateBalance(wallectId, balance.subtract(amount)); } @Transactional @Override public void credit(Long wallectId, BigDecimal amount) { VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactionEntity(); transactionEntity.setAmount(amount); transactionEntity.setCreateTime(System.currentTimeMillis()); transactionEntity.setType(TransactionType.CREDIT); transactionEntity.setFromWalletId(wallectId); transactionRepo.saveTransaction(transactionEntity); VirtualWalletEntity entity = walletRepo.getWallectEntity(wallectId); BigDecimal balance = entity.getBalance(); walletRepo.updateBalance(wallectId, balance.add(amount)); } @Transactional @Override public void tranfer(Long fromWallectId, Long toWallectId, BigDecimal amount) throws NoSufficientBalanceException { VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactionEntity(); transactionEntity.setAmount(amount); transactionEntity.setCreateTime(System.currentTimeMillis()); transactionEntity.setType(TransactionType.TRANSFER); transactionEntity.setFromWalletId(fromWallectId); transactionEntity.setToWalletId(toWallectId); transactionRepo.saveTransaction(transactionEntity); debit(fromWallectId,amount); credit(toWallectId,amount); } } @Getter @Setter public class VirtualWallet { private Long id; private Long createTime; private BigDecimal balance; }
3.4 基于充血模型实现MVC
实际上,基于充血模型的 DDD 开发模式实现的代码,也是按照 MVC 三层架构分层的。Controller 层还是负责暴露接口,Repository 层还是负责数据存取,Service 层负责核心业务逻辑。它跟基于贫血模型的传统开发模式的区别主要在 Service 层。
在基于贫血模型的传统开发模式中,Service 层包含 Service 类和 BO 类两部分,BO 是贫血模型,只包含数据,不包含具体的业务逻辑。业务逻辑集中在 Service 类中。在基于充血模型的 DDD 开发模式中,Service 层包含 Service 类和 Domain 类两部分。Domain 就相当于贫血模型中的 BO。不过,Domain 与 BO 的区别在于它是基于充血模型开发的,既包含数据,也包含业务逻辑。而 Service 类变得非常单薄。
3.4.1 开发模式
基于充血模型的 DDD 的开发模式,那对应的开发流程就完全不一样了。在这种开发模式下,我们需要事先理清楚所有的业务,定义领域模型所包含的属性和方法。领域模型相当于可复用的业务中间层。新功能需求的开发,都基于之前定义好的这些领域模型来完成。我们知道,越复杂的系统,对代码的复用性、易维护性要求就越高,我们就越应该花更多的时间和精力在前期设计上。而基于充血模型的 DDD 开发模式,正好需要我们前期做大量的业务调研、领域模型设计,所以它更加适合这种复杂系统的开发。
3.4.2 实战
/** * CongestiveVirtualWallectServiceImpl类 * * 基于充血模型的设计:方法和数据在一起 * */ @Service("CongestiveVirtualWallectServiceImpl") public class CongestiveVirtualWallectServiceImpl implements VirtualWallectService { @Autowired private VirtualWalletRepository walletRepo; @Autowired private VirtualWalletTransactionRepository transactionRepo; @Override public BigDecimal getBalance(Long wallectId) { return walletRepo.getBanlance(wallectId); } @Transactional @Override public void debit(Long wallectId, BigDecimal amount) throws NoSufficientBalanceException, InsufficientBalanceException { VirtualWalletEntity entity = walletRepo.getWallectEntity(wallectId); VirtualWallet wallet = covert(entity); wallet.debit(amount); VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactionEntity(); transactionEntity.setAmount(amount); transactionEntity.setCreateTime(System.currentTimeMillis()); transactionEntity.setType(TransactionType.DEBIT); transactionEntity.setFromWalletId(wallectId); transactionRepo.saveTransaction(transactionEntity); walletRepo.updateBalance(wallectId, wallet.balance()); } @Transactional @Override public void credit(Long wallectId, BigDecimal amount) throws InvalidAmountException { VirtualWalletEntity entity = walletRepo.getWallectEntity(wallectId); VirtualWallet wallet = covert(entity); wallet.cerbit(amount); VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactionEntity(); transactionEntity.setAmount(amount); transactionEntity.setCreateTime(System.currentTimeMillis()); transactionEntity.setType(TransactionType.CREDIT); transactionEntity.setFromWalletId(wallectId); transactionRepo.saveTransaction(transactionEntity); walletRepo.updateBalance(wallectId, wallet.balance()); } @Transactional @Override public void tranfer(Long fromWallectId, Long toWallectId, BigDecimal amount) throws NoSufficientBalanceException, InsufficientBalanceException, InvalidAmountException { VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactionEntity(); transactionEntity.setAmount(amount); transactionEntity.setCreateTime(System.currentTimeMillis()); transactionEntity.setType(TransactionType.TRANSFER); transactionEntity.setFromWalletId(fromWallectId); transactionEntity.setToWalletId(toWallectId); transactionRepo.saveTransaction(transactionEntity); debit(fromWallectId,amount); credit(toWallectId,amount); } private VirtualWallet covert(VirtualWalletEntity entity) { VirtualWallet result = new VirtualWallet(entity.getId(),entity.getCreateTime(),entity.getBalance()); return result; } } /** * VirtualWallet类 - Domain领域模型(充血模型) * */ public class VirtualWallet { //虚拟钱包账号 private Long id; private Long createTime = System.currentTimeMillis(); //余额 private BigDecimal balance = BigDecimal.ZERO; //是允许透支 private boolean isAllowedOverdraft = true; //透支额度 private BigDecimal overdraftAmount = BigDecimal.ZERO; //冻结额度 private BigDecimal frozenAmount = BigDecimal.ZERO; public VirtualWallet(Long preAllocatedId) { this.id = preAllocatedId; } public VirtualWallet(Long id, Long createTime, BigDecimal balance) { this.id = id; this.createTime = createTime; this.balance = balance; } public BigDecimal balance() { return balance; } /** * 出账 * * @param amount * @throws NoSufficientBalanceException */ public void debit(BigDecimal amount) throws InsufficientBalanceException { BigDecimal totalAvaliableBalance = getAvaliableBalance(); if (totalAvaliableBalance.compareTo(amount) < 0) { throw new InsufficientBalanceException("Failed to debit, the reason is the virtual wallet balance is less than amount."); } this.balance = balance.subtract(amount); } /** * 入账 * * @param amount * @throws NoSufficientBalanceException */ public void cerbit(BigDecimal amount) throws InvalidAmountException { if (amount.compareTo(BigDecimal.ZERO) < 0) { throw new InvalidAmountException("Failed to cerbit, the reason is the amount is less than zero."); } this.balance = balance.add(amount); } /** * 冻结部分余额 * * @param amount */ public void freeze(BigDecimal amount) { } /** * 解冻部分余额 * * @param amount */ public void unfreeze(BigDecimal amount) { } /** * 增加透支额度 * * @param amount */ public void increaseOverdraftAmount(BigDecimal amount) { } /** * 减少透支额度 * * @param amount */ public void decreaseOverdraftAmount(BigDecimal amount) { } /** * 开启透支 * */ public void openOverdraft() { } /** * 关闭透支 * */ public void closeOverdraft() { } public BigDecimal getAvaliableBalance() { BigDecimal totalAvaliableBalance = this.balance.subtract(this.frozenAmount); if (isAllowedOverdraft) { totalAvaliableBalance.add(overdraftAmount); } return totalAvaliableBalance; } public void setBalance(BigDecimal balance) { this.balance = balance; } public void setCreateTime(Long createTime) { this.createTime = createTime; } }
四、总结
基于贫血模型的传统的开发模式,重 Service 轻 BO;基于充血模型的 DDD 开发模式,轻 Service 重 Domain。
在基于充血模型的 DDD 开发模式,Service的主要功能有三个:
- Service 类负责与 Repository 交流。为了保持领域模式VirtualWallet的独立性,不与任何其他层的代码(Repository 层的代码)或开发框架(比如 Spring、MyBatis)耦合在一起,一般需要Service层将流程性的代码逻辑(比如从 DB 中取数据、映射数据)与领域模型的业务逻辑解耦,从而让领域模型更加可复用。
- Service 类负责跨领域模型的业务聚合功能。比如:VirtualWalletService 类中的 transfer() 转账函数会涉及两个钱包的操作,因为这部分业务逻辑无法放到 VirtualWallet 类中,所以我们暂且把转账业务放到 VirtualWalletService 类中了。
- Service 类负责一些非功能性及与三方系统交互的工作。比如幂等、事务、发邮件、发消息、记录日志、调用其他系统的 RPC 接口等,都可以放到 Service 类中。