我们都知道,很多业务系统都是基于MVC三层架构来开发,实际上,这是一种基于贫血模型的MVC三层架构开发模式。虽然现在以及成为标准的web项目开发模式,但是却违反了面向对象编程风格,因而被人称为”反模式“。特别是领域驱动设计(Domain Driven Design,简称DDD)盛行之后,这种基于贫血模型的传统的开发模式就更加被人诟病。
做过Java开发项目的,都知道MVC架构模式。MVC 三层架构中的 M 表示 Model,V 表示 View,C 表示 Controller。它将整个项目分为三层:展示层、逻辑层、数据层。MVC 三层开发架构是一个比较笼统的分层方式,落实到具体的开发层面,很多项目也并不会 100% 遵从 MVC 固定的分层方式,而是会根据具体的项目需求,做适当的调整。
比如,现在很多 Web 或者 App 项目都是前后端分离的,后端负责暴露接口给前端调用。这种情况下,我们一般就将后端项目分为 Repository 层、Service 层、Controller 层。其中,Repository 层负责数据访问,Service 层负责业务逻辑,Controller 层负责暴露接口。当然,这只是其中一种分层和命名方式。不同的项目、不同的团队,可能会对此有所调整。不过,万变不离其宗,只要是依赖数据库开发的 Web 项目,基本的分层思路都大差不差。
在这种MVC架构里面,有很多只包含数据(成员变量及对应的getter、setter方法),不包含业务逻辑的类,这种就叫做贫血模型(Anemic Domain Model)。这种贫血模型将数据与操作分离,破坏了面向对象的封装特性,是一种典型的面向过程的编程风格。
在贫血模型中,数据和业务逻辑被分割到不同的类中。充血模型(Rich Domain Model)则正好相反,数据和对应的业务逻辑被封装到同一个类中。因此,这种充血模型满足面向对象的封装特性,是典型的面向对象编程风格。
领域驱动设计主要是用来指导如何解耦业务系统,划分业务模块,定义业务领域模型及其交互。这个概念在2004年就被提出,不过它被大众所熟知还得基于另外一个概念的兴起,那就是微服务。
我们知道,除了监控、调用链追踪、API 网关等服务治理系统的开发之外,微服务还有另外一个更加重要的工作,那就是针对公司的业务,合理地做微服务拆分。而领域驱动设计恰好就是用来指导划分服务的。所以,微服务加速了领域驱动设计的盛行。
不过领域驱动设计不是银弹,也没必要花很多时间研究它。做好领域驱动设计的关键是对业务的熟悉程度,而不是对DDD的熟悉程度。实际上,基于充血模型的DDD开发模式实现的代码,也是按照MVC三层架构分的。Controller层负责暴露接口,Repository层还是负责数据存取,Service层负责核心业务逻辑,它们之间的主要区别在于Service层。
在传统的贫血模型开发模式中,Service层包含Service和Bo两个部分,Bo只包含数据不包含业务逻辑,业务逻辑集中在Service层;而在DDD开发模式种,Service层包含Service类和Domain类两个部分,Domain就相当于贫血模型中的BO,不过Domain是既包含数据,又包含业务逻辑的,相对的是Service类变得非常单薄。
我们前面也讲过,面向过程编程风格有种种弊端,比如,数据和操作分离之后,数据本身的操作就不受限制了。任何代码都可以随意修改数据。既然基于贫血模型的这种传统开发模式是面向过程编程风格的,那它又为什么会被广大程序员所接受呢?关于这个问题,我总结了下面三点原因。
- 业务系统足够简单,很多系统几乎是基于SQL的CRUD操作,贫血模型就足以应对
- 充血模型的设计比贫血模型更加有难度,贫血模型只需要定义好数据,在service实现功能即可
- 思维固化,转型有成本
刚刚我们讲到,基于贫血模型的传统的开发模式,比较适合业务比较简单的系统开发。相对应的,基于充血模型的 DDD 开发模式,更适合业务复杂的系统开发。比如,包含各种利息计算模型、还款模型等复杂业务的金融系统。
两者的区别一个重逻辑,一个重数据,所以根本的区别是什么呢?为什么复杂系统要用基于充血模型的DDD模式开发更有优势?
实际上,这两者还有一个非常重要的区别:会导致不同的开发流程。而基于充血模型的DDD开发模式在应对大型复杂系统时更有优势,为什么这么说呢?
在基于贫血模型开发时,大部分是SQL驱动的,按照固定的模板,实现SQL和接口返回,这样时间久了,各种长得差不多的、区别很小的SQL语句满天飞。这个过程中很少有人会应用领域模型、OOP的概念,也很少有代码复用意识。对于复杂的业务系统来说,这种开发方式会让代码越来越混乱,最终导致无法维护。
在DDD开发模式下,我们需要事先理清所有的业务,定义领域模型所包含的属性和方法,领域模型相当于可复用的业务中间层。新功能需求的开发,都基于之前定义好的这些领域模型来完成。越是复杂的系统,对代码的复用性、易维护性要求就越高,我们越是应该花时间在前期设计上【90%设计,10%编码】。而基于充血模型的 DDD 开发模式,正好需要我们前期做大量的业务调研、领域模型设计,所以它更加适合这种复杂系统的开发。
很多具有支付、购买功能的应用(比如淘宝、滴滴出行、极客时间等)都支持钱包的功能。应用为每个用户开设一个系统内的虚拟钱包账户,支持用户充值、提现、支付、冻结、透支、转赠、查询账户余额、查询交易流水等操作。为了方便理解,我们现在给这个钱包定义以下五个功能:
- 从用户的银行卡账户转账到应用的公共银行卡账户
- 将用户的充值金额加到虚拟钱包余额上
- 记录刚刚的操作流水
我们可以把整个钱包系统的业务划分为两部分,其中一部分单纯跟应用内的虚拟钱包账户打交道,另一部分单纯跟银行账户打交道。我们基于这样一个业务划分,给系统解耦,将整个钱包系统拆分为两个子系统:虚拟钱包系统和三方支付系统。
我们在这里只讲解虚拟钱包系统。
从图中我们可以看出,虚拟钱包系统要支持的操作非常简单,就是余额的加加减减。其中,充值、提现、查询余额三个功能,只涉及一个账户余额的加减操作,而支付功能涉及两个账户的余额加减操作:一个账户减余额,另一个账户加余额。
至于交易流水,交易流水的数据格式包含两个钱包账号,一个是入账钱包账号,一个是出账钱包账号。为什么要有两个账号信息呢?这主要是为了兼容支付这种涉及两个账户的交易类型。不过,对于充值、提现这两种交易类型来说,我们只需要记录一个钱包账户信息就够了。
下面来看一下如何用两种模式来实现这样的虚拟钱包系统?
如果你有开发web项目的经验,那么这个系统对你来说将会非常简单,这是一个典型的Web后端项目三层架构,Controller和VO负责暴露接口,伪代码如下:
1
2
3
4
5
6
7
8
9
10
11
|
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) { ...} //转账
//省略查询transaction的接口
}
|
Service 和 BO 负责核心业务逻辑,Repository 和 Entity 负责数据存取。Service层的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
public class VirtualWalletBo {//省略getter/setter/constructor方法
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 NoSufficientBalanceException(...);
}
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) {
VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactionEntity();
transactionEntity.setAmount(amount);
transactionEntity.setCreateTime(System.currentTimeMillis());
transactionEntity.setType(TransactionType.CREDIT);
transactionEntity.setFromWalletId(walletId);
transactionRepo.saveTransaction(transactionEntity);
VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
BigDecimal balance = walletEntity.getBalance();
walletRepo.updateBalance(walletId, balance.add(amount));
}
@Transactional
public void transfer(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);
}
}
|
在上一节课中,我们讲到,基于充血模型的 DDD 开发模式,跟基于贫血模型的传统开发模式的主要区别就在 Service 层,Controller 层和 Repository 层的代码基本上相同。所以,我们重点看一下,Service 层按照基于充血模型的 DDD 开发模式该如何来实现。
在这种开发模式下,我们把虚拟钱包 VirtualWallet 类设计成一个充血的 Domain 领域模型,并且将原来在 Service 类中的部分业务逻辑移动到 VirtualWallet 类中,让 Service 类的实现依赖 VirtualWallet 类。具体的代码实现如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
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) {
//...跟基于贫血模型的传统开发模式的代码一样...
}
}
|
看了上面的代码,你可能会说,领域模型 VirtualWallet 类很单薄,包含的业务逻辑很简单。相对于原来的贫血模型的设计思路,这种充血模型的设计思路,貌似并没有太大优势。你说得没错!这也是大部分业务系统都使用基于贫血模型开发的原因。不过,如果虚拟钱包系统需要支持更复杂的业务逻辑,那充血模型的优势就显现出来了。比如,我们要支持透支一定额度和冻结部分余额的功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
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 = 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 类添加了简单的冻结和透支逻辑之后,功能看起来就丰富了很多,代码也没那么单薄了。如果功能继续演进,我们可以增加更加细化的冻结策略、透支策略、支持钱包账号(VirtualWallet id 字段)自动生成的逻辑(不是通过构造函数经外部传入 ID,而是通过分布式 ID 生成算法来自动生成 ID)等等。VirtualWallet 类的业务逻辑会变得越来越复杂,也就很值得设计成充血模型了。
1.DDD开发模式中业务逻辑都在Domain中,那么Service的职责是什么?
- 负责与repo层交流,保持领域模型的独立性,不与其他层耦合过深
- 业务聚合,有些逻辑是多个函数组合到一起的,使用service进行聚合
- Service负责一些非功能性及与三方系统交互的工作,比如幂等、事务、发邮件、发消息、记录日志等
2.Controller和Repo层仍然是贫血模型,是否有必要进行充血领域建模呢?
- 没有必要,Controller 层主要负责接口的暴露,Repository 层主要负责与数据库打交道,这两层包含的业务逻辑并不多
- Repo层的Entity由于生命周期有限,一旦传递到Service层后就会被转化为BO或者Domain来处理后续逻辑,所以不需要充血模型
- Controller层的VO实际上是一种DTO,从功能上来讲是不包含逻辑只包含数据的,所以设计成贫血模型也是合理的