充血模型和贫血模型

贫血模型

概念

只包含数据,不包含业务逻辑的类,就叫作 贫血模型(Anemic Domain Model)

结构

在前后端分离的项目中,服务端基于 MVC 三层架构,分为 :

例:


////////// Controller+VO(View Object) //////////
public class UserController {
  private UserService userService; //通过构造函数或者IOC框架注入
  
  public UserVo getUserById(Long userId) {
    UserBo userBo = userService.getUserById(userId);
    UserVo userVo = [...convert userBo to userVo...];
    return userVo;
  }
}

public class UserVo {//省略其他属性、get/set/construct方法
  private Long id;
  private String name;
  private String cellphone;
}

////////// Service+BO(Business Object) //////////
public class UserService {
  private UserRepository userRepository; //通过构造函数或者IOC框架注入
  
  public UserBo getUserById(Long userId) {
    UserEntity userEntity = userRepository.getUserById(userId);
    UserBo userBo = [...convert userEntity to userBo...];
    return userBo;
  }
}

public class UserBo {//省略其他属性、get/set/construct方法
  private Long id;
  private String name;
  private String cellphone;
}

////////// Repository+Entity //////////
public class UserRepository {
  public UserEntity getUserById(Long userId) { //... }
}

public class UserEntity {//省略其他属性、get/set/construct方法
  private Long id;
  private String name;
  private String cellphone;
}

其中 Bo 是纯粹的数据结构,不包含任何业务逻辑。业务逻辑集中在 Service 中。我们通过 Service 来操作 Bo

同理,EntityVo 都是基于贫血模型设计的。这种贫血模型将数据与操作分离,破坏了面向对象的封装特性,是一种典型的面向过程的编程风格。

基于DDD的充血模型

概念

充血模型(Rich Domain Model)中,数据和对应的业务逻辑被封装到同一个类中。因此,这种充血模型满足面向对象的封装特性,是典型的 面向对象编程 风格。跟基于贫血模型的传统开发模式的区别主要在 Service 层

结构

在基于充血模型的 DDD开发模式 中,Service 层包含 Service 类Domain 类 两部分。
Domain 就相当于贫血模型中的 BO。不过,DomainBO 的区别在于它是基于充血模型开发的,既包含数据,也包含业务逻辑
Service 类变得非常单薄。

总结一下的话就是,基于贫血模型的传统的开发模式,重 ServiceBO;基于充血模型的 DDD开发模式,**轻 ServiceDomain **。

贫血模型泛滥的原因

开发流程

例:基于充血模型的DDD开发虚拟钱包系统

需求

很多具有支付、购买功能的应用(比如淘宝、滴滴出行、极客时间等)都支持钱包的功能。
应用为每个用户开设一个系统内的虚拟钱包账户,支持用户充值、提现、支付、冻结、透支、转赠、查询账户余额、查询交易流水等操作。

钱包功能对应虚拟钱包系统的操作:

数据结构设计

方式二在执行 支付 操作时,需要记录两笔数据。方式一在执行 充值提现 操作时,会有一个字段的空间被浪费。

支付 实际上就是一个转账的操作,在一个账户上加上一定的金额,在另一个账户上减去相应的金额。
我们需要保证加金额和减金额这两个操作,要么都成功,要么都失败。
如果一个成功,一个失败,就会导致数据的不一致,一个账户明明减掉了钱,另一个账户却没有收到钱。

保证数据一致性的方法有很多,比如依赖数据库事务的原子性。
但是如果做了分库分表,为了保证数据的强一致性,利用分布式事务的开源框架,逻辑一般都比较复杂、本身的性能也不高,会影响业务的执行时间。

权衡利弊,我们选择方式一这种稍微有些冗余的数据格式设计思路。

虚拟钱包支持的操作,仅仅是余额的加加减减操作,不涉及复杂业务概念,职责单一、功能通用。
如果耦合太多业务概念到里面,势必影响系统的通用性,而且还会导致系统越做越复杂。
因此,我们不希望将充值、支付、提现这样的业务概念添加到虚拟钱包系统中。

我们不应该在 虚拟钱包系统交易流水 中记录交易类型,而应该将交易类型记录在 钱包交易流水 中:

我们通过查询上层 钱包系统 的交易流水信息,去满足用户查询交易流水的功能需求,而 虚拟钱包 中的交易流水就只是用来解决数据一致性问题。实际上,它的作用还有很多,比如用来对账等。

代码设计


public class VirtualWalletController {
  // 通过构造函数或者IOC框架注入
  private VirtualWalletService virtualWalletService;
  
  public BigDecimal getBalance(Long walletId) { ... } //查询余额
  public void debit(Long walletId, BigDecimal amount) { ... } //出账
  public void credit(Long walletId, BigDecimal amount) { ... } //入账
  public void transfer(Long fromWalletId, Long toWalletId, BigDecimal amount) { ...} //转账
}


public class VirtualWallet { // Domain领域模型(充血模型)
  private Long id;
  private Long createTime = System.currentTimeMillis();;
  private BigDecimal balance = BigDecimal.ZERO;
  
  public VirtualWallet(Long preAllocatedId) {
    this.id = preAllocatedId;
  }
  
  public BigDecimal balance() {
    return this.balance;
  }
  
  public void debit(BigDecimal amount) {
    if (this.balance.compareTo(amount) < 0) {
      throw new InsufficientBalanceException(...);
    }
    this.balance.subtract(amount);
  }
  
  public void credit(BigDecimal amount) {
    if (amount.compareTo(BigDecimal.ZERO) < 0) {
      throw new InvalidAmountException(...);
    }
    this.balance.add(amount);
  }
}

public class VirtualWalletService {
  // 通过构造函数或者IOC框架注入
  private VirtualWalletRepository walletRepo;
  private VirtualWalletTransactionRepository transactionRepo;
  
  public VirtualWallet getVirtualWallet(Long walletId) {
    VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
    VirtualWallet wallet = convert(walletEntity);
    return wallet;
  }
  
  public BigDecimal getBalance(Long walletId) {
    return walletRepo.getBalance(walletId);
  }
  
  public void debit(Long walletId, BigDecimal amount) {
    VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
    VirtualWallet wallet = convert(walletEntity);
    wallet.debit(amount);
    walletRepo.updateBalance(walletId, wallet.balance());
  }
  
  public void credit(Long walletId, BigDecimal amount) {
    VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
    VirtualWallet wallet = convert(walletEntity);
    wallet.credit(amount);
    walletRepo.updateBalance(walletId, wallet.balance());
  }
  
  public void transfer(Long fromWalletId, Long toWalletId, BigDecimal amount) {
      VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactionEntity();
      transactionEntity.setAmount(amount);
      transactionEntity.setCreateTime(System.currentTimeMillis());
      transactionEntity.setFromWalletId(fromWalletId);
      transactionEntity.setToWalletId(toWalletId);
      transactionEntity.setStatus(Status.TO_BE_EXECUTED);
      Long transactionId = transactionRepo.saveTransaction(transactionEntity);
      try {
        debit(fromWalletId, amount);
        credit(toWalletId, amount);
      } catch (InsufficientBalanceException e) {
        transactionRepo.updateStatus(transactionId, Status.CLOSED);
        ...rethrow exception e...
      } catch (Exception e) {
        transactionRepo.updateStatus(transactionId, Status.FAILED);
        ...rethrow exception e...
      }
      transactionRepo.updateStatus(transactionId, Status.EXECUTED);
    }
}

其中 Service 类承担的职责:

Controller 层和 Repository 层没有必要进行充血领域建模