设计模式学习笔记 - 面向对象 - 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.mzph.cn/news/703424.shtml

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

相关文章

自动化部署证书 acme.sh 使用教程

简介 acme.sh 是一个开源的 ACME 协议的客户端工具&#xff0c;用于自动化申请、更新和部署 SSL/TLS 证书。通过使用 acme.sh&#xff0c;用户可以轻松地在服务器上设置 HTTPS 加密连接&#xff0c;而无需手动操作。它支持多种 DNS 接口和证书颁发机构&#xff0c;可以与各种 …

U盘拒绝访问?快速恢复数据的实用方案!

当您尝试访问U盘时&#xff0c;突然遇到“U盘拒绝访问”的提示&#xff0c;这无疑是一个令人头疼的问题。这不仅意味着您无法读取或写入U盘中的数据&#xff0c;还可能意味着重要文件的安全受到威胁。本文将深入探讨U盘拒绝访问的原因&#xff0c;并为您提供至少两种实用的数据…

electron+vue3全家桶+vite项目搭建【27】封装窗口工具类【1】雏形

文章目录 引入思路抽出公共声明文件抽出全局通用数据类型和方法主进程模块1.抽离基础常量2.封装窗口工具类 渲染进程模块测试结果 引入 demo项目地址 可以看到我们之前在主进程中的逻辑全部都塞到index.ts文件中&#xff0c;包括窗口的一些事件处理&#xff0c;handle监听&am…

机器学习打分函数在分子对接中的应用系列-GB_Score

欢迎浏览我的CSND博客&#xff01; Blockbuater_drug …点击进入 文章目录 前言一、GB-Score是什么&#xff1f;二、文献复现 -训练和验证环境1. GB score验证虚拟环境的配置2. Usage1- Preparing ligand and protein file2- Generating features3 - Repeat and extend current…

Linux第66步_linux字符设备驱动_挂载和卸载

1、了解linux中的驱动类型: 1)、字符设备驱动 字符设备是limnux驱动中最基本的一类设备驱动&#xff0c;字符设备就是一个一个字节&#xff0c;按照字节流进行读写操作的设备&#xff0c;读写数据是分先后顺序的。如&#xff1a;GPIO输入输出、UART、I2C、SPI、USB、LCD、音频…

基于相位的运动放大:如何检测和放大难以察觉的运动(01/2)

基于相位的运动放大&#xff1a;如何检测和放大难以察觉的运动 目录 一、说明二、结果的峰值三、金字塔背景3.1 可操纵金字塔3.2 亚倍频程复数可控金字塔 四、基本方针4.1 1D 问题陈述4.2 一维方法4.3 实际实施说明 五、放大倍率的限制5.1 空间支持的影响5.2 频带的影响 六、推…

好书推荐丨细说Python编程:从入门到科学计算

文章目录 写在前面Python简介推荐图书内容简介编辑推荐作者简介 推荐理由粉丝福利写在最后 写在前面 本期博主给大家推荐一本Python基础入门的全新正版书籍&#xff0c;对Python、机器学习、人工智能感兴趣的小伙伴们快来看看吧~ Python简介 Python 是一种广泛使用的高级、解…

多目标追踪概述

1. 目标跟踪分类 单目标跟踪&#xff1a;在视频的初始帧画面上框出单个目标&#xff0c;预测后续帧中该目标的大小与位置多目标跟踪&#xff1a;追踪多个目标的大小和位置&#xff0c;且每一帧中目标的数量和位置都可能变化 2. 多目标跟踪目前的主要问题 形态变化&#xff1a…

做了个很牛的网站,可以搜索网站的网站到底有多好用?

今天给大家推荐的网站叫做&#xff1a;毒蘑菇 - 搜索 毒蘑菇搜索&#xff0c;顾名思义呢&#xff0c;搜索的功能比较好用&#xff0c;大家上网的时候总是需要记住网站的地址&#xff0c;即使你知道网站的名称&#xff0c;也得跳转到百度然后在搜索&#xff0c;有时候百度上那么…

天哪!还有这些逆天的fofa​语句?(二)

接上文 天哪&#xff01;还有这些逆天的fofa语句&#xff1f; 再分享几条&#xff0c;个人觉得比较有意思的fofa语句。 情侣飞行器 之前写过文章的&#xff0c;有兴趣的师傅可以试着翻翻以前的文章去破解密码 fofa语句&#xff1a;"static/js/index.d2dcdf5b.js"…

sql-labs第46关(order by盲注脚本)

一、环境 网上有自己找 二、解释 order by 注入我们看他的true和false来进行注入出来 二、实操 让我们用sort 看看源码 最终我们的id是放到order by后面了 如果我们直接用列去排序 ?sortusername/password username&#xff1a; password&#xff1a; 可以看到顺序是不…

一个39岁程序员的自白,大龄程序员的出路在哪里?

一个39岁程序员的自白&#xff0c;大龄程序员的出路在哪里&#xff1f; 大龄程序员&#xff0c;最悲惨的&#xff0c;可能是但凡你发个贴&#xff0c;下面就会有类似这种人来怼你 本文来自知乎一个大龄程序员老哥&#xff08;白圣君&#xff09;的自白&#xff0c;涤生哥已经经…

JAVA集合进阶(Set、Map集合)

一、Set系列集合 1.1 认识Set集合的特点 Set集合是属于Collection体系下的另一个分支&#xff0c;它的特点如下图所示 下面我们用代码简单演示一下&#xff0c;每一种Set集合的特点。 //Set<Integer> set new HashSet<>(); //无序、无索引、不重复 //Set<…

【架构】GPU架构总结

文章目录 GPU架构GPU渲染内存架构Streaming Multiprocessor(SM)CUDA CoreTensor CoreRT CoreCPU-GPU异构系统GPU资源管理模型 GPU架构演进G80 架构Fermi 架构Maxwell架构Tesla架构Pascal架构Volta 架构Turing架构Ampere 架构Hopper架构 参考文献 GPU架构 主要组成包括&#xf…

外汇天眼:掌握这个方法,你也能成为交易高手!

在金融市场这个大潮中&#xff0c;外汇交易因其高杠杆、24小时交易等特点吸引着无数交易者。然而成功的交易并非易事&#xff0c;对于投资者来说&#xff0c;外汇交易市场是一个复杂且多变的市场&#xff0c;要在外汇市场中获得成功就需要扎实的外汇金融基础知识和独特的策略&a…

压力测试工具Jmeter的下载与使用

1、进入官网下载Jmeter https://jmeter.apache.org/ 国内镜像&#xff08;下载的慢的话可以用国内镜像下载&#xff09; https://mirrors.cloud.tencent.com/apache/jmeter/binaries/ 2、跳转到下载页面 3、根据不同系统下载相应版本的Jmeter压缩包&#xff0c;Linux系统下载…

医院LIS(全称Laboratory Information Management System)系统源码

目录 一、医院LIS系统概况 二、医院LIS系统建设必要性 三、为什么要使用LIS系统 四、技术框架 &#xff08;1&#xff09;总体框架 &#xff08;2&#xff09;技术细节 &#xff08;3&#xff09;LIS主要功能模块 五、LIS系统优势 &#xff08;1&#xff09;客户/用户…

LeetCode_Java_环形链表(题目+思路+代码)

141.环形链表 给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统内部使用整数 pos 来表示链表尾连接到链表中的位…

49.仿简道云公式函数实战-文本函数-Ip

1. Ip函数 获取当前用户的ip地址 注意是Ipv4的地址 2. 函数用法 IP() 3. 函数示例 获取当前用户的ip地址IP() 4. 代码实战 首先我们在function包下创建text包&#xff0c;在text包下创建IpFunction类&#xff0c;代码如下&#xff1a; package com.ql.util.express.sel…

如何使用Lychee+cpolar搭建本地私人图床并实现远程访问存储图片

文章目录 1.前言2. Lychee网站搭建2.1. Lychee下载和安装2.2 Lychee网页测试2.3 cpolar的安装和注册 3.本地网页发布3.1 Cpolar云端设置3.2 Cpolar本地设置 4.公网访问测试5.结语 1.前言 图床作为图片集中存放的服务网站&#xff0c;可以看做是云存储的一部分&#xff0c;既可…