设计模式学习笔记 - 面向对象 - 8.实践:贫血模型和充血模型的原理及实践

1.Web开发常用的贫血MVC架构违背OOP吗?

前面我们依据讲过了面向对象四大特性、接口和抽象类、面向对象和面向过程编程风格,基于接口而非实现编程和多用组合少用继承设计思想。接下来,通过实战来学习如何将这些理论应用到实际的开发中。

大部分开发人员都是做业务系统的。我们都知道,很多业务系统都是基于 MVC 三层架构来开发的。实际上,更准确点讲,这是一种基于贫血模型的 MVC 三层架构开发模式。

虽然,这种开发模式已成为标准的 Web 项目的开发模式,但它违反了面向对象编程风格,是一种彻彻底底的面向过程编程风格,因而它被有些人成为“反模式(anti pattern)”。特别是领域驱动设计Domain Driven Desgin,简称 DDD)盛行之后,这种基于贫血模型的传统开发模式就更加被人诟病。而基于充血模型的 DDD 开发模式越来越被人们提倡。下面将结合一个虚拟钱包系统的开发案例,彻底搞清楚这两种开发模式。

在实战之前,首先,需要弄明白下面的问题:

  1. 什么是贫血模型、重新模型?
  2. 为什么说基于贫血模型是违反 OOP
  3. 基于贫血模型的传统开发模式既然违反了 OOP,为什么有如此盛行?
  4. 什么情况下我们应该考虑使用基于充血模型的 DDD 开发模式?

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

大部分后端工程师都比较熟悉 MVC 三层架构,其中 M 表示 ModelV 表示 ViewC 表示 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;
}

其中,UserEntityUserRepository 组成了数据访问层,UserBoUserService 组成了业务逻辑层,UserVoUserController 组成了接口层。

从代码中,可以发现,UserBo 是一个纯粹的数据结构,只包含数据,不包含任何业务逻辑。业务逻辑集中在 UserService 中。我们通过 UserService 来操作 UserBo。换句话说,Service 层的数据和业务逻辑,被分为 BOService 两个类中。像 UserBo 这样,只包含数据,不包含业务逻辑的类,就叫做贫血模型。同理 UserEntityUserVo 都是基于贫血模型设计的。这种贫血模型将数据与操作分离,破坏了面向对象的封装特性,是一种典型的面向过程的编程风格。

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

先看下,什么是充血模型?

在贫血模型中,数据和业务逻辑操作被分割到不同的类中。充血模型正好相反,数据和对应的业务逻辑被封装到同一个类中。因此,这种充血模型满足面向对象的封装特性,是典型的面向对象编程风格。

在看下什么是领域驱动设计?

领域驱动设计,即 DDD ,主要用来知道如何解耦业务系统,划分业务模块,定义领域模型及其交互。领域驱动设计这个概念早在 2004 年就被提出了,不过它被大众熟知,还是基于另一个概念的兴起,那就是微服务。

除了监控、调用链追踪、API 网关等服务治理系统的开发外,微服务还有另一个更加重要的工作,即针对公司的业务,合理地做微服务拆分。而领域驱动设计恰好就是用来划分服务的。所以,微服务加速了领域驱动设计的盛行。

做好领域驱动设计的关键是,看你对自己所做的业务系统的熟悉程度,而不是对领域驱动设计这个概念本身的掌握程度。即便你对领域驱动搞得再清除,但是对业务不熟悉,也不一定能设计出合理的领域设计。所以,不要把领域驱动设计当银弹,不要花太多时间去过度地去研究它。

实际上,基于充血模型的 DDD 开发模式实现的代码,也是按照 MVC 三层架构分层的。 Repository 层负责数据访问,Service 层负责业务逻辑、Controller 层负责暴露接口。它和基于贫血模型的开发模式的祝好区别在 Service 层。

在基于贫血模型的传统开发模式中,Service 层包含 Service 类和 BO 类两部分,BO 是贫血模型,只包含数据,不包含具体的业务逻辑。业务逻辑集中在 Service 类中。在基于充血模型的 DDD 开发模式中,Service 层包含 Service 类和 Domain 类两部分。

  • Domain 类就相当于【贫血模型中的BO】。不过,DomainBO 的区别在于它是基于充血模型开发的,既包含数据,也包含业务逻辑。
  • Service 类变得非常单薄。

总结的话,基于贫血模型的传统开发模式,重ServiceBO;基于充血模型的 DDD 开发模式,轻 ServiceDomain

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

前面讲过,基于贫血模型的传统开发模式将业务和数据分离,违反了 OOP 的封装特性,是一种面过程的编程风格。但是,现在几乎所有的 Web x项目都是基于这种贫血模型开发的,甚至 Spring 框架的官方 demo 都是按照这种开发模式来编写的。

  • 第一点原因是,大部分情况下,开发的业务系统可能都比较简单,简单到就是基于 SQLCRUD 操作,所以就不需要动脑子精心设计充血模型,贫血模型就足以应付这种简单业务的开发。另外,因业务简单,及时使用充血模型,那模型本身包含的业务逻辑也不会很多,设计出来的领域模型也比较单薄,跟贫血模型差不多,没有太大意义。
  • 第二点原因是,充血模型的设计要比贫血模型更加有难度。因为充血模型是一种面向对象的编程风格。我们从一开始就要设计好针对数据要暴露哪些操作,定义哪些业务逻辑。而不是向贫血模型那样,只要定义好数据,之后有什么功能开发需求,就在 Service 层定义什么操作,不需要事先做太多设计。
  • 第三点原因,思维已固化,转型有成本。基于贫血模型的传统开发模式经历了这么多年,已深得人心、习以为常。如果转向充血模型、领域驱动设计,那势必有一定的学习成本、转型成本。很多人在没遇到开发痛点的情况下,是不愿意做这件事的。

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

刚刚讲过,基于贫血模型的传统开发模式,比较适合业务简单的系统开发。相对应的,基于充血模型的 DDD 开发模式,更适合业务复杂的系统开发。比如,包含各种利息计算模型、还款模型等复杂业务的金融系统。

你可能会有疑问,这两种开发按摩时,落实到代码层面,区别就是一个将业务逻辑放到 Service 类中,一个将业务逻辑放到 Domain 领域模型中吗?
为什么基于贫血模型的传统开发模式,就不能应对复杂业务系统的开发,而基于充血模型的 DDD 开发模式就可以呢?

实际上,除了代码层面的区别之外,还有一个非常重要的区别,那就是两种不同的开发模式会导致不同的开发流程。基于充血模型的 DDD 开发模式的流程,在应对复杂业务系统的开发时更加有优势。为什么这么说?再回忆下,基于贫血模型的开发模式,是怎么实现需求的?

基本上都是 SQL 驱动的开发模式。我们接到一个后端的开发需求时,就去看接口的数据对应到到那种数据库表或者几张表,然后思考如何编写 SQL 语句来获取数据。之后就是定义 EntityBOVO、然后模板式的往对应的 RepositoryServiceController 中类添加代码。

业务包裹在一个大的 SQL 语句中,而 Service 层可以做的事情很少。SQL 都是针对特定的业务功能编写的,复用性差。当要开发另一个业务功能时,只能重写个满足新需求的 SQL 语句,这就可能导致各种长的差不多的、区别很小的 SQL 语句满天飞。

所以,在这个过程中,很少有人应用领域模型、OOP 的概念,也很少有代码复用意识。对于简单业务系统来说,这种开发方式问题不大。但对于复杂业务系统的开发来说,这样的开发方式会让代码越来越混乱,最终导致无法维护。

若在项目中,应用基于充血模型的 DDD 的开发模式,那对应的开发流程完全不一样了。在这种开发模式下,我们需要事先搞清楚所有的业务,定义领域模型所包含的属性和方法。领域模型相当于可复用的业务中间层。新功能需求的开发,都是基于之前定义好的这些领域模型来完成。

大家都知道,越复杂的系统,对代码的复用性、易维护性的要求就越高,我们就应该话更多的时间和精力在前期设计上。而基于充血模型的 DDD 开发模式,正好需要我们前期做大量的业务调研、领域模型设计,所以它更加适合这种复杂系统的开发。

2.利用基于充血模型的 DDD 开发一个虚拟钱包系统

2.1 钱包业务背景介绍

下图是一个经典地钱包功能界面。
在这里插入图片描述
一般来说,每个虚拟钱包都对应一个真实的支付账户,可能是银行卡账户或者是第三方账户(如支付宝或微信钱包)。为了方便业务简单,本系统限定钱包只支持充值、体现、支付、查询余额、查询交易流水这五个功能。

  1. 充值:用户通过三方支付渠道,把银行卡账户内的钱,充值到虚拟钱包账号中。整个流程分为三步:
    • 用户从银行卡账户转账到应用的公共银行卡账户。
    • 将用户的充值金额加到虚拟钱包余额上。
    • 记录刚刚这边交易的流水。
  2. 支付:用户用钱包内的余额,支付购买商品。实际上,支付的过程就是一个转账的过程,从影虎的虚拟钱包账户划钱到商家的虚拟账号上。此外,也需要记录这笔支付的交易流水。
  3. 体现:除了充值、支付外,用户还可以将钱包中的余额,体现到自己的银行卡中。这个过程实际上就是扣减用户虚拟钱包中的余额,并出发真正地银行转账操作,从应用的公共银行账户转钱到用户的银行账户。同样,我们也需要记录这笔体现的交易流水信息。
  4. 查询余额:看一下虚拟钱包中的余额数字即可。
  5. 查询交易流水:查询交易流水也比较简单。只支持三种类型的交易流水:充值、支付、体现。在用户充值、支付、体现的时候,会记录相应的交易信息。在需要查询时,只需要将之前记录的交易流水,按照时间、交易类型等条件过滤后,显示出来即可。

2.2 钱包系统的设计思路

根据刚刚的业务实现流程和数据流转,可以把钱包系统划分为两部分,其中一部分单纯内的虚拟钱包账户打交道,另一部分单纯跟银行账户打交道。我们基于这样一个业务划分,给系统解耦,将整个钱包系统拆分成两个子系统:虚拟钱包系统和三方支付系统。

虚拟钱包三方支付
用户虚拟钱包
商家虚拟钱包
用户银行卡
商家银行卡
应用公共银行卡

因篇幅有限,接下来只聚焦于虚拟钱包系统的设计与实现。对于三方支付系统以及整个钱包系统的设计与实现,你可以自己思考下。

现在看下,如果需要支持钱包的这 5 个核心功能,虚拟钱包系统需要对应实现哪些操作。我列出下这个五个功能对应的操作。注意,交易流水的记录和查询,暂时打了个问号,因为这块比较特殊,待会再讲。

钱包虚拟钱包
充值+ 余额
体现- 余额
支付+ - 余额
查询余额查询余额
查询交易流水???

从表中,可以看出,虚拟钱包系统要支持的操作非常简单,就是余额的加加减减。其中,充值、提现、查询余额三个功能只涉及一个账户余额的加减操作,而支付功能设计两个账户的余额加减操作:一个账户减余额,另一个账户加余额。

再看下交易流水如何记录和查询?先看下交易流水包含的信息。

交易流水ID交易时间交易金额交易类类型(充值提现支付入账钱包账号出账钱包账户

交易流水的数据格式包含两个钱包账号,一个是入账钱包账号,一个是出账钱包账号。为什么要有两个账号信息呢? 主要是为了兼容支付这种涉及两个账户的交易类型。不过对于充值、提现这两种交易类型,只需要记录一个钱包账户信息就够了。

整个虚拟钱包的设计思路至此就讲完了。再看下,如何分别基于贫血模型的传统开发模式和基于充血模型的 DDD 开发模式,来实现这样一个虚拟钱包系统。

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

这是一个典型的 Web 后端项目的三层结构。其中,ControllerVO 负责暴露接口,具体的代码实现如下所示。注意,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的接口
}

ServiceBO 负责核心业务逻辑,RepositoryEntity 负责数据存取。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 打交道,而不是让领域模型 VirtualWalletRepositiry 打交道,是因为我们想保持领域模型的独立性,不与其他的代码(Repositiry 层的代码)或者软件开发框架耦合在一起,将流程性的代码逻辑(比如从 DB 中取数据、映射数据)与领域模型的业务逻辑解耦,让领域模型更加复用。

2.Service 类负责领域模型的业务功能聚合功能。VirtualWalletService 类中的 transfer() 转账函数会涉及两个钱包的操作,因此者部分代码是无法放到 VirtualWallet 类中了。当然,随着功能演进,使得转账业务变得复杂起来之后,我们也可以将转账业务抽取出来,设计成一个独立的领域模型。

3.Service 类负责一些非功能性及与三方系统交互的工作。比如幂等、事务、发邮件、发消息、记录日志、调用其他系统的 RPC 接口等,都可以放到 Service 类中。

第二个 要讨论的问题是:在基于充血模型的 DDD 开发模式中,尽管 Service 层被改造成了充血模型,但是 Controller 层和 Repository 还是贫血模型的,是否有必要也进行充血领域建模呢?

答案是没有必要。Controller 层负责暴露接口,Repository 层负责与数据库打交道,这两层的业务逻辑并不多。在业务功能简单时,就没有必要使做充血建模,即便设计成充血模型,类也非常单薄,看起来很奇怪。

尽管这样的设计是一种面向过程的编程风格,但是只要我们控制好面向过程编程风格的副作用,照样可以开发出优秀的软件。

副作用如何控制?

就拿 RepositoryEntity 来说,即便它被设计成贫血模型,违反面向对象编程的封装特性,有被任意代码修改数据的风险,但 Entity 的生命周期是有限的。一般来讲,我们把它传递到 Service 层之后,就会转化成 BO 或者 Doman 来继续后面的业务逻辑。Entity 的生命周期到此就结束了,所以也并不会被到处任意修改。

再说说 Controller 层的 VO。实际上 VO 是一种 DTO(数据传输对象)。它主要是作为接口的数据传输承载体,将数据发送给其他系统。从功能上来讲,它理应不包含业务逻辑,只包含数据。所以,我们将它涉及成贫血模型也是比较合理的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/491455.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第二节:Vben Admin 登录逻辑梳理和对接后端准备

系列文章目录 上一节&#xff1a;第一节&#xff1a;Vben Admin介绍和初次运行 文章目录 系列文章目录前言项目路径的概述一、登录逻辑梳理loginApi接口查看Mock 二、后端程序对接准备关闭Mock 总结 前言 第一节&#xff0c;我们已经配置了前端环境&#xff0c;运行起来了我们…

zabbix监控业务数据

前言 监控系统除了监控os和数据库性能相关的指标外&#xff0c;业务数据也是重点监控的对象。 一线驻场的运维同学应该深有体会&#xff0c;每天需要向甲方或者公司反馈现场的数据情况&#xff0c;正常情况下一天巡检两次&#xff0c;早上上班后和下午下班前各一次。监控项目…

复旦大学EMBA联合澎湃科技:共议科技迭代 创新破局

1月18日&#xff0c;由复旦大学管理学院、澎湃新闻、厦门市科学技术局联合主办&#xff0c;复旦大学EMBA项目、澎湃科技承办的“君子知道”复旦大学EMBA前沿论坛在厦门成功举办。此次论坛主题为“科技迭代 创新破局”&#xff0c;上海、厦门两地的政策研究专家、科学家、科创企…

RDMA内核态函数ib_post_recv()源码分析

接上文&#xff0c;上文分析了内核rdma向发送队列添加发送请求的函数ib_post_send&#xff0c;本文分析一下向接收队列添加接收请求的函数ib_post_recv。其实函数调用流程与上文类似&#xff0c;不再重复说明&#xff0c;可参考链接。 函数调用过程 最终会调用到这个函数 下面…

力扣随笔删除有序数组中的重复项(简单26)

思路&#xff1a;根据类似于滑动窗口的思想&#xff0c;定义一个指针&#xff1b;使指针左边的区域全部为不重复元素&#xff08;包括指针所指的数字&#xff09; 以示例2为例&#xff0c;left&#xff1a;红色加粗 遍历指针i&#xff1a;黑色加粗 窗口范围&#xff0c;左边界到…

剪辑视频调色怎么让画质变得清晰 视频剪辑调色技巧有哪些方面 剪辑视频免费的软件有哪些 会声会影调色在哪里 会声会影模板素材

视频调色的作用有很多&#xff0c;除了进行风格化剪辑以外&#xff0c;还可以让作品的画质变得清晰。通过调色来增强画面的清晰度&#xff0c;在观感上也会显得十分自然。视频调色的技巧有很多&#xff0c;并且原理大都十分简单。有关剪辑视频调色怎么让画质变得清晰&#xff0…

linux系统---httpd

目录 Internet的起源 一、http协议——超文本传输协议 1.http相关概念 二、HTTP请求访问的完整过程 1、 建立连接 2、 接收请求 3、 处理请求 常用请求Method: GET、POST、HEAD、PUT、DELETE、TRACE、OPTIONS 3.1 常见的HTTP方法 3.2 GET和POST比较 4、访问资源 …

推荐一个 Obsidian 的 ChatGPT 插件

源码地址&#xff1a;https://github.com/nhaouari/obsidian-textgenerator-plugin Text Generator 是目前我使用过的最好的 Obsidian 中的 ChatGPT 功能插件。它旨在智能生成内容&#xff0c;以便轻松记笔记。它不仅可以在 Obsidian 中直接使用 ChatGPT&#xff0c;还提供了优…

Linux键盘输入实验-创建按键的设备节点

一. 简介 Linux内核针对 GPIO驱动开发,提供了 pinctrl子系统与gpio子系统,方便了 GPIO驱动程序的开发。 本文开始学习如何利用 Linux内核的 pinctrl子系统,与 gpio子系统提供的 API函数,开发按键驱动。 这里主要学习在设备树文件中创建按键的设备节点。 二. Linux按键…

介绍 Gradio 与 Hugging Face

&#x1f349; CSDN 叶庭云&#xff1a;https://yetingyun.blog.csdn.net/ 我们可以使用 Gradio 库为我们的模型构建演示。Gradio 允许您完全使用 Python 为任何机器学习模型构建、自定义和共享基于 Web 的演示。使机器学习模型变得可交互和易于使用。 为什么首先要为您的机器…

JavaAPI常用类03

目录 java.lang.Math Math类 代码 运行 Random类 代码 运行 Date类/Calendar类/ SimpleDateFormat类 Date类 代码 运行 Calendar类 代码 运行 SimpleDateFormat类 代码一 运行 常用的转换符 代码二 运行 java.math BigInteger 代码 运行 BigDecimal …

sonar-java 手写一个规则-单元测试分析

前言 最近做项目&#xff0c;定制sonar规则&#xff0c;提高Java代码质量&#xff0c;在编写的sonar规则&#xff0c;做验证时&#xff0c;使用单元测试有一些简单的心得感悟&#xff0c;分享出来。 自定义规则模式 sonar的自定义规则很简单&#xff0c;一般而言有2种模式可…