大部分工程师都是做业务开发的,很多业务系统都是基于MVC三层架构来开发的,确切点说,这是一种基于贫血模型的MVC三层架构开发模式。这种开发模式已经成为标准的Web项目开发模式,但它却违反了面向对象编程风格,是一种彻彻底底的面向过程的编程风格。特别是领域驱动设计(DDD)盛行之后,这种基于贫血模型的传统开发模式就更加被人诟病,而基于充血模型的DDD开发模式越来越被人提倡。
基于贫血模型的MVC开发模式
对于大部分的后端开发工程师来说,MVC三层架构都不会陌生,M表示 Model,V表示View,C表示Controller。它将整个项目分为三层:展示层、逻辑层、数据层。MVC三层开发架构是一个比较笼统的分层方式,落实到具体的开发层面,很多项目也并不会100%遵从MVC固定的分层方式,而是会根据具体的项目需求,做适当的调整。
现在很多Web或者App项目都是前后端分离的,后端负责暴露接口给前端调用。这种情况下,我们一般就将后端项目分为Repository层、Service层、Controller层。其中,Repository层负责数据访问,Service层负责业务逻辑,Controller层负责暴露接口。
目前几乎所有的业务后端系统都是这种基于贫血模型的MVC开发模式搭建的。现在以这种模式来开发一个虚拟钱包系统,虚拟钱包账户支持用户充值、提现、支付、查询余额、查询交易流水等操作。
习惯了贫血模型开发的同学,在设计的时候,第一步都是数据建模,数据建模采用的是数据库建模那套方法论,也就是要满足3范式,要画ER图等等,然后根据数据模型创建关系数据库的表结构,之后写DAO,再往后根据业务需求写对应的Service实现。数据模型只包含数据,行为集中体现在Service层。
虚拟钱包系统Controller接口定义如下(省略了具体的代码实现),Controller主要就是调用Service的方法。
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 层的代码如下所示。
public class VirtualWalletBo {
//省略getter/setter/constructor方法
private Long id;
private Long createTime;
private BigDecimal balance;
}
public Enum TransactionType {
DEBIT,
CREDIT,
TRANSFER;
}
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 walletRepo.getBalance(walletId);
}
@Transactional
public void debit(Long walletId, BigDecimal amount) {
VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
BigDecimal balance = walletEntity.getBalance();
if (balance.compareTo(amount) < 0) {
throw new NoSufficientBalanceException(...);
}
VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactionEntity();
transactionEntity.setAmount(amount);
transactionEntity.setCreateTime(System.currentTimeMillis());
transactionEntity.setType(TransactionType.DEBIT);
transactionEntity.setFromWalletId(walletId);
transactionRepo.saveTransaction(transactionEntity);
walletRepo.updateBalance(walletId, balance.subtract(amount));
}
@Transactional
public void credit(Long walletId, BigDecimal amount) {
VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactionEntity();
transactionEntity.setAmount(amount);
transactionEntity.setCreateTime(System.currentTimeMillis());
transactionEntity.setType(TransactionType.CREDIT);
transactionEntity.setFromWalletId(walletId);
transactionRepo.saveTransaction(transactionEntity);
VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
BigDecimal balance = walletEntity.getBalance();
walletRepo.updateBalance(walletId, balance.add(amount));
}
@Transactional
public void transfer(Long fromWalletId, Long toWalletId, BigDecimal amount) {
VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactionEntity();
transactionEntity.setAmount(amount);
transactionEntity.setCreateTime(System.currentTimeMillis());
transactionEntity.setType(TransactionType.TRANSFER);
transactionEntity.setFromWalletId(fromWalletId);
transactionEntity.setToWalletId(toWalletId);
transactionRepo.saveTransaction(transactionEntity);
debit(fromWalletId, amount);
credit(toWalletId, amount);
}
}
从代码中可以发现,VirtualWalletBo是一个纯粹的数据结构,只包含数据,不包含任何业务逻辑。业务逻辑集中在VirtualWalletService中,通过VirtualWalletService来操作VirtualWalletBo。核心业务逻辑被分割为BO和Service两个类中。VirtualWalletBo是只包含数据不包含业务逻辑的贫血模型,数据与操作分离破坏了面向对象的封装特性,是一种典型的面向过程的编程风格。
贫血的数据模型数据模型关注的是数据存储,所有的业务都离不开数据,都离不开对数据的CRUD,数据模型建模的决策因素主要是扩展性、性能等非功能属性,不考虑业务语义的表征能力,是完全是面向数据存储的。
基于充血模型的DDD开发模式
DDD提倡接触到需求第一步就是考虑领域模型,而不是将其切割成数据和行为,然后数据用数据库实现数据部分,使用接口服务实现行为部分,最后造成需求的首肢分离。DDD的宗旨是先考虑业务语义,而不是数据。DDD强调业务抽象和面向对象编程,而不是过程式业务逻辑实现。重点不同导致编程世界观不同。
我们再来看一下,如何利用基于充血模型的DDD开发模式来实现这个虚拟钱包系统。两种开发模式的主要区别就在Service层,Controller层和Repository层的代码基本上相同。
在DDD开发模式下,首先要把虚拟钱包VirtualWallet类设计成一个充血的领域模型,并且将原来在Service类中的部分业务逻辑移动到VirtualWallet类中,让Servicen层的实现去依赖VirtualWallet这个集中了数据与行为属性的“饱满模型”。
// 领域模型(充血模型)
public class VirtualWallet {
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 = this.balance.subtract(amount);
}
public void credit(BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new InvalidAmountException(...);
}
this.balance = 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);
}
@Transactional
public void debit(Long walletId, BigDecimal amount) {
VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
VirtualWallet wallet = convert(walletEntity);
wallet.debit(amount);
VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactionEntity();
transactionEntity.setAmount(amount);
transactionEntity.setCreateTime(System.currentTimeMillis());
transactionEntity.setType(TransactionType.DEBIT);
transactionEntity.setFromWalletId(walletId);
transactionRepo.saveTransaction(transactionEntity);
walletRepo.updateBalance(walletId, wallet.balance());
}
@Transactional
public void credit(Long walletId, BigDecimal amount) {
VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
VirtualWallet wallet = convert(walletEntity);
wallet.credit(amount);
VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactionEntity();
transactionEntity.setAmount(amount);
transactionEntity.setCreateTime(System.currentTimeMillis());
transactionEntity.setType(TransactionType.CREDIT);
transactionEntity.setFromWalletId(walletId);
transactionRepo.saveTransaction(transactionEntity);
walletRepo.updateBalance(walletId, wallet.balance());
}
@Transactional
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.frozenAmount);
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 = this.balance.subtract(amount);
}
public void credit(BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new InvalidAmountException(...);
}
this.balance = this.balance.add(amount);
}
}
领域模型VirtualWallet类在功能越来越多,业务逻辑越来越复杂时就显得不那么单薄了。
领域模型建模的关键是看模型能否显性化、清晰的表达业务语义,扩展性是其次。领域模型必须准确反映业务语言,在写的过程中要求结合业务对类进行职责分明的封装,并且需要将不同类的领域边界划分清楚,一个类体现的是"业务的高度浓缩",也就是一个类既具备能力(行为)也具备属性(数据)。
什么项目适合基于充血模型的DDD开发模式
基于贫血模型的传统开发模式,大部分都是SQL驱动(SQL-Driven)的。我们接到一个后端接口的开发需求的时候,就去看接口需要的数据对应到数据库中,需要哪张表或者哪几张表,然后思考如何编写SQL语句来获取数据。之后就是定义Entity、VO等,然后模板式地往对应的Repository、Service、Controller类中添加代码。
业务逻辑包裹在一个大的SQL语句中,而Service层可以做的事情很少。SQL都是针对特定的业务功能编写的,复用性差。当要开发另一个业务功能的时候,只能重新写个满足新需求的SQL语句,这就可能导致各种长得差不多、区别很小的SQL语句满天飞。
在这个过程中,很少有人会应用领域模型、OOP的概念,也很少有代码复用意识。对于简单业务系统来说,这种开发方式问题不大。但对于复杂业务系统的开发来说,这样的开发方式会让代码越来越混乱,最终导致无法维护。
基于充血模型的DDD开发模式,对应的开发流程就完全不一样了。在这种开发模式下,我们需要事先理清楚所有的业务,定义领域模型所包含的属性和行为,领域模型就是可复用的业务中间层。新需求的开发都基于之前定义好的这些领域模型来完成。
越复杂的系统,对代码的复用性、易维护性要求就越高,我们就越应该花更多的时间和精力在前期领域模型设计上。而基于充血模型的DDD开发模式,正好需要我们前期做大量的业务调研、领域模型设计,所以它更加适合这种复杂系统的开发。
DDD并非银弹。对于业务不复杂的系统开发来说,基于贫血模型的传统开发模式简单够用,基于充血模型的DDD开发模式有点大材小用,无法发挥作用。
基于贫血模型的MVC开发模式受欢迎的原因
第一点,大部分情况下,我们开发的系统业务可能都比较简单(这是事实),简单到就是基于SQL的CRUD操作,所以,我们根本不需要动脑子精心设计充血模型,贫血模型就足以应付这种简单业务的开发工作,虽然从Controller层到Service层到Repository层一捅到底非常简单粗暴,但业务逻辑与依赖关系一目了然。因为业务比较简单,即便使用充血模型,模型本身包含的业务逻辑也并不会很多,设计出来的领域模型也会比较单薄,跟贫血模型差不多,没有太大意义。
第二点,领域模型的设计要比贫血模型更加有难度。领域模型从一开始就要设计好针对数据要暴露哪些操作,定义哪些业务逻辑。而贫血模型只需要定义数据,之后有什么功能开发需求就在Service层定义什么操作,不需要事先做太多设计。
第三点,思维已固化,转型有成本。基于贫血模型的传统开发模式经历了这么多年,已经深得人心、习以为常。如果转向领域驱动设计的开发模式,那势必有一定的学习成本、转型成本。很多人在没有遇到开发痛点的情况下,是不愿意做这件事情的。