• 极客时间-设计模式之美笔记(3) 面向对象-实战(一)


    业务开发常用的基于贫血模型的MVC架构违背OOP吗?

    1.什么是基于贫血模型的传统开发模式?

      平常使用的DTO 只包含数据,不包含业务逻辑的类,就叫作贫血模型(Anemic Domain Model)将数据与操作分离,破坏了面向对象的封装特性,是一种典型的面向过程的编程风格。

    2.什么是充血模型?

      在贫血模型中,数据和业务逻辑被分割到不同的类中。充血模型(Rich Domain Model)正好相反,数据和对应的业务逻辑被封装到同一个类中。因此,这种充血模型满足面向对象的封装特性,是典型的面向对象编程风格。

    3.两者的区别是什么?

      实际上,基于充血模型的 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。

    4.什么项目应该考虑使用基于充血模型的 DDD 开发模式?

      基于贫血模型的传统的开发模式,比较适合业务比较简单的系统开发。相对应的,基于充血模型的 DDD 开发模式,更适合业务复杂的系统开发。比如,包含各种利息计算模型、还款模型等复杂业务的金融系统。

    5.总结

      我们平时做 Web 项目的业务开发,大部分都是基于贫血模型的 MVC 三层架构,在专栏中我把它称为传统的开发模式。之所以称之为“传统”,是相对于新兴的基于充血模型的 DDD 开发模式来说的。基于贫血模型的传统开发模式,是典型的面向过程的编程风格。相反,基于充血模型的 DDD 开发模式,是典型的面向对象的编程风格。

      不过,DDD 也并非银弹。对于业务不复杂的系统开发来说,基于贫血模型的传统开发模式简单够用,基于充血模型的 DDD 开发模式有点大材小用,无法发挥作用。相反,对于业务复杂的系统开发来说,基于充血模型的 DDD 开发模式,因为前期需要在设计上投入更多时间和精力,来提高代码的复用性和可维护性,所以相比基于贫血模型的开发模式,更加有优势。

    如何利用基于充血模型的DDD开发一个虚拟钱包系统?

    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 = 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);
      }
      
      @Transactional
      public 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());
      }
      
      @Transactional
      public 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());
      }
    
      @Transactional
      public void transfer(Long fromWalletId, Long toWalletId, BigDecimal amount) {
        //...跟基于贫血模型的传统开发模式的代码一样...
      }
    }

    充血模型是将处理数据的操作放在了Domain中。

    第一个要讨论的问题是:

      在基于充血模型的 DDD 开发模式中,将业务逻辑移动到 Domain 中,Service 类变得很薄,但在我们的代码设计与实现中,并没有完全将 Service 类去掉,这是为什么?或者说,Service 类在这种情况下担当的职责是什么?哪些功能逻辑会放到 Service 类中?区别于 Domain 的职责,Service 类主要有下面这样几个职责。

    1.Service 类负责与 Repository 交流。

      在我的设计与代码实现中,VirtualWalletService 类负责与 Repository 层打交道,调用 Respository 类的方法,获取数据库中的数据,转化成领域模型 VirtualWallet,然后由领域模型 VirtualWallet 来完成业务逻辑,最后调用 Repository 类的方法,将数据存回数据库。这里我再稍微解释一下,之所以让 VirtualWalletService 类与 Repository 打交道,而不是让领域模型 VirtualWallet 与 Repository 打交道,那是因为我们想保持领域模型的独立性,不与任何其他层的代码(Repository 层的代码)或开发框架(比如 Spring、MyBatis)耦合在一起,将流程性的代码逻辑(比如从 DB 中取数据、映射数据)与领域模型的业务逻辑解耦,让领域模型更加可复用。

    2.Service 类负责跨领域模型的业务聚合功能。

      VirtualWalletService 类中的 transfer() 转账函数会涉及两个钱包的操作,因此这部分业务逻辑无法放到 VirtualWallet 类中,所以,我们暂且把转账业务放到 VirtualWalletService 类中了。当然,虽然功能演进,使得转账业务变得复杂起来之后,我们也可以将转账业务抽取出来,设计成一个独立的领域模型。

    3.Service 类负责一些非功能性及与三方系统交互的工作。

      比如幂等、事务、发邮件、发消息、记录日志、调用其他系统的 RPC 接口等,都可以放到 Service 类中。

    第二个要讨论问题是:

      在基于充血模型的 DDD 开发模式中,尽管 Service 层被改造成了充血模型,但是 Controller 层和 Repository 层还是贫血模型,是否有必要也进行充血领域建模呢?答案是没有必要。

      Controller 层主要负责接口的暴露,Repository 层主要负责与数据库打交道,这两层包含的业务逻辑并不多,前面我们也提到了,如果业务逻辑比较简单,就没必要做充血建模,即便设计成充血模型,类也非常单薄,看起来也很奇怪。

      尽管这样的设计是一种面向过程的编程风格,但我们只要控制好面向过程编程风格的副作用,照样可以开发出优秀的软件。

      那这里的副作用怎么控制呢?就拿 Repository 的 Entity 来说,即便它被设计成贫血模型,违反面向对象编程的封装特性,有被任意代码修改数据的风险,但 Entity 的生命周期是有限的。一般来讲,我们把它传递到 Service 层之后,就会转化成 BO 或者 Domain 来继续后面的业务逻辑。Entity 的生命周期到此就结束了,所以也并不会被到处任意修改。我们再来说说 Controller 层的 VO。实际上 VO 是一种 DTO(Data Transfer Object,数据传输对象)。它主要是作为接口的数据传输承载体,将数据发送给其他系统。从功能上来讲,它理应不包含业务逻辑、只包含数据。所以,我们将它设计成贫血模型也是比较合理的。

    总结:

      基于充血模型的 DDD 开发模式跟基于贫血模型的传统开发模式相比,主要区别在 Service 层。在基于充血模型的开发模式下,我们将部分原来在 Service 类中的业务逻辑移动到了一个充血的 Domain 领域模型中,让 Service 类的实现依赖这个 Domain 类。在基于充血模型的 DDD 开发模式下,Service 类并不会完全移除,而是负责一些不适合放在 Domain 类中的功能。比如,负责与 Repository 层打交道、跨领域模型的业务聚合功能、幂等事务等非功能性的工作。

      基于充血模型的 DDD 开发模式跟基于贫血模型的传统开发模式相比,Controller 层和 Repository 层的代码基本上相同。这是因为,Repository 层的 Entity 生命周期有限,Controller 层的 VO 只是单纯作为一种 DTO。两部分的业务逻辑都不会太复杂。业务逻辑主要集中在 Service 层。所以,Repository 层和 Controller 层继续沿用贫血模型的设计思路是没有问题的。

  • 相关阅读:
    urllib3使用池管理发送请求和requests常用方法的基本使用+session使用
    Ajax爬取动态数据和HTTPS自动默认证书
    urllib库中的URL编码解码和GETPOST请求
    urllib的使用和进阶——urllib.request
    1.8学习进度总结
    1.7学习进度总结
    1.5学习进度总结
    1.4学习进度总结
    第十二周周进度总结
    第十一周周进度总结
  • 原文地址:https://www.cnblogs.com/xcgShare/p/14512671.html
Copyright © 2020-2023  润新知