学到贫血之-贫血模型和充血模型
学习自:设计模式之美
1 基于贫血模型的传统开发模式
// 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;
}
UserEntity 和 UserRepository 组成了数据访问层,UserBo 和 UserService 组成了业务逻辑层,UserVo 和 UserController 在这里属于接口层。
UserBo 是一个纯粹的数据结构,只包含数据,不包含任何业务逻辑。业务逻辑集中在UserService 中,通过 UserService 来操作 UserBo。换句话说,Service 层的数据和业务逻辑,被分割为 BO 和 Service 两个类中。
像 UserBo 这样,只包含数据,不包含业务逻辑的类,就叫作贫血模型(Anemic Domain Model)。同理,UserEntity、UserVo 都是基于贫血模型设计的。这种贫血模型将数据与操作分离,破坏了面向对象的封装特性,是一种典型的面向过程的编程风格。
2 基于充血模型的 DDD 开发模式
充血模型(Rich Domain Model)将数据和对应的业务逻辑封装到同一个类中。这种模型满足面向对象的封装特性,是典型的面向对象编程风格。
基于充血模型的 DDD 开发模式实现的代码,也是按照 MVC 三层架构分层的。Controller 层还是负责暴露接口,Repository 层还是负责数据存取,Service 层负责核心业务逻辑。它跟基于贫血模型的传统开发模式的区别主要在 Service 层。
基于充血模型的 DDD 开发模式中,Service 层包含 Service 类和 Domain 类两部分。Domain 就相当于贫血模型中的 BO。不过,Domain 与 BO 的区别在于它既包含数据,也包含业务逻辑。而 Service 类变得非常单薄。
@Data
@Slf4j
public class Order extends BaseDomain {/*** 订单ID*/private String orderNo;...// 也有业务处理逻辑public static Order readFromDO(OrderDO orderDO) {Order result = new Order();return result;}public OrderDO convertToDO() {OrderDO result = new OrderDO();return result;}public OrderDTO convertToDTO() {OrderDTO result = new OrderDTO();return result;}public void putExt(String key, String value) {}
}
3 开发一个虚拟钱包系统
3.1 需求背景
每个用户开设一个系统内的虚拟钱包账户,支持用户充值、提现、支付、冻结、透支、转赠、查询账户余额、查询交易流水等操作。
每个虚拟钱包账户都会对应用户的一个真实的支付账户,有可能是银行卡账户,也有可能是三方支付账户(比如支付宝、微信钱包)。
整个钱包系统的业务划分为两部分,其中一部分单纯跟应用内的虚拟钱包账户打交道,另一部分单纯跟银行账户打交道。基于这样一个业务划分,给系统解耦,将整个钱包系统拆分为两个子系统:虚拟钱包系统和三方支付系统。
钱包的核心功能
3.2 基于贫血模型的传统开发模式
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) { ...} //转账//省略查询transaction的接口
}// Service 和 BO 负责核心业务逻辑
public class VirtualWalletBo {private Long id;private Long createTime;private BigDecimal balance;
}public class VirtualWalletService {private VirtualWalletRepository walletRepo;private VirtualWalletTransactionRepository transactionRepo;public VirtualWalletBo getVirtualWallet(Long walletId) {VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);VirtualWalletBo walletBo = convert(walletEntity);return walletBo;}public BigDecimal getBalance(Long walletId) {return walletRepo.getBalance(walletId);}@Transactionalpublic void debit(Long walletId, BigDecimal amount) {...}@Transactionalpublic void credit(Long walletId, BigDecimal amount) {...}@Transactionalpublic void transfer(Long fromWalletId, Long toWalletId, BigDecimal amount) {...}
}
3.3 基于充血模型的 DDD 开发模式
虚拟钱包 VirtualWallet 类设计成一个充血的 Domain 领域模型,并且将原来在 Service 类中的部分业务逻辑移动到 VirtualWallet 类中,让 Service 类的实现依赖 VirtualWallet 类。
public class VirtualWallet {private Long id;public VirtualWallet(Long preAllocatedId) {this.id = preAllocatedId;}public void freeze(BigDecimal amount) { ... }public void unfreeze(BigDecimal amount) { ...}public void increaseOverdraftAmount(BigDecimal amount) { ... }public void decreaseOverdraftAmount(BigDecimal amount) { ... }public void closeOverdraft() { ... }public void openOverdraft() { ... }public BigDecimal balance() {return this.balance;}public BigDecimal getAvaliableBalance() {BigDecimal totalAvaliableBalance = this.balance.subtract(this.frozenAmount);if (isAllowedOverdraft) {totalAvaliableBalance += this.overdraftAmount;}return totalAvaliableBalance;}...
}public class VirtualWalletService {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);}@Transactionalpublic void debit(Long walletId, BigDecimal amount) {...}@Transactionalpublic void credit(Long walletId, BigDecimal amount) {...}
}
Service 的职责
Service 类负责与 Repository 交流,调用 Respository 类的方法,获取数据库中的数据,转化成领域模型 VirtualWallet,然后由领域模型 VirtualWallet 来完成业务逻辑,最后调用 Repository 类的方法,将数据存回数据库。
之所以让 VirtualWalletService 类与 Repository 打交道,而不是让领域模型 VirtualWallet 与 Repository 打交道,那是因为我们想保持领域模型的独立性,不与任何其他层的代码(Repository 层的代码)或开发框架(比如 Spring、MyBatis)耦合在一起,将流程性的代码逻辑(比如从 DB 中取数据、映射数据)与领域模型的业务逻辑解耦,让领域模型更加可复用。
Service 类负责一些非功能性及与三方系统交互的工作。比如幂等、事务、发邮件、发消息、记录日志、调用其他系统的 RPC 接口等,都可以放到 Service 类中。
Controller 层和 Repository 层是否有必要进行充血领域建模呢
没有必要。Controller 层主要负责接口的暴露,Repository 层主要负责与数据库打交道,这两层包含的业务逻辑并不多。如果业务逻辑比较简单,就没必要做充血建模,即便设计成充血模型,类也非常单薄,看起来也很奇怪。