引入
最近学习了王争的MVC架构和OOP的相关知识,学习到了贫血模型和充血模型的相关知识。
MVC架构
在Java后台中,MVC架构是一种常见的系统架构,其核心目的在我看来是为了解耦。M-model,V-view,C-controller,另外MVC对于不同的项目和业务而言并没有非常严格的限制,在前后分离的项目中,对于后台而言,MVC一般分为为DAO(Repository)、Service、Controller。
贫血模型
举个例子来讲,User、UserDAO作为数据访问层,UserBO、UserService作为业务逻辑层,UserVO、UserController作为接口层;其中UserBO只作为纯粹的数据结构,没有业务处理,业务逻辑集中在Service中。像UserBO这样的纯数据结构的就可以称之为贫血模型,同样的还有User和UserVO,这样的设计破坏了Java面向对象设计的封装特性,属于面向过程的编程风格。
充血模型
基于充血模型的DDD开发模式,与贫血模型相反的是,充血模型将数据和业务放在一个类里面。DDD领域驱动设计,DDD核心是为了根据业务对系统的服务进行拆分。领域驱动设计的核心还是基于对业务的理解,不能一味追求这样的概念。对于充血模型的开发的MVC架构,其核心区别在于Service层:包含Domain类和Service类。Domain对于BO而言,添加了一定的业务逻辑,降低Service中的业务逻辑量。那么充血模型对于贫血模型好在哪里呢?对于贫血模型而言,由于数据和业务的分离,数据在脱离业务的情况下可以被任务程序修改,数据操作将不受限制等。为什么贫血模型这么盛行?一是对于大部分业务而言都比较简单,基本上都是围绕SQL的CRUD操作,仅仅通过贫血模型设计就可以完成业务。而是充血模型的设计难度较大。
设计思路的区别
前者通常是在拿到需求后,先根据数据库表建立Modle,然后Servcie、Controller等进行代码填充,其中一个很重要的核心就是SQL,对于这个需求而言,大部分业务都是围绕这简单亦或复杂的SQL来完成的,当这个模块需要其它功能的时候,往往都是在基础上添加SQL来实现的。这样就会导致其中有很大一部分代码产生冗余,随着业务的深入,将会有大量类似的SQL出现在系统中。在这个过程中,基本上就忽略了DDD开发模式,失去了很多代码复用的机回。 基于充血模型的DDD开发模式下,首先需要根据业务,定义领域模型所包含的数据和方法,相当于设计可复用的业务中间层。对于之后的新功能的开发,都将基于这些已经定义好的领域模型来开发。两者很大的区别就在于后者会花费更多的时间在领域模型设计上。
电子钱包的设计
业务背景
- 充值
- 用户银行卡金额A转到系统的公共银行卡
- 用户虚拟钱包: +A
- 记录流水 :充值+A
- 支付
- 用户虚拟钱包 转账A 到商家的虚拟钱包
- 公共银行卡 转账A 到商家的银行卡
- 记录流水: 支付-A
- 提现
- 用户虚拟钱包 -A
- 公共银行卡 转账-A 到用户银行卡
- 记录流水: 提现-A
- 查询余额
- 查询用户虚拟钱包
- 交易流水
- 在每次交易的时候记录,只需要查询即可
根据以上业务,可以将系统拆分为虚拟钱包和三方支付两个模块。下面着重思考虚拟钱包的设计。 对于上述所有的业务都需要一个操作,就是交易流水的记录。对于该功能的数据库表设计有两种思路:
- 数据冗余,强一致性 表:流水ID、交易时间、交易类型(充值提现支付)、交易金额、入账账户、出账账户
- 无数据冗余,非强一致性 表:流水ID、交易时间、交易类型(充值、提现、支付、被支付)、交易金额、交易账户
以上两种哪种更好呢?答案是第一种,因为在交易流水业务中,保证数据一致性是非常重要的,为了保证支付过程中数据的一致性,可以通过数据库事务来处理,但是这样不够灵活,比如分库分表没有办法直接利用数据库的事务来处理。虽然有一些开源的分布式事务框架,但是更权衡的来说,这里我们只需要保证结果的一致性,不需要保证过程的一致性:在支付的时候,先记录一条流水,标记为待执行,然后再去执行钱包的加减操作,当双方都执行完成后,再去把流水标记为已完成,如果任何一方操作失败,流水记录都将被标记为失败。这样就能保证交易流水结果的强一致性。对于上述两种设计,如果采取第二种思路,一次支付需要记录两条流水,对于这个行为而言本身就需要保持一致性,所以第一种更好,即使它存在一定的数据冗余的弊端。
虚拟钱包系统
对于虚拟钱包系统而言,他不应该感知交易类型,它只负责金额的加加减减,但是为了交易流水的查询功能,又不得不需要这些信息,所以为了能够保持最基本的操作的复用性,我们单独剥离出这个系统,在这个系统以外再添加一个记录交易流水类型的表。
- 钱包交易流水:交易流水ID、时间、金额、交易类型(充值、提现、支付)、入账账号、出账账号、虚拟钱包流水ID
- 虚拟钱包交易流水:交易流水ID、时间、金额、类型(加、减)、虚拟钱包账号、钱包交易流水ID
对于虚拟钱包交易流水而言,它只服务于保持数据一致性上。
基于贫血模型的传统开发模式
其核心在于Service层:VirtualWalletBo、VirtualWalletService
public class VirtualWalletBo{
Long id;
LocalDateTime createTime;
BigDecimal balance;
}
public class VirtualWalletService{
VirtualWalletDAO dao;
VirtualWalletTransactionDAO transactionDAO;
public VirtualWalletBo get(Long id){
//根据id查数据库VirtualWalletEntity,转为bo
}
publicBigDecimalgetBalance(LongwalletId){
return dao.getBalance(walletId);
}
public void debit(Long id,BigDecimal amount){
//记账
//更新账户金额
}
public void credit(Long id,BigDecimal amount){
//存钱
//更新账户金额
}
public void transfer(Long fromId,Long toId,BigDecimal amount){
//创建流水(记录为待执行):VirtualWalletTransactionEntity
debit(fromId,amount);
credit(toId,amount);
//更新流水状态为完成
}
上面的基本上包含了钱包的所有操作:存\取\流转\查
基于充血模型的DDD开发模式
在这种开发模式下,将VirtualWallet类设计成一个充血的Domain领域模型,并将原来在service种的部分业务逻辑放到VirtualWallet类中,让Service类的实现依赖VirtualWallet类.
public class VirtualWallet{//Domain
Long id;
LocalDateTime createTime = LocalDateTime.now();
BigDecimal balance = BigDecimal.ZERO;
// get set
public void debit(BigDecimal amount){
//判断等相关操作
this.balance.substract(amount);
}
public void credit(BigDecimal amount){
this.balance.add(amount);
}
}
在Service中仍然还是那些方法,不过记账存钱都调用Domain中的方法,其它方法保持不变.
事实上此时Domain类很单薄,只有两个业务:记账存钱,不过其设计思路在扩充业务逻辑的时候就能体现这样设计的优势了,比如冻结\透支等业务的加入,这时候Domain就没有那么单薄了.
思考和灵活运用
- 既然Domain已经加入了业务,为什么还要Service呢?
- Service类负责与DAO交流,通过DAO获取数据库数据转化为Domain,然后由该领域模型来完成业务,最后调用DAO将数据存回数据库.事实上,Service是为了保持Domain不与任何其他层的的代码或者框架耦合在一起,将流程性的代码逻辑(比如从DB取数据,映射数据)与领域模型的业务逻辑解耦,让其更加可复用.
- Service类负责跨领域模型的业务聚合功能
- Service类负责一些非功能性以及与三方系统交互的工作.
- 为什么Controller和DAO层不进行充血模型设计呢? 没有必要.因为对于这两部分的业务功能都比较简单,贫血模型设计就能完成业务需求.尽管这样作为一种面向过程的编程风格,其副作用也能被大幅度降低.
- DAO层的Entity:由于其生命周期是有限的,一般来讲被传递到Service之后就会被转化为BO或Domain来继续后面的业务,所以其数据安全性也没有什么问题.
- Controller层的VO:VO实际上是一种DTO(数据传输对象),主要作用是作为接口的数据传输载体.从功能上来说,它不包含业务逻辑
所以上述两层设计成贫血模型也是比较合理的.
总结
基于充血模型的 DDD 开发模式跟基于贫血模型的传统开发模式相比,主要区别在 Service层。在基于充血模型的开发模式下,我们将部分原来在 Service 类中的业务逻辑移动到了一个充血的 Domain 领域模型中,让 Service 类的实现依赖这个 Domain 类。 在基于充血模型的 DDD 开发模式下,Service 类并不会完全移除,而是负责一些不适合放在 Domain 类中的功能。比如,负责与 Repository 层打交道、跨领域模型的业务聚合功能、幂等事务等非功能性的工作