学习自:设计模式之美
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);
}
@Transactional
public void debit(Long walletId, BigDecimal amount) {...}
@Transactional
public void credit(Long walletId, BigDecimal amount) {...}
@Transactional
public 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);
}
@Transactional
public void debit(Long walletId, BigDecimal amount) {
...
}
@Transactional
public 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 层主要负责与数据库打交道,这两层包含的业务逻辑并不多。如果业务逻辑比较简单,就没必要做充血建模,即便设计成充血模型,类也非常单薄,看起来也很奇怪。