1.Web开发常用的贫血MVC架构违背OOP吗?
前面我们依据讲过了面向对象四大特性、接口和抽象类、面向对象和面向过程编程风格,基于接口而非实现编程和多用组合少用继承设计思想。接下来,通过实战来学习如何将这些理论应用到实际的开发中。
大部分开发人员都是做业务系统的。我们都知道,很多业务系统都是基于 MVC
三层架构来开发的。实际上,更准确点讲,这是一种基于贫血模型的 MVC
三层架构开发模式。
虽然,这种开发模式已成为标准的 Web
项目的开发模式,但它违反了面向对象编程风格,是一种彻彻底底的面向过程编程风格,因而它被有些人成为“反模式(anti pattern)”。特别是领域驱动设计(Domain Driven Desgin
,简称 DDD
)盛行之后,这种基于贫血模型的传统开发模式就更加被人诟病。而基于充血模型的 DDD
开发模式越来越被人们提倡。下面将结合一个虚拟钱包系统的开发案例,彻底搞清楚这两种开发模式。
在实战之前,首先,需要弄明白下面的问题:
- 什么是贫血模型、重新模型?
- 为什么说基于贫血模型是违反
OOP
? - 基于贫血模型的传统开发模式既然违反了
OOP
,为什么有如此盛行? - 什么情况下我们应该考虑使用基于充血模型的
DDD
开发模式?
1.1什么是基于贫血模型的传统开发模式?
大部分后端工程师都比较熟悉 MVC
三层架构,其中 M
表示 Model
,V
表示 View
,C
表示 Controller
。它们将项目分为:展示层、逻辑层和数据层。
随着架构的演进,现在很多 Web
或者 App
项目都是前后端分离的,在这种情况下,一般将后端分为 Repository
层、Service
层、Controller
层。其中 Repository
层负责数据访问,Service
层负责业务逻辑、Controller
层负责暴露接口。
现在,在看下什么是贫血模型?
随机上,目前几乎所有的业务后端系统,都是基于贫血模型。我们举个简单的例子。
/// 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 userBo;
}
}
public class UserVo { // 省略其他属性、get/set/constructor方法
private Long id;
private String name;
private String phone;
}
/// 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/constructor方法
private Long id;
private String name;
private String phone;
}
/// Repository+Entity /
public class UserRepository {
public UserEntity getUserById(Long userId) { /*...*/ }
}
public class UserEntity { // 省略其他属性、get/set/constructor方法
private Long id;
private String name;
private String phone;
}
其中,UserEntity
和 UserRepository
组成了数据访问层,UserBo
和 UserService
组成了业务逻辑层,UserVo
和 UserController
组成了接口层。
从代码中,可以发现,UserBo
是一个纯粹的数据结构,只包含数据,不包含任何业务逻辑。业务逻辑集中在 UserService
中。我们通过 UserService
来操作 UserBo
。换句话说,Service
层的数据和业务逻辑,被分为 BO
和 Service
两个类中。像 UserBo
这样,只包含数据,不包含业务逻辑的类,就叫做贫血模型。同理 UserEntity
、UserVo
都是基于贫血模型设计的。这种贫血模型将数据与操作分离,破坏了面向对象的封装特性,是一种典型的面向过程的编程风格。
1.2 什么是基于充血模型的 DDD
开发模式?
先看下,什么是充血模型?
在贫血模型中,数据和业务逻辑操作被分割到不同的类中。充血模型正好相反,数据和对应的业务逻辑被封装到同一个类中。因此,这种充血模型满足面向对象的封装特性,是典型的面向对象编程风格。
在看下什么是领域驱动设计?
领域驱动设计,即 DDD
,主要用来知道如何解耦业务系统,划分业务模块,定义领域模型及其交互。领域驱动设计这个概念早在 2004 年就被提出了,不过它被大众熟知,还是基于另一个概念的兴起,那就是微服务。
除了监控、调用链追踪、API
网关等服务治理系统的开发外,微服务还有另一个更加重要的工作,即针对公司的业务,合理地做微服务拆分。而领域驱动设计恰好就是用来划分服务的。所以,微服务加速了领域驱动设计的盛行。
做好领域驱动设计的关键是,看你对自己所做的业务系统的熟悉程度,而不是对领域驱动设计这个概念本身的掌握程度。即便你对领域驱动搞得再清除,但是对业务不熟悉,也不一定能设计出合理的领域设计。所以,不要把领域驱动设计当银弹,不要花太多时间去过度地去研究它。
实际上,基于充血模型的 DDD
开发模式实现的代码,也是按照 MVC
三层架构分层的。 Repository
层负责数据访问,Service
层负责业务逻辑、Controller
层负责暴露接口。它和基于贫血模型的开发模式的祝好区别在 Service
层。
在基于贫血模型的传统开发模式中,Service
层包含 Service
类和 BO
类两部分,BO
是贫血模型,只包含数据,不包含具体的业务逻辑。业务逻辑集中在 Service
类中。在基于充血模型的 DDD
开发模式中,Service
层包含 Service
类和 Domain
类两部分。
Domain
类就相当于【贫血模型中的BO
】。不过,Domain
与BO
的区别在于它是基于充血模型开发的,既包含数据,也包含业务逻辑。Service
类变得非常单薄。
总结的话,基于贫血模型的传统开发模式,重Service
轻 BO
;基于充血模型的 DDD
开发模式,轻 Service
重 Domain
。
1.3 为什么基于贫血模型的传统开发模式如此受欢迎?
前面讲过,基于贫血模型的传统开发模式将业务和数据分离,违反了 OOP
的封装特性,是一种面过程的编程风格。但是,现在几乎所有的 Web
x项目都是基于这种贫血模型开发的,甚至 Spring 框架的官方 demo
都是按照这种开发模式来编写的。
- 第一点原因是,大部分情况下,开发的业务系统可能都比较简单,简单到就是基于
SQL
的CRUD
操作,所以就不需要动脑子精心设计充血模型,贫血模型就足以应付这种简单业务的开发。另外,因业务简单,及时使用充血模型,那模型本身包含的业务逻辑也不会很多,设计出来的领域模型也比较单薄,跟贫血模型差不多,没有太大意义。 - 第二点原因是,充血模型的设计要比贫血模型更加有难度。因为充血模型是一种面向对象的编程风格。我们从一开始就要设计好针对数据要暴露哪些操作,定义哪些业务逻辑。而不是向贫血模型那样,只要定义好数据,之后有什么功能开发需求,就在
Service
层定义什么操作,不需要事先做太多设计。 - 第三点原因,思维已固化,转型有成本。基于贫血模型的传统开发模式经历了这么多年,已深得人心、习以为常。如果转向充血模型、领域驱动设计,那势必有一定的学习成本、转型成本。很多人在没遇到开发痛点的情况下,是不愿意做这件事的。
1.4 什么项目应该考虑使用基于充血模型的DDD
开发模式?
刚刚讲过,基于贫血模型的传统开发模式,比较适合业务简单的系统开发。相对应的,基于充血模型的 DDD
开发模式,更适合业务复杂的系统开发。比如,包含各种利息计算模型、还款模型等复杂业务的金融系统。
你可能会有疑问,这两种开发按摩时,落实到代码层面,区别就是一个将业务逻辑放到
Service
类中,一个将业务逻辑放到Domain
领域模型中吗?
为什么基于贫血模型的传统开发模式,就不能应对复杂业务系统的开发,而基于充血模型的DDD
开发模式就可以呢?
实际上,除了代码层面的区别之外,还有一个非常重要的区别,那就是两种不同的开发模式会导致不同的开发流程。基于充血模型的 DDD
开发模式的流程,在应对复杂业务系统的开发时更加有优势。为什么这么说?再回忆下,基于贫血模型的开发模式,是怎么实现需求的?
基本上都是 SQL
驱动的开发模式。我们接到一个后端的开发需求时,就去看接口的数据对应到到那种数据库表或者几张表,然后思考如何编写 SQL
语句来获取数据。之后就是定义 Entity
、BO
、VO
、然后模板式的往对应的 Repository
、Service
、Controller
中类添加代码。
业务包裹在一个大的
SQL
语句中,而Service
层可以做的事情很少。SQL
都是针对特定的业务功能编写的,复用性差。当要开发另一个业务功能时,只能重写个满足新需求的SQL
语句,这就可能导致各种长的差不多的、区别很小的SQL
语句满天飞。
所以,在这个过程中,很少有人应用领域模型、OOP
的概念,也很少有代码复用意识。对于简单业务系统来说,这种开发方式问题不大。但对于复杂业务系统的开发来说,这样的开发方式会让代码越来越混乱,最终导致无法维护。
若在项目中,应用基于充血模型的 DDD
的开发模式,那对应的开发流程完全不一样了。在这种开发模式下,我们需要事先搞清楚所有的业务,定义领域模型所包含的属性和方法。领域模型相当于可复用的业务中间层。新功能需求的开发,都是基于之前定义好的这些领域模型来完成。
大家都知道,越复杂的系统,对代码的复用性、易维护性的要求就越高,我们就应该话更多的时间和精力在前期设计上。而基于充血模型的 DDD
开发模式,正好需要我们前期做大量的业务调研、领域模型设计,所以它更加适合这种复杂系统的开发。
2.利用基于充血模型的 DDD 开发一个虚拟钱包系统
2.1 钱包业务背景介绍
下图是一个经典地钱包功能界面。
一般来说,每个虚拟钱包都对应一个真实的支付账户,可能是银行卡账户或者是第三方账户(如支付宝或微信钱包)。为了方便业务简单,本系统限定钱包只支持充值、体现、支付、查询余额、查询交易流水这五个功能。
- 充值:用户通过三方支付渠道,把银行卡账户内的钱,充值到虚拟钱包账号中。整个流程分为三步:
- 用户从银行卡账户转账到应用的公共银行卡账户。
- 将用户的充值金额加到虚拟钱包余额上。
- 记录刚刚这边交易的流水。
- 支付:用户用钱包内的余额,支付购买商品。实际上,支付的过程就是一个转账的过程,从影虎的虚拟钱包账户划钱到商家的虚拟账号上。此外,也需要记录这笔支付的交易流水。
- 体现:除了充值、支付外,用户还可以将钱包中的余额,体现到自己的银行卡中。这个过程实际上就是扣减用户虚拟钱包中的余额,并出发真正地银行转账操作,从应用的公共银行账户转钱到用户的银行账户。同样,我们也需要记录这笔体现的交易流水信息。
- 查询余额:看一下虚拟钱包中的余额数字即可。
- 查询交易流水:查询交易流水也比较简单。只支持三种类型的交易流水:充值、支付、体现。在用户充值、支付、体现的时候,会记录相应的交易信息。在需要查询时,只需要将之前记录的交易流水,按照时间、交易类型等条件过滤后,显示出来即可。
2.2 钱包系统的设计思路
根据刚刚的业务实现流程和数据流转,可以把钱包系统划分为两部分,其中一部分单纯内的虚拟钱包账户打交道,另一部分单纯跟银行账户打交道。我们基于这样一个业务划分,给系统解耦,将整个钱包系统拆分成两个子系统:虚拟钱包系统和三方支付系统。
虚拟钱包 | 三方支付 |
---|---|
用户虚拟钱包 商家虚拟钱包 | 用户银行卡 商家银行卡 应用公共银行卡 |
因篇幅有限,接下来只聚焦于虚拟钱包系统的设计与实现。对于三方支付系统以及整个钱包系统的设计与实现,你可以自己思考下。
现在看下,如果需要支持钱包的这 5 个核心功能,虚拟钱包系统需要对应实现哪些操作。我列出下这个五个功能对应的操作。注意,交易流水的记录和查询,暂时打了个问号,因为这块比较特殊,待会再讲。
钱包 | 虚拟钱包 |
---|---|
充值 | + 余额 |
体现 | - 余额 |
支付 | + - 余额 |
查询余额 | 查询余额 |
查询交易流水 | ??? |
从表中,可以看出,虚拟钱包系统要支持的操作非常简单,就是余额的加加减减。其中,充值、提现、查询余额三个功能只涉及一个账户余额的加减操作,而支付功能设计两个账户的余额加减操作:一个账户减余额,另一个账户加余额。
再看下交易流水如何记录和查询?先看下交易流水包含的信息。
交易流水ID | 交易时间 | 交易金额 | 交易类类型(充值 、提现 、支付 ) | 入账钱包账号 | 出账钱包账户 |
---|
交易流水的数据格式包含两个钱包账号,一个是入账钱包账号,一个是出账钱包账号。为什么要有两个账号信息呢? 主要是为了兼容支付这种涉及两个账户的交易类型。不过对于充值、提现这两种交易类型,只需要记录一个钱包账户信息就够了。
整个虚拟钱包的设计思路至此就讲完了。再看下,如何分别基于贫血模型的传统开发模式和基于充血模型的 DDD
开发模式,来实现这样一个虚拟钱包系统。
2.3 基于贫血模型的传统开发模式
这是一个典型的 Web
后端项目的三层结构。其中,Controller
和 VO
负责暴露接口,具体的代码实现如下所示。注意,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 walletId, BigDecimal amount) { ... } // 转账
// 省略查询transaction的接口
}
Service
和 BO
负责核心业务逻辑,Repository
和 Entity
负责数据存取。Repository
这一层的代码实现比较简单,不是讲解的重点,所以省略了。Service
层的代码如下所示。注意,这里省略了一些不重要的校验代码,比如,对 amount
是否小于 0、钱包是否存在的校验等。
public class VirtualWalletBo {
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 NoSufficentBalanceException(...);
}
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) {
VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
BigDecimal balance = walletEntity.getBalance();
VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactionEntity();
transactionEntity.setAmount(amount);
transactionEntity.setCreateTime(System.currentTimeMillis());
transactionEntity.setType(TransactionType.CREDIT);
transactionEntity.setFromWalletId(walletId);
transactionRepo.saveTransaction(transactionEntity);
walletRepo.updateBalance(walletId, balance.add(amount));
}
// 转账
@Transactional
public void credit(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);
}
}
2.4 基于充血模型的 DDD
上面讲到,基于充血模型的 DDD
开发模式,跟基于贫血模型的传统开发模式的主要区别在 Service
层, Contriller
层和 Repository
层上的代码基本相同。所以,重点看一下,Service
层按照基于充血模型的 DDD
开发模式该如何实现。
在这种开发模式下,要把虚拟钱包 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 (balance.compareTo(amount) < 0) {
throw new NoSufficentBalanceException(...);
}
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 credit(Long fromWalletId, Long toWalletId, BigDecimal amount) {
// 跟基于贫血模型的代码一样...
}
}
看了上面的代码,你可能会说,领域模型 VirtualWallet
类很单薄,包含的业务逻辑很简单。相对于贫血模型来说,貌似没有太大的优势。
的确,这也是大部分业务系统使用基于贫血模型开发的原因。不过,如果虚拟钱包系统需要支持更复杂的业务逻辑处理,那充血模型的优势就体现出来了。比如,我们需要支持透支一定额度和冻结部分余额的功能。正好时候,我们重新看下 VirtualWallet
类的实现代码。
public class VirtualWallet { // Domain领域模型(充血模型)
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(BigDecimal amount) {...} // 关闭透支
public void openOverdraft(BigDecimal amount) {...} // 打开透支
public BigDecimal balance() {
return this.balance;
}
public BigDecimal getAvailableBalance() { // 获取可用余额 = 实际余额 - 冻结金额 + 可透支金额
BigDecimal totalAvailableBalance = this.balance.subtract(this.frozenAmount);
if (isAllowedOverdraft) {
totalAvailableBalance = totalAvailableBalance.add(this.overdraftAmount);
}
return totalAvailableBalance;
}
// 提现
public void debit(BigDecimal amount) {
BigDecimal totalAvailableBalance = getAvailableBalance();
if (totalAvailableBalance.compareTo(amount) < 0) {
throw new NoSufficentBalanceException(...);
}
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
类添加了简单的冻结和透支逻辑后,功能看起来就丰富了很多,代码也没有那么单薄了。如果功能继续演进,我们可以增加更加细化的冻结策略、投资策略、支持钱包账号(ID
字段)自动生成逻辑(不通过构造函数经外部传入 ID
,而是通过分布式 ID
生成算法来自动生成 ID
)等等。VirtualWallet
类的业务逻辑会变得越来越复杂,也就很值得设计成充血模型了。
2.4 辩证思考与灵活应用
对于虚拟钱包系统的设计与两种开发模式的代码实现,你应该比较清晰的了解了。现在,有两个问题值得讨论下。
第一个要讨论的问题是:在基于充血模型的 DDD
开发模式中,将业务逻辑移动到 Domain
中,Service
类变得很单薄,但是在代码设计与实现中,并没有完全将 Service
类去掉,这是为什么?
Service
类主要有以下几个职责。
1.Service
类负责与 Repository
交流。在我们的设计与实现中,VirtualWalletService
类与 Repositiry
打交道,调用 Repositiry
类的方法,获取数据库中的数据,转化成领域模型 VirtualWallet
,然后由领域模型 VirtualWallet
来完成业务逻辑,最后调用 Repositiry
类的方法,将数据存回数据库。
之所以让
VirtualWalletService
类与Repositiry
打交道,而不是让领域模型VirtualWallet
与Repositiry
打交道,是因为我们想保持领域模型的独立性,不与其他的代码(Repositiry
层的代码)或者软件开发框架耦合在一起,将流程性的代码逻辑(比如从DB
中取数据、映射数据)与领域模型的业务逻辑解耦,让领域模型更加复用。
2.Service
类负责领域模型的业务功能聚合功能。VirtualWalletService
类中的 transfer()
转账函数会涉及两个钱包的操作,因此者部分代码是无法放到 VirtualWallet
类中了。当然,随着功能演进,使得转账业务变得复杂起来之后,我们也可以将转账业务抽取出来,设计成一个独立的领域模型。
3.Service
类负责一些非功能性及与三方系统交互的工作。比如幂等、事务、发邮件、发消息、记录日志、调用其他系统的 RPC
接口等,都可以放到 Service
类中。
第二个 要讨论的问题是:在基于充血模型的 DDD
开发模式中,尽管 Service
层被改造成了充血模型,但是 Controller
层和 Repository
还是贫血模型的,是否有必要也进行充血领域建模呢?
答案是没有必要。Controller
层负责暴露接口,Repository
层负责与数据库打交道,这两层的业务逻辑并不多。在业务功能简单时,就没有必要使做充血建模,即便设计成充血模型,类也非常单薄,看起来很奇怪。
尽管这样的设计是一种面向过程的编程风格,但是只要我们控制好面向过程编程风格的副作用,照样可以开发出优秀的软件。
副作用如何控制?
就拿 Repository
的 Entity
来说,即便它被设计成贫血模型,违反面向对象编程的封装特性,有被任意代码修改数据的风险,但 Entity
的生命周期是有限的。一般来讲,我们把它传递到 Service
层之后,就会转化成 BO
或者 Doman
来继续后面的业务逻辑。Entity 的生命周期到此就结束了,所以也并不会被到处任意修改。
再说说 Controller
层的 VO
。实际上 VO
是一种 DTO
(数据传输对象)。它主要是作为接口的数据传输承载体,将数据发送给其他系统。从功能上来讲,它理应不包含业务逻辑,只包含数据。所以,我们将它涉及成贫血模型也是比较合理的。