怎样推荐企业建设网站和互联网推广,牡丹江哪个网络好,WordPress用Aplayer,易语言怎么用网站做背景音乐1.Web开发常用的贫血MVC架构违背OOP吗#xff1f;
前面我们依据讲过了面向对象四大特性、接口和抽象类、面向对象和面向过程编程风格#xff0c;基于接口而非实现编程和多用组合少用继承设计思想。接下来#xff0c;通过实战来学习如何将这些理论应用到实际的开发中。
大部…1.Web开发常用的贫血MVC架构违背OOP吗
前面我们依据讲过了面向对象四大特性、接口和抽象类、面向对象和面向过程编程风格基于接口而非实现编程和多用组合少用继承设计思想。接下来通过实战来学习如何将这些理论应用到实际的开发中。
大部分开发人员都是做业务系统的。我们都知道很多业务系统都是基于 MVC 三层架构来开发的。实际上更准确点讲这是一种基于贫血模型的 MVC 三层架构开发模式。
虽然这种开发模式已成为标准的 Web 项目的开发模式但它违反了面向对象编程风格是一种彻彻底底的面向过程编程风格因而它被有些人成为“反模式anti pattern”。特别是领域驱动设计Domain Driven Desgin简称 DDD盛行之后这种基于贫血模型的传统开发模式就更加被人诟病。而基于充血模型的 DDD 开发模式越来越被人们提倡。下面将结合一个虚拟钱包系统的开发案例彻底搞清楚这两种开发模式。
在实战之前首先需要弄明白下面的问题
什么是贫血模型、重新模型为什么说基于贫血模型是违反 OOP基于贫血模型的传统开发模式既然违反了 OOP为什么有如此盛行什么情况下我们应该考虑使用基于充血模型的 DDD 开发模式
1.1什么是基于贫血模型的传统开发模式
大部分后端工程师都比较熟悉 MVC 三层架构其中 M 表示 ModelV 表示 ViewC 表示 Controller。它们将项目分为展示层、逻辑层和数据层。
随着架构的演进现在很多 Web 或者 App 项目都是前后端分离的在这种情况下一般将后端分为 Repository 层、Service 层、Controller 层。其中 Repository 层负责数据访问Service 层负责业务逻辑、Controller 层负责暴露接口。
现在在看下什么是贫血模型
随机上目前几乎所有的业务后端系统都是基于贫血模型。我们举个简单的例子。
/// ControllerVO(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;
}/// ServiceBO(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;
}/// RepositoryEntity /
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数据传输对象。它主要是作为接口的数据传输承载体将数据发送给其他系统。从功能上来讲它理应不包含业务逻辑只包含数据。所以我们将它涉及成贫血模型也是比较合理的。