当前位置: 首页 > news >正文

设计模式学习笔记 - 面向对象 - 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(数据传输对象)。它主要是作为接口的数据传输承载体,将数据发送给其他系统。从功能上来讲,它理应不包含业务逻辑,只包含数据。所以,我们将它涉及成贫血模型也是比较合理的。

相关文章:

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

1.Web开发常用的贫血MVC架构违背OOP吗&#xff1f; 前面我们依据讲过了面向对象四大特性、接口和抽象类、面向对象和面向过程编程风格&#xff0c;基于接口而非实现编程和多用组合少用继承设计思想。接下来&#xff0c;通过实战来学习如何将这些理论应用到实际的开发中。 大部…...

AI新纪元:可能的盈利之道

本文来源于Twitter大神宝玉&#xff08;dotey&#xff09;在聊 Sora 的时候&#xff0c;总结了 Sora 的价值和可能的盈利方向&#xff0c;我把这部分内容单独摘出来再整理一下。现在的生成式 AI 大家应该不陌生&#xff0c;用它总结文章、翻译、写作、画图&#xff0c;当然真正…...

k8s的svc流量通过iptables和ipvs转发到pod的流程解析

文章目录 1. k8s的svc流量转发1.1 service 说明1.2 endpoints说明1.3 pod 说明1.4 svc流量转发的主要工作 2. iptables规则解析2.1 svc涉及的iptables链流程说明2.2 svc涉及的iptables规则实例2.2.1 KUBE-SERVICES规则链2.2.2 KUBE-SVC-EFPSQH5654KMWHJ5规则链2.2.3 KUBE-SEP-L…...

【踩坑】修复报错 you should not try to import numpy from its source directory

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhang.cn] 报错如下&#xff1a; 修复方法一&#xff1a; pip install pyinstaller5.9 修复方法二&#xff1a; pip install numpy1.24.1...

预测脱碳企业的信用评级-论文代码复现

文献来源 【Forecasting credit ratings of decarbonized firms: Comparative assessmentof machine learning models】 文章有代码复现有两个基本工作&#xff0c;1.是提取每个算法的重要性&#xff1b;2.计算每个算法的评价指标 算法有 CRT 分类决策树 ANN 人工神经网络 R…...

目标检测——KITTI目标跟踪数据集

KITTI目标跟踪数据集是由德国卡尔斯鲁厄理工学院和丰田美国技术研究院联合创建的一个大规模自动驾驶场景下的计算机视觉算法评测数据集。这个数据集主要用于评估立体图像、光流、视觉测距、3D物体检测和3D跟踪等计算机视觉技术在车载环境下的性能这个数据集包含了在市区、乡村和…...

25-k8s集群中-RBAC用户角色资源权限

一、RBAC概述 1&#xff0c;k8s集群的交互逻辑&#xff08;简单了解&#xff09; 我们通过k8s各组件架构&#xff0c;知道各个组件之间是使用https进行数据加密及交互的&#xff0c;那么同理&#xff0c;我们作为“使用”k8s的各种资源的使用者&#xff0c;也是通过https进行数…...

Android 面试问题 2024 版(其二)

Android 面试问题 2024 版&#xff08;其二&#xff09; 六、多线程和并发七、性能优化八、测试九、安全十、Material设计和 **UX/UI** 六、多线程和并发 Android 中的进程和线程有什么区别&#xff1f; 答&#xff1a;进程是在自己的内存空间中运行的应用程序的单独实例&…...

SpringMVC的异常处理

异常分类 : 预期异常(检查型异常)和运行时异常 1、使用@ExceptionHandle注解处理异常 @ExceptionHandle(value={***.class} 异常类型) public modelandview handelException(){} 仅限当前类使用 2、全局处理方式 @ControllerAdvice + @ExceptionHandle 新建类 @Cont…...

【计算机网络】1 因特网概述

一.网络、互联网和因特网 1.网络&#xff08;network&#xff09;&#xff0c;由若干结点&#xff08;node&#xff09;和连接这些结点的链路&#xff08;link&#xff09;组成。 2.多个网络还可以通过路由器互联起来&#xff0c;这样就构成了一个覆盖范围更大的网络&#xf…...

【Ubuntu】Anaconda的安装和使用

目录 1 安装 2 使用 1 安装 &#xff08;1&#xff09;下载安装包 官网地址&#xff1a;Unleash AI Innovation and Value | Anaconda 点击Free Download 按键。 然后 点击下图中的Download开始下载安装包。 &#xff08;2&#xff09;安装 在安装包路径下打开终端&#…...

OpenAI推出首个AI视频模型Sora:重塑视频创作与体验

链接&#xff1a;华为OD机考原题附代码 Sora - 探索AI视频模型的无限可能 随着人工智能技术的飞速发展&#xff0c;AI视频模型已成为科技领域的新热点。而在这个浪潮中&#xff0c;OpenAI推出的首个AI视频模型Sora&#xff0c;以其卓越的性能和前瞻性的技术&#xff0c;引领着…...

mybatis总结传参三

十、&#xff08;不推荐&#xff09;多个参数-按位置传参 参数位置从 0 开始&#xff0c; 引用参数语法 #{ arg 位置 } &#xff0c; 第一个参数是 #{arg0}, 第二个是 #{arg1} 注意&#xff1a; mybatis-3.3 版本和之前的版本使用 #{0},#{1} 方式&#xff0c; 从 myba…...

JSONVUE

1.JSON学习 1.概念: JSON是把JS对象变成字符串. 2.作用: 多用于网络中数据传输. JavaScript对象 let person{name:"张三",age:18}//将JS对象转换为 JSON数据let person2JSON{"name":"张三","age":18}; 3.JS对象与JSON字符串转换…...

OSCP靶机--Medjed

OSCP靶机–Medjed 考点&#xff1a;(1.ftp文件上传 2.sql注入写shell 3.第三软件提权) 1.nmap ┌──(root㉿kali)-[~/Desktop] └─# nmap 192.168.200.127 -sV -sC -p- --min-rate 5000 Starting Nmap 7.92 ( https://nmap.org ) at 2024-02-25 19:42 EST Nmap scan repo…...

【Unity】Unity与安卓交互

问题描述 Unity和安卓手机进行交互&#xff0c;是我们开发游戏中最常见的场景。本教程将从一个简单的例子来演示一下。 本教程需要用到Android Studio2021.1.1 1.Android Studio新建一个工程 2.选择Empty Activity 然后点击Next 3.点击Finish完成创建 4.选择File-New-New Mo…...

QYFB-02 无线风力报警仪 风速风向超限声光报警

产品概述 无线风力报警仪是由测控报警仪、无线风速风向传感器和太阳能供电盒组成&#xff0c;可观测大气中的瞬时风速&#xff0c;具有风速报警设定和报警输出控制功能&#xff1b;风力报警仪采用无线信号传输、显示屏输出&#xff0c;风速显示采用高亮LED数码管显示&#xff…...

css知识:盒模型盒子塌陷BFC

1. css盒模型 标准盒子模型&#xff0c;content-box 设置宽度即content的宽度 width content 总宽度content&#xff08;width设定值&#xff09; padding border IE/怪异盒子模型&#xff0c;border-box width content border padding 总宽度 width设定值 2. 如何…...

Nginx的反向代理:实现灵活的请求转发和内容缓存

一、引言&#xff1a;代理服务器的简介 本节介绍代理服务器的基本配置。学习如何通过不同协议将 NGINX 请求传递给代理的服务器&#xff0c;修改发送到代理服务器的客户端请求标头&#xff0c;以及配置来自代理服务器的响应缓冲。 代理通常用于在多个服务器之间分配负载&…...

免费享受企业级安全:雷池社区版WAF,高效专业的Web安全的方案

网站安全成为了每个企业及个人不可忽视的重要议题。 随着网络攻击手段日益狡猾和复杂&#xff0c;选择一个强大的安全防护平台变得尤为关键。 推荐的雷池社区版——一个为网站提供全面安全防护解决方案的平台&#xff0c;它不仅具备高效的安全防护能力&#xff0c;还让网站安…...

基于SpringBoot的航班进出港管理系统

文章目录 项目介绍主要功能截图&#xff1a;部分代码展示设计总结项目获取方式 &#x1f345; 作者主页&#xff1a;超级无敌暴龙战士塔塔开 &#x1f345; 简介&#xff1a;Java领域优质创作者&#x1f3c6;、 简历模板、学习资料、面试题库【关注我&#xff0c;都给你】 &…...

Odoo系统安装部署并结合内网穿透实现固定域名访问本地ERP系统

文章目录 前言1. 下载安装Odoo&#xff1a;2. 实现公网访问Odoo本地系统&#xff1a;3. 固定域名访问Odoo本地系统 前言 Odoo是全球流行的开源企业管理套件&#xff0c;是一个一站式全功能ERP及电商平台。 开源性质&#xff1a;Odoo是一个开源的ERP软件&#xff0c;这意味着企…...

幻兽帕鲁(Palworld 1.4.1)私有服务器搭建(docker版)

文章目录 说明客户端安装服务器部署1Panel安装和配置docker服务初始化设置设置开机自启动设置镜像加速 游戏服务端部署游戏服务端参数可视化配置 Palworld连接服务器问题总结 说明 服务器硬件要求&#xff1a;Linux系统/Window系统&#xff08;x86架构&#xff0c;armbian架构…...

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

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

智慧城市与数字孪生:共创未来城市新篇章

一、引言 随着科技的飞速发展&#xff0c;智慧城市与数字孪生已成为现代城市建设的核心议题。智慧城市注重利用先进的信息通信技术&#xff0c;提升城市治理水平&#xff0c;改善市民生活品质。而数字孪生则通过建立物理城市与数字模型之间的连接&#xff0c;为城市管理、规划…...

Java数据结构---初识集合框架

目录 一、什么是集合框架 二、集合框架的重要性 三、背后涉及的数据结构及算法 1.什么是数据结构 2.容器背后对应的数据结构 3.相关的Java知识 4.什么是算法 一、什么是集合框架 Java 集合框架 Java Collection Framework &#xff0c;又被称为容器 container &#xff0…...

Spring Cloud学习

1、什么是SpringCloud Spring cloud 流应用程序启动器是基于 Spring Boot 的 Spring 集成应用程序&#xff0c;提供与外部系统的集成。Spring cloud Task&#xff0c;一个生命周期短暂的微服务框架&#xff0c;用于快速构建执行有限数据处理的应用程序。Spring cloud 流应用程…...

【计算机网络】1.4 接入网和物理媒体

1.4 接入网和物理媒体 问题&#xff1a;怎样将端系统和边缘路由器连接&#xff1f; 答&#xff1a;有线方式&#xff08;住宅接入网络、单位接入网络等&#xff09;或无线方式&#xff08;无线接入网络&#xff09;。 有线接入方式 光纤同轴混合网是基于已有的有线电视网开发的…...

关于螺栓的基本拧紧技术了解多少——SunTorque智能扭矩系统

螺栓是机械中常见的紧固件之一&#xff0c;用于将两个或多个部件连接在一起&#xff0c;并保持它们之间的紧密配合。拧紧螺栓是一项基本的技术&#xff0c;但在实际操作中&#xff0c;许多工人并不了解正确的拧紧方法&#xff0c;从而导致螺栓松动、连接失效等问题的出现。因此…...

C# .Net 发布后,把dll全部放在一个文件夹中,让软件目录更整洁

PublishFolderCleaner – Github 测试环境: .Net 8 Program.cs 代码 // https://github.com/dotnet-campus/dotnetcampus.DotNETBuildSDK/tree/master/PublishFolderCleanerusing System.Diagnostics; using System.Text;// 名称, 不用写 .exe var exeName "AbpDemo&…...

[更新]ARCGIS之土地耕地占补平衡、进出平衡系统报备坐标txt格式批量导出工具(定制开发版)

序言 之前开发的耕地占补平衡报备格式&#xff0c;现在之前的基础上集成了耕地进出平衡报备格式导出。 之前版本软件详见&#xff1a;软件介绍 一、软件简介 本软件是基于arcgis二次开发的工具&#xff08;插件&#xff09;&#xff0c;需要授权后才能使用&#xff1b; 本软件…...

todolist

一开始想自己写个todolist的网页&#xff0c;一直没时间&#xff0c;直接拿这个博客记录了&#xff0c;因为仅我可见比较麻烦&#xff0c;就放在全部可见记录了 目录 2024年3月todoes了解一下深入学习k8s&#xff0c;比如pod运行多个容器 &#xff0c;编写自己的镜像 2024年2月…...

【Java程序设计】【C00307】基于Springboot的基Hadoop的物品租赁管理系统(有论文)

基于Springboot的基Hadoop的物品租赁管理系统&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 这是一个基于Springboot的基于 Hadoop的物品租赁系统的设计与实现&#xff0c;本系统有管理员、用户二种角色权限&#xff1b; 前台首页&#…...

GIT中对子仓库的使用方法介绍

git 子仓库 主仓库中添加子仓库 git submodule add <url> <path>更新子代码代码 git submodule update --init克隆含有子仓库的仓库 git clone --recurse-submodules <url>主仓库中删除子仓库 1、进入包含子仓库的父仓库的根目录 2、使用以下命令将子仓…...

ClickHouse 指南(三)最佳实践 -- 跳数索引

Data Skipping Indexes Data Skipping Indexes 2 1、简介 影响ClickHouse查询性能的因素很多。在大多数情况下&#xff0c;关键因素是ClickHouse在计算查询WHERE子句条件时是否可以使用主键。因此&#xff0c;选择适用于最常见查询模式的主键对于有效的表设计至关重要。 然…...

Mybatis总结--传参二

#叫做占位符 Mybatis是封装的JDBC 增强版 内部还是用的jdbc 每遇到一个#号 这里就会变为&#xff1f;占位符 一个#{}就是对应一个问号 一个占位符 用这个对象执行sql语句没有sql注入的风险 八、多个参数-使用Param 当 Dao 接口方法有多个参数&#xff0c;需要通过名称使…...

2024年数字化转型风口趋势大赏

人工智能和自动化确实为提高效率和数据驱动的见解提供了巨大的潜力&#xff0c;但这些技术无法完全取代人类技能和情境决策。在混合模型中将人工智能功能与人类专业知识相结合的企业将实现最大的效益。 随着人工智能和自动化的不断发展&#xff0c;企业必须调整其战略、流程和人…...

某款服务器插上4张TDP功耗75瓦PCIE卡无法开机的调试过程

1.服务器厂家说这款服务器测过别家的4卡&#xff0c;所以一开始并没有怀疑服务器硬件有问题 2.拔掉另外三张&#xff0c;只保留cpu0对应的riser0 slot0上的一张卡&#xff0c;仍然无法开机。 3.怀疑是这张pcie卡bar空间太大导致。换另一款bar空间小的卡&#xff0c;仍然无法开…...

数据结构与算法——排序算法

目录 文章目录 前言 一.排序的基本概念 1.什么是就地排序 2.什么是内部排序和外部排序 3.什么是稳定排序 4.判定一个排序算法的是稳定的 二.插入排序算法 1.直接插入排序 1.1基本思想 1.2复杂度 1.3稳定性 1.4代码演示 2.折半插入排序 2.1基本思想 2.2性能 3.…...

阿里巴巴alibaba API商品详情接口系列(商品属性,价格,主图)阿里巴巴alibaba根据ID取商品详情 API 返回值说明

阿里巴巴Alibaba的API商品详情接口系列通常用于获取指定商品的详细信息&#xff0c;包括商品属性、价格、主图等。与来赞达Lazada的API类似&#xff0c;具体的返回值可能会根据API的版本和阿里巴巴平台的更新而有所不同。 以下是一个假设的阿里巴巴API商品详情接口的返回值示例…...

lcd画圆

//****************************************************************** //函数名&#xff1a; _draw_circle_8 //功能&#xff1a; 8对称性画圆算法(内部调用) //输入参数&#xff1a;(xc,yc) :圆中心坐标 // (x,y):光标相对于圆心的坐标 // c:填…...

React组件详解

React组件分为两大类 1.函数组件 2.类组件&#xff08;最常用&#xff09; 组件化 import ReactDom from "react-dom";// // 1.通过函数创建一个组件 // 2.函数名字必须大写开头 // 3.函数必须有返回值 function Func1() {return <h2>这是一个基础组件</h…...

C++面试:内存溢出、内存泄漏的原因与解决

目录 内存溢出&#xff08;Memory Overflow&#xff09; 内存溢出介绍 解决内存溢出问题的方法 内存泄漏&#xff08;Memory Leak&#xff09; 内存泄露基础 解决内存泄漏问题的方法 内存溢出&#xff08;Memory Overflow&#xff09; 内存溢出介绍 内存溢出是指程序在执…...

【Java程序员面试专栏 算法思维】二 高频面试算法题:二分查找

一轮的算法训练完成后,对相关的题目有了一个初步理解了,接下来进行专题训练,以下这些题目就是汇总的高频题目,本篇主要聊聊二分查找,包括基础二分,寻找目标值的左右边界,搜索旋转数组以及波峰,以及x的平方根问题,所以放到一篇Blog中集中练习 题目关键字解题思路时间空…...

kaldi 详细安装教程、PyTorch-Kaldi、TIMIT下载、Librispeech下载

kaldi 详细安装教程 本kaldi 安装教程 转载于该链接kaldi 详细安装教程 安装系统依赖&#xff08;如果经常使用linux 服务器&#xff0c;一般都会有&#xff09; apt-get updateapt-get install -y --no-install-recommends g make automake autoconf bzip2 unzip wget sox …...

EtherCAT 转 ModbusTCP 网关

功能概述 本产品是 EtherCAT 和 Modbus TCP 网关&#xff0c;使用数据映射方式工作。 本产品在 EtherCAT 侧作为 EtherCAT 从站&#xff0c;接 TwinCAT 、CodeSYS 、PLC 等&#xff1b;在 ModbusTCP 侧做为 ModbusTCP 主站&#xff08;Client&#xff09;或从站&#xff08;Se…...

iMazing2024Windows和Mac的iOS设备管理软件(可以替代iTunes进行数据备份和管理)

iMazing2024是一款兼容 Windows 和 Mac 的 iOS 设备管理软件&#xff0c;可以替代 iTunes 进行数据备份和管理。以下是一些 iMazing 的主要功能和优点&#xff1a; 数据备份和恢复&#xff1a;iMazing 提供了强大的数据备份和恢复功能&#xff0c;可以备份 iOS 设备上的各种数据…...

carpower

车载android 电源管理 车载音响电源管理器_definitely的技术博客_51CTO博客...

数据结构2月25日

第一道&#xff1a; 第二道&#xff1a; 1、插入到prev和next中间 1.new(struct list_head*)malloc(sizeof(struct list_head*)); if(newNULL) { printf("失败\n"); return; } new->nextprev->next; prev->nextnew; return; 2、删除prve和next…...

改进 RAG:自查询检索

原文地址&#xff1a;Improving RAG: Self Querying Retrieval 2024 年 2 月 11 日 让我们来解决构建 RAG 系统时的一个大问题。 我们不能依赖语义搜索来完成每个检索任务。只有当我们追求单词的含义和意图时&#xff0c;语义搜索才有意义。 But in case&#xff0c;我们正…...