2.8常用的基于贫血模型的MVC架构违背OOP吗?
2.8.1什么是基于贫血模型的传统开发模式?
-
MVC:
MVC 三层架构中的 M 表示 Model,V 表示 View,C 表示 Controller。它将整个项目分为三层:展示层、逻辑层、数据层,是一个比较笼统的分层方式,落实到具体的开发层面,并不会100%遵从。
-
前后端分离的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 userVo; } } public class UserVo {//省略其他属性、get/set/construct方法 private Long id; private String name; private String cellphone; } ////////// 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/construct方法 private Long id; private String name; private String cellphone; } ////////// Repository+Entity ////////// public class UserRepository { public UserEntity getUserById(Long userId) { //... } } public class UserEntity {//省略其他属性、get/set/construct方法 private Long id; private String name; private String cellphone; }
-
像 UserBo 这样,只包含数据,不包含业务逻辑的类,就叫作贫血模型(Anemic Domain Model)。同理,UserEntity、UserVo 都是基于贫血模型设计的。这种贫血模型将数据与操作分离,破坏了面向对象的封装特性,是一种典型的面向过程的编程风格。
2.8.2更加被推崇的开发模式:基于充血模型的 DDD 开发模式
-
什么是领域驱动设计DDD?
领域驱动设计,即 DDD,主要是用来指导如何解耦业务系统,划分业务模块,定义业务领域模型及其交互。
被大家熟知,主要源于微服务概念的兴起:除了监控、调用链追踪、API 网关等服务治理系统的开发之外,微服务还有另外一个更加重要的工作,那就是针对公司的业务,合理地做微服务拆分。而领域驱动设计恰好就是用来指导划分服务的。所以,微服务加速了领域驱动设计的盛行。
建议:概念高大上,实际五分钱,更重要的是对所做业务的熟悉程度,不要花太多精力在概念的本身的掌握上。
-
充血模型的三层架构
基于充血模型的 DDD 开发模式实现的代码,也是按照 MVC 三层架构分层的。Controller 层还是负责暴露接口,Repository 层还是负责数据存取,Service 层负责核心业务逻辑。它跟基于贫血模型的传统开发模式的区别主要在 Service 层。
- 在基于贫血模型的传统开发模式中,Service 层包含 Service 类和 BO 类两部分,BO 是贫血模型,只包含数据,不包含具体的业务逻辑。业务逻辑集中在 Service 类中。
- 在基于充血模型的 DDD 开发模式中,Service 层包含 Service 类和 Domain 类两部分。Domain 就相当于贫血模型中的 BO。不过,Domain 与 BO 的区别在于它是基于充血模型开发的,既包含数据,也包含业务逻辑。而 Service 类变得非常单薄。
- 总结:基于贫血模型的传统的开发模式,重 Service 轻 BO;基于充血模型的 DDD 开发模式,轻 Service 重 Domain。
2.8.3为什么基于贫血模型的传统开发模式如此受欢迎?
-
系统业务可能都比较简单,简单到就是基于 SQL 的 CRUD 操作,所以,不需要动脑子精心设计充血模型,贫血模型就足以应付这种简单业务的开发工作。
-
充血模型的设计要比贫血模型更加有难度。因为充血模型是一种面向对象的编程风格。
要先设计好针对数据要暴露哪些操作,定义哪些业务逻辑。而不是像贫血模型那样,我们只需要定义数据,之后有什么功能开发需求,我们就在 Service 层定义什么操作,不需要事先做太多设计。
-
思维已固化,转型有成本:学习成本、转型成本。很多人在没有遇到开发痛点的情况下,是不愿意做这件事情的。
2.8.4什么项目应该考虑使用基于充血模型的 DDD 开发模式?
- 基于贫血模型的传统的开发模式,比较适合业务比较简单的系统开发。相对应的,基于充血模型的 DDD 开发模式,更适合业务复杂的系统开发。比如,包含各种利息计算模型、还款模型等复杂业务的金融系统。
- 应用基于充血模型的 DDD 的开发模式,需要事先理清楚所有的业务,定义领域模型所包含的属性和方法。领域模型相当于可复用的业务中间层。新功能需求的开发,都基于之前定义好的这些领域模型来完成。
- 越复杂的系统,对代码的复用性、易维护性要求就越高,就越应该花更多的时间和精力在前期设计上。而基于充血模型的 DDD 开发模式,正好需要前期做大量的业务调研、领域模型设计,所以它更加适合这种复杂系统的开发。
2.9如何利用基于充血模型的DDD开发一个虚拟钱包系统
2.9.1需求分析
限定钱包暂时只支持充值、提现、支付、查询余额、查询交易流水这五个核心的功能
-
充值:用户通过三方支付渠道,把自己银行卡账户内的钱,充值到虚拟钱包账号中
来源 操作 目标 用户虚拟钱包 ➕150元 用户银行卡 150元➡ 应用公共银行卡 交易记录 充值➕150元 -
支付:用户用钱包内的余额,支付购买应用内的商品,从用户的虚拟钱包账户划钱到商家的虚拟钱包账户上
来源 操作 目标 用户虚拟钱包 150元➡ 商家虚拟钱包 交易记录 支付➖150元 -
提现:用户还可以将虚拟钱包中的余额,提现到自己的银行卡中
来源 操作 目标 用户虚拟钱包 ➖150元 应用公共银行卡 150元➡ 用户银行卡 交易记录 提现➖150元 -
查询余额:显示钱包余额
-
查询交易流水:只支持三种类型的交易流水:充值、支付、提现
2.9.2钱包系统设计
-
把整个钱包系统的业务划分为两部分:
- 虚拟钱包系统:单纯跟应用内的虚拟钱包账户打交道。
- 三方支付系统:单纯跟银行账户打交道。
- 基于这样一个业务划分,给系统解耦。
-
支持钱包的这五个核心功能,虚拟钱包系统需要对应实现哪些操作。
钱包 虚拟钱包 充值 ➕余额 提现 ➖余额 支付 ➕➖余额 查询余额 查询余额 查询交易流水 ??? -
虚拟钱包系统不应该感知具体的业务交易类型。虚拟钱包支持的操作,仅仅是余额的加加减减操作,不涉及复杂业务概念,职责单一、功能通用。
-
记录两条交易流水信息:整个钱包系统分为两个子系统,上层钱包系统的实现,依赖底层虚拟钱包系统和三方支付系统,在钱包系统这一层额外记录一条包含交易类型的交易流水信息,而在底层的虚拟钱包系统中记录不包含交易类型的交易流水信息。
系统 钱包交易流水 交易流水ID 交易时间 交易金额 交易类型(充值、提现、支付) 入账钱包账号 出账钱包账号 虚拟钱包交易流水ID 虚拟钱包交易流水 交易流水ID 交易时间 交易金额 交易类型(加、减) 虚拟钱包账号 钱包交易流水ID
-
下面两节分别用基于贫血模型的传统开发模式和基于充血模型的 DDD 开发模式,来实现这样一个虚拟钱包系统
2.9.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 fromWalletId, Long toWalletId, BigDecimal amount) { ...} //转账 }
-
Service 和 BO 负责核心业务逻辑,Repository 和 Entity 负责数据存取。Repository 这一层的代码实现省略掉了。Service 层的代码如下所示。注意,这里省略了一些不重要的校验代码,比如,对 amount 是否小于 0、钱包是否存在的校验等等。
public class VirtualWalletBo {//省略getter/setter/constructor方法 private Long id; private Long createTime; private BigDecimal balance; } 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); } public void debit(Long walletId, BigDecimal amount) { VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId); BigDecimal balance = walletEntity.getBalance(); if (balance.compareTo(amount) < 0) { throw new NoSufficientBalanceException(...); } walletRepo.updateBalance(walletId, balance.subtract(amount)); } public void credit(Long walletId, BigDecimal amount) { VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId); BigDecimal balance = walletEntity.getBalance(); walletRepo.updateBalance(walletId, balance.add(amount)); } public void transfer(Long fromWalletId, Long toWalletId, BigDecimal amount) { VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactionEntity(); transactionEntity.setAmount(amount); transactionEntity.setCreateTime(System.currentTimeMillis()); transactionEntity.setFromWalletId(fromWalletId); transactionEntity.setToWalletId(toWalletId); transactionEntity.setStatus(Status.TO_BE_EXECUTED); Long transactionId = transactionRepo.saveTransaction(transactionEntity); try { debit(fromWalletId, amount); credit(toWalletId, amount); } catch (InsufficientBalanceException e) { transactionRepo.updateStatus(transactionId, Status.CLOSED); ...rethrow exception e... } catch (Exception e) { transactionRepo.updateStatus(transactionId, Status.FAILED); ...rethrow exception e... } transactionRepo.updateStatus(transactionId, Status.EXECUTED); } }
2.9.4基于充血模型的 DDD 开发模式
-
基于充血模型的 DDD 开发模式,跟基于贫血模型的传统开发模式的主要区别就在 Service 层,Controller 层和 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 (this.balance.compareTo(amount) < 0) { throw new InsufficientBalanceException(...); } this.balance.subtract(amount); } public void credit(BigDecimal amount) { if (amount.compareTo(BigDecimal.ZERO) < 0) { throw new InvalidAmountException(...); } 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); } public void debit(Long walletId, BigDecimal amount) { VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId); VirtualWallet wallet = convert(walletEntity); wallet.debit(amount); walletRepo.updateBalance(walletId, wallet.balance()); } public void credit(Long walletId, BigDecimal amount) { VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId); VirtualWallet wallet = convert(walletEntity); wallet.credit(amount); walletRepo.updateBalance(walletId, wallet.balance()); } public void transfer(Long fromWalletId, Long toWalletId, BigDecimal amount) { //...跟基于贫血模型的传统开发模式的代码一样... } }
-
增加复杂的业务功能
看了上面的代码可知,领域模型 VirtualWallet 类很单薄,包含的业务逻辑很简单。相对于原来的贫血模型的设计思路,这种充血模型的设计思路,貌似并没有太大优势,这也是大部分业务系统都使用基于贫血模型开发的原因。不过,如果虚拟钱包系统需要支持更复杂的业务逻辑,那充血模型的优势就显现出来了。比如,我们要支持透支一定额度和冻结部分余额的功能。这个时候,我们重新来看一下 VirtualWallet 类的实现代码。
public class VirtualWallet { 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() { ... } public void openOverdraft() { ... } public BigDecimal balance() { return this.balance; } public BigDecimal getAvaliableBalance() { BigDecimal totalAvaliableBalance = this.balance.subtract(this.frozenAmount); if (isAllowedOverdraft) { totalAvaliableBalance += this.overdraftAmount; } return totalAvaliableBalance; } public void debit(BigDecimal amount) { BigDecimal totalAvaliableBalance = getAvaliableBalance(); if (totoalAvaliableBalance.compareTo(amount) < 0) { throw new InsufficientBalanceException(...); } this.balance.subtract(amount); } public void credit(BigDecimal amount) { if (amount.compareTo(BigDecimal.ZERO) < 0) { throw new InvalidAmountException(...); } this.balance.add(amount); } }
-
领域模型 VirtualWallet 类添加了简单的冻结和透支逻辑之后,功能看起来就丰富了很多,代码也没那么单薄了。如果功能继续演进,我们可以增加更加细化的冻结策略、透支策略、支持钱包账号(VirtualWallet id 字段)自动生成的逻辑(不是通过构造函数经外部传入 ID,而是通过分布式 ID 生成算法来自动生成 ID)等等。VirtualWallet 类的业务逻辑会变得越来越复杂,也就很值得设计成充血模型了。
2.9.5辩证思考与灵活应用
-
在基于充血模型的 DDD 开发模式中,将业务逻辑移动到 Domain 中,Service 类变得很薄,但在我们的代码设计与实现中,并没有完全将 Service 类去掉,这是为什么?或者说,Service 类在这种情况下担当的职责是什么?哪些功能逻辑会放到 Service 类中?
-
区别于 Domain 的职责,Service 类主要有下面这样几个职责:
- Service 类负责与 Repository 交流。VirtualWalletService 类负责与 Repository 层打交道,调用 Respository 类的方法,获取数据库中的数据,转化成领域模型 VirtualWallet,然后由领域模型 VirtualWallet 来完成业务逻辑,最后调用 Repository 类的方法,将数据存回数据库。
- Service 类负责跨领域模型的业务聚合功能。VirtualWalletService 类中的 transfer() 转账函数会涉及两个钱包的操作,因此这部分业务逻辑无法放到 VirtualWallet 类中,所以,我们暂且把转账业务放到 VirtualWalletService 类中了。当然,虽然功能演进,使得转账业务变得复杂起来之后,也可以将转账业务抽取出来,设计成一个独立的领域模型。
- Service 类负责一些非功能性及与三方系统交互的工作。比如幂等、事务、发邮件、发消息、记录日志、调用其他系统的 RPC 接口等,都可以放到 Service 类中。
-
在基于充血模型的 DDD 开发模式中,尽管 Service 层被改造成了充血模型,但是 Controller 层和 Repository 层还是贫血模型,是否有必要也进行充血领域建模呢?
没有必要。Controller 层主要负责接口的暴露,Repository 层主要负责与数据库打交道,这两层包含的业务逻辑并不多,前面我们也提到了,如果业务逻辑比较简单,就没必要做充血建模,即便设计成充血模型,类也非常单薄,看起来也很奇怪。
Repository 的 Entity 即便它被设计成贫血模型,违反面相对象编程的封装特性,有被任意代码修改数据的风险,但 Entity 的生命周期是有限的。一般来讲,我们把它传递到 Service 层之后,就会转化成 BO 或者 Domain 来继续后面的业务逻辑。Entity 的生命周期到此就结束了,所以也并不会被到处任意修改。
另外,Controller 层的 VO实际上是一种 DTO(Data Transfer Object,数据传输对象)。它主要是作为接口的数据传输承载体,将数据发送给其他系统。从功能上来讲,它理应不包含业务逻辑、只包含数据。所以,我们将它设计成贫血模型也是比较合理的。
2.9.6延伸观点
- potato00fa:我对DDD的看法就是,它可以把原来最重的service逻辑拆分并且转移一部分逻辑,可以使得代码可读性略微提高,另一个比较重要的点是使得模型充血以后,基于模型的业务抽象在不断的迭代之后会越来越明确,业务的细节会越来越精准,通过阅读模型的充血行为代码,能够极快的了解系统的业务,对于开发来说能说明显的提升开发效率。
在维护性上来说,如果项目新进了开发人员,如果是贫血模型的service代码,无论代码如何清晰,注释如何完备,代码结构设计得如何优雅,都没有办法第一时间理解系统的核心业务逻辑,但是如果是充血模型,直接阅读充血模型的行为方法,起码能够很快理解70%左右的业务逻辑,因为充血模型可以说是业务的精准抽象,我想,这就是领域模型驱动能够达到"驱动"效果的由来吧
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:设计模式简记-面向对象实战一如何基于充血模型的DDD设计开发虚拟钱包系统 - Python技术站