第一部分:实战一

第一部分:实战一

实战一(上)

什么是基于贫血模型的传统开发模式?

  1. 基于MVC架构的代码样例就是典型的贫血模型开发。
  2. 此样例中,UserEntity 和 UserRepository 组成了数据访问层,UserBo 和 UserService 组成了业务逻辑层,UserVo 和 UserController在这里属于接口层。
  3. UserBo 是一个纯粹的数据结构,只包含数据,不包含任何业务逻辑。业务逻辑集中在 UserService 中。剩下两组同理。
  4. 像 UserBo 这样,只包含数据,不包含业务逻辑的类,就叫作贫血模型(Anemic Domain Model)。
  5. 贫血模型将数据与操作分离,破坏了面向对象的封装特性,是一种典型的面向过程的编程风格。

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;
}

什么是基于充血模型的 DDD 开发模式?

什么是充血模型?

  1. 在贫血模型中,数据和业务逻辑被分割到不同的类中。
  2. 充血模型(Rich Domain Model)正好相反,数据和对应的业务逻辑被封装到同一个类中
  3. 充血模型满足面向对象的封装特性,是典型的面向对象编程风格。

接下来,我们再来看一下,什么是领域驱动设计?

  1. 领域驱动设计,即 DDD,主要是用来指导如何解耦业务系统,划分业务模块,定义业务领域模型及其交互。
  2. 不要花过多时间在DDD的概念研究上,要结合业务开发。只有熟悉业务开发,在开发过程中或多或少地使用它,才能做出合理的领域设计。
  3. 基于充血模型的 DDD 开发模式实现的代码,也是按照 MVC.三层架构分层的。Controller 层还是负责暴露接口,Repository 层还是负责数据存取,Service 层负责核心业务逻辑。它跟基于贫血模型的传统开发模式的区别主要在Service 层。
  4. 基于贫血模型的传统的开发模式,重 Service 轻 BO;基于充血模型的 DDD 开发模式,轻 Service 重 Domain。
  5. Domain 与 BO 的区别在于它是基于充血模型开发的,既包含数据,也包含业务逻辑。

为什么基于贫血模型的传统开发模式如此受欢迎?

  1. 第一点原因是,大部分情况下,我们开发的系统业务可能都比较简单。
  2. 第二点原因是,充血模型的设计要比贫血模型更加有难度。
  3. 第三点原因是,思维已固化,转型有成本。

什么项目应该考虑使用基于充血模型的 DDD 开发模式?

  1. 基于充血模型的 DDD 开发模式,更适合业务复杂的系统开发。
  2. 业务逻辑包裹在一个大的 SQL 语句中,而 Service 层可以做的事情很少。当我要开发另一个业务功能的时候,只能重新写个满足新需求的 SQL 语句,这就可能导致各种长得差不多、区别很小的 SQL 语句满天飞。
  3. 基于充血模型的 DDD 开发模式,在应对复杂业务系统的开发的时候更加有优势,正好需要我们前期做大量的业务调研、领域模型设计,所以它更加适合这种复杂系统的开发。
  4. 不过,DDD 也并非银弹。对于业务不复杂的系统开发来说,基于贫血模型的传统开发模式简单够用,基于充血模型的 DDD 开发模式有点大材小用,无法发挥作用。

实战一(下)

钱包业务背景介绍

  1. 五个功能:充值、支付、提现、查询余额、交易流水。
  2. 业务划分两大块:虚拟钱包、三方支付。文中主要涉及虚拟钱包。
  3. 交易流水的两种实现方式:一条记录,出账入账在一起;两条记录,支付与被支付。文中推荐第二种。
  4. 交易流水还可以分两种形式:用户看的,包含交易类型;系统看的,只有余额的加减。

image

钱包系统设计思路

将整个钱包系统的业务划分成两部分,其中一部分单纯跟应用内的虚拟钱包账户打交道,另一部分单纯跟银行打交道。因此将整个系统划分为两个子系统:虚拟钱包系统和三方支付系统。

image

虚拟钱包需要对应的操作:虚拟钱包系统要支持的操作非常简单,就是余额的加加减减。其中,充值、提现、查询余额三个功能,只涉及一个账户余额的加减操作,而支付功能涉及两个账户的余额加减操作:一个账户减余额,另一个账户加余额。交易流水需要如何记录和查询

image

交易流水设计:从系统设计的角度,我们不应该在虚拟钱包系统的交易流水中记录交易类型。从产品需求的角度来说,我们又必须记录交易流水的交易类型。听起来比较矛盾,可以通过记录两条交易流水信息的方式来解决。在钱包系统这一层额外再记录一条包含交易类型的交易流水信息,而在底层的虚拟钱包系统中记录不包含交易类型的交易流水信息。

image

基于贫血模型的传统开发模式

  1. 基于虚拟钱包,一个典型的 Web 后端项目的三层结构。
  2. Controller 中,接口实现比较简单,主要就是调用 Service 的方法。
  3. 重点说Service 层,VirtualWalletBo 是一个纯粹的数据结构,只包含数据,不包含任何业务逻辑,业务逻辑集中在 VirtualWalletService 中。

Controller 层代码:

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 { ... } // 
}

Service 层代码:

public class VirtualWalletBo {// 省略 getter/setter/constructor 方法
	private Long id;
	private Long createTime;
	private BigDecimal balance;
}

public class VirtualWalletService {
	// 通过构造函数或者 IOC 框架注入
	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 virtualWalletRepo.getBalance(walletId);
	}
	
	public void debit(Long walletId, BigDecimal amount) {
		VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
		BigDecimal balance = walletEntity.getBalance();
		if (balance.compareTo(amount) < 0) {
			throw new NoSufficientBalanceException(...);
		}
		walletRepo.updateBalance(walletId, balance.subtract(amount));
	}
	
	public void credit(Long walletId, BigDecimal amount) {
		VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
		BigDecimal balance = walletEntity.getBalance();
		walletRepo.updateBalance(walletId, balance.add(amount));
	}
	
	public void transfer(Long fromWalletId, Long toWalletId, BigDecimal amount) {
		VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTran
		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);
	}
}

基于充血模型的 DDD 开发模式

  1. 也是重点说Service 层,把虚拟钱包 VirtualWallet 类设计成一个充血的Domain 领域模型,并且将原来在 Service 类中的中的部分业务逻辑移动到 VirtualWallet 类中,让Service 类的实现依赖 VirtualWallet 类。
  2. 此例子中,领域模型 VirtualWallet 类很单薄,包含的业务逻辑很简单。不过,如果虚拟钱包系统需要支持更复杂的业务逻辑,那充血模型的优势就显现出来了。比如,我们要支持透支一定额度和冻结部分余额的功能。

基于充血模型的 DDD 开发模式,跟基于贫血模型的传统开发模式的主要区别就在 Service 层,Controller 层和 Repository 层的代码基本上相同。把虚拟钱包 VirtualWallet 类设计成一个充血的 Domain 领域模型,并且将原来在 Service 类中的部分业务逻辑移动到 VirtualWallet 类中,让Service 类的实现依赖 VirtualWallet 类。

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 virtualWalletRepo.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
		//... 跟基于贫血模型的传统开发模式的代码一样...
	}

}

如果虚拟钱包系统需要支持更复杂的业务逻辑,那充血模型的优势就显现出来了。比如,我们要支持透支一定额度和冻结部分余额的功能。VirtualWallet 类的实现代码:

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 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.frozenAmou
		if (isAllowedOverdraft) {
			totalAvaliableBalance += this.overdraftAmount;
		}
		return totalAvaliableBalance;
	}

	public void debit(BigDecimal amount) {
		BigDecimal totalAvaliableBalance = getAvaliableBalance();
		if (totoalAvaliableBalance.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);
	}
}

辩证思考与灵活应用

在基于充血模型的 DDD 开发模式中,哪些功能逻辑会放到 Service 类中?

  1. Service 类负责与 Repository 交流。将流程性的代码逻辑与领域模型的业务逻辑解耦,让领域模型更加可复用。
  2. Service 类负责跨领域模型的业务聚合功能。
  3. Service 类负责一些非功能性及与三方系统交互的工作。

Controller 层和 Repository 层还是贫血模型,是否有必要也进行充血领域建模呢?

  1. 没必要做充血建模,即便设计成充血模型,类也非常单薄,看起来也很奇怪。
  2. 我们把Entity 传递到 Service 层之后,就会转化成 BO 或Domain 来继续后面的业务逻辑。Entity 的生命周期到此就结束了,所以也并不会被到处任意修改。
  3. Controller 层的 VO,主要是作为接口的数据传输承载体,将数据发送给其他系统,理应不包含业务逻辑、只包含数据。
posted @ 2021-10-03 03:06  起床睡觉  阅读(54)  评论(0编辑  收藏  举报