设计模式学习笔记 - 面向对象 - 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);
    }

    // 提现
    @Transactional
    public 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));
    }

    // 充值
    @Transactional
    public 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));
    }

    // 转账
    @Transactional
    public 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);
    }

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/409737.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

WiFi又演进了,这次是WiFi 7

现在很多笔记本laptop、电视TV、手机Phone,甚至车机IVI都有了WiFi和蓝牙BT的接入功能。 不管WiFi、蓝牙BlueTooth、NBIoT、ZigBee等等无线的技术、无线通信模块的技术,其本质都是在无线频谱上以某种频段某种调制方式传输某个协议的数据进行通信,所以通信标准的演进就决定着…

Linux之vim的使用详细解析

个人主页&#xff1a;点我进入主页 专栏分类&#xff1a;C语言初阶 C语言进阶 数据结构初阶 Linux C初阶 算法 欢迎大家点赞&#xff0c;评论&#xff0c;收藏。 一起努力&#xff0c;一起奔赴大厂 目录 一.vim简介 二.vim的基本概念 三.vim的基本操作 3.1准备 …

Kafka:kafka的主从模式和故障切换 ②

一、Kafka整体架构图 二、Kafka原题回答 Kafka集群有主从模式吗&#xff1f; Kafka集群实际上并没有严格意义上的主从模式。Kafka的设计是基于分布式的&#xff0c;每个Topic都会切分为多个Partition&#xff0c;每个Partition都有一个Leader和多个Follower。 所有的读写操作…

计算机网络面经-TCP的拥塞控制

写在前边 前边我们分享了网络分层协议、TCP 三次握手、TCP 四次分手。今天我们继续深入分享一下 TCP 中的拥塞控制。 对于 TCP 的拥塞控制,里边设计到很多细节,平平无奇的羊希望通过这一节能够将这部分内容串通起来,能够让你更深刻的记忆这部分内容。 思维导图 1、什么…

AIGC专栏9——Scalable Diffusion Models with Transformers (DiT)结构解析

AIGC专栏9——Scalable Diffusion Models with Transformers &#xff08;DiT&#xff09;结构解析 学习前言源码下载地址网络构建一、什么是Diffusion Transformer (DiT)二、DiT的组成三、生成流程1、采样流程a、生成初始噪声b、对噪声进行N次采样c、单次采样解析I、预测噪声I…

Spring的另一大的特征:AOP

目录 AOP &#xff08;Aspect Oriented Programming&#xff09;AOP 入门案例&#xff08;注解版&#xff09;AOP 工作流程——代理AOP切入点表达式AOP 通知类型AOP通知获取数据获取切入点方法的参数获取切入点方法返回值获取切入点方法运行异常信息 百度网盘分享链接输入密码数…

【Linux基础】Linux自动化构建工具make/makefile

背景 会不会写makefile&#xff0c;从一个侧面说明了一个人是否具备完成大型工程的能力一个工程中的源文件不计数&#xff0c;其按类型、功能、模块分别放在若干个目录中&#xff0c;makefile定义了一系列的规则来指定&#xff0c;哪些文件需要先编译&#xff0c;哪些文件需要后…

性格正直的人适合什么职业?

有信仰&#xff0c;有责任&#xff0c;有骨气&#xff0c;有尊严&#xff0c;这应该是大多数人对正直的人的理解&#xff0c;他们的心中有信仰&#xff0c;肩上有责任&#xff0c;灵魂有骨气&#xff0c;头上有尊严&#xff0c;不管在什么时候都能够坚守道德准则&#xff0c;不…

【文生视频】Diffusion Transformer:OpenAI Sora 原理、Stable Diffusion 3 同源技术

文生视频 Diffusion Transformer&#xff1a;Sora 核心架构、Stable Diffusion 3 同源技术 提出背景变换器的引入Diffusion Transformer (DiT)架构Diffusion Transformer (DiT)总结 OpenAI Sora 设计思路阶段1: 数据准备和预处理阶段2: 架构设计阶段3: 输入数据的结构化阶段4: …

蓝桥杯算法赛 第 6 场 小白入门赛 解题报告 | 珂学家 | 简单场 + 元宵节日快乐

前言 整体评价 因为适逢元宵节&#xff0c;所以这场以娱乐为主。 A. 元宵节快乐 题型: 签到 节日快乐&#xff0c;出题人也说出来自己的心愿, 祝大家AK快乐! import java.util.Scanner;public class Main {public static void main(String[] args) {System.out.println(&qu…

信息抽取(UIE):使用自然语言处理技术提升证券投资决策效率

一、引言 在当今快速变化的证券市场中&#xff0c;信息的价值不言而喻。作为一名资深项目经理&#xff0c;我曾领导一个关键项目&#xff0c;旨在通过先进的信息抽取技术&#xff0c;从海量的文本数据中提取关键事件&#xff0c;如企业并购、新产品发布以及政策环境的变动。这些…

学会字符转换

字符转换 题目描述&#xff1a;解法思路&#xff1a;解法代码&#xff1a;运行结果&#xff1a; 题目描述&#xff1a; 输入⼀一个字符串&#xff0c;将字符串中大写字母全部转为小写字母&#xff0c;小写字母转成大写字母&#xff0c;其他字符保持不变。注&#xff1a;字符串…

typescript使用解构传参

看下面这个函数 interface Student {id: number;name: string;class: string;sex: string;}function matriculation(student: Student) {//...}我们要调用它,就需要传递一个实现了Student约束的对象进去 interface Student {id: number;name: string;class: string;sex: string…

音视频数字化(数字与模拟-电视)

上一篇文章【音视频数字化(数字与模拟-音频广播)】谈了音频的广播,这次我们聊电视系统,这是音频+视频的采集、传输、接收系统,相对比较复杂。 音频系统的广播是将声音转为电信号,再调制后发射出去,利用“共振”原理,收音机接收后解调,将音频信号还原再推动扬声器,我…

Liunx--nginx负载均衡--前后端分离项目部署

一.nginx简介 Nginx是一个高性能的HTTP和反向代理服务器&#xff0c;它以其轻量级、占用资源少、并发能力强而广受欢迎。 详细介绍 开发背景与特点&#xff1a;Nginx由俄罗斯人Igor Sysoev开发&#xff0c;它是一个自由的、开源的软件。Nginx设计上注重性能和效率&#xff0c;能…

数据库安全性与完整性设计

文章标签集合[数据库安全,数据敏感,通信安全,MD5,盐加密] 1 系统设计 1.1设计目标 &#xff08;1&#xff09;确定系统中需要保护的敏感数据和通信内容&#xff1b; &#xff08;2&#xff09;设计合适的签名、加密和解密算法&#xff1b; &#xff08;3&#xff09;实现…

docker-compose 搭建laravel环境

laravel环境包含nginx,mysql,php7.4,redis 一、安装好docker后pull镜像 1.nginx镜像 docker pull nginx:latest单独启动容器 docker run --name nginx -p 80:80 -d nginx 2.php镜像 docker pull php:7.4-fpm3.mysql镜像 docker pull mysql:5.74.redis镜像 docker pull r…

Mysql5.7主从复制搭建

注意不适用Mysql8 Docker搭建Mysql主从复制 docker run -p 3307:3306 --name mysql-master \ -v /usr/local/develop/mysql-master/log:/var/log/mysql \ -v /usr/local/develop/mysql-master/data:/var/lib/mysql \ -v /usr/local/develop/mysql-master/conf:/etc/mysql/con…

基于相位的运动放大:如何检测和放大难以察觉的运动(02/2)

目录 一、说明二、算法三、准备处理四、高斯核五、带通滤波器六、复杂的可操纵金字塔七、最终预处理步骤八、执行处理九、金字塔的倒塌十、可视化结果十一、结论 一、说明 日常物体会产生人眼无法察觉的微妙运动。在视频中&#xff0c;这些运动的幅度小于一个像素&#xff0c;…

记录一些mac电脑重装mysql和pgsql的坑

为什么要重装,是想在mac电脑 创建data目录…同事误操作,导致电脑重启不了.然后重装系统后,.就连不上数据库了.mysql和pgsql两个都连不上.网上也查了很多资料.实在不行,.就重装了… 重装mysql. 1.官网下载 https://www.mysql.com/downloads/ 滑到最下面 选择 选择对应的芯片版本…