zoukankan      html  css  js  c++  java
  • 设计模式简记-面向对象实战一如何基于充血模型的DDD设计开发虚拟钱包系统

    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%左右的业务逻辑,因为充血模型可以说是业务的精准抽象,我想,这就是领域模型驱动能够达到"驱动"效果的由来吧
  • 相关阅读:
    WSS基础
    SPCAMLEditor1.0 publish
    关于代码调用SSP获取UserProfile出错的解决方案
    WSS Alert(邮件提醒) 定制
    MOSS字段编辑权限控制方案发布源码
    ListViewWebPart Code
    再议WSS RenderingTemplate
    Windows SharePoint Services Search和Office SharePoint Server Search的区别
    MOSS信息管理策略定制(MOSS custom policies)
    发布一个小工具SPCamlEditor
  • 原文地址:https://www.cnblogs.com/wod-Y/p/12337721.html
Copyright © 2011-2022 走看看