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);}// 提现@Transactionalpublic 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));}// 充值@Transactionalpublic 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));}// 转账@Transactionalpublic 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);}// 提现@Transactionalpublic 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());}// 充值@Transactionalpublic 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());}// 转账@Transactionalpublic 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(数据传输对象)。它主要是作为接口的数据传输承载体,将数据发送给其他系统。从功能上来讲,它理应不包含业务逻辑,只包含数据。所以,我们将它涉及成贫血模型也是比较合理的。