暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

领域驱动设计:贫血模型与充血模型

星河之码 2022-03-10
1372

尺有所短,寸有所长;不忘初心,方得始终。

DDD的特点之一就是基于充血模型的面向对象开发模式,而传统开发中都使用的是MVC 架构是基于贫血模型的面向过程开发风格。那么什么是贫血模型与充血模型?

1贫血模型

领域对象只有属性值与get和set方法,没有任何业务逻辑。所有的业务逻辑都放在业务层的service上。基于贫血模型的MVC架构非常常见的,具体原因:

  • 我们一般的业务中就是基于SQL 的 CRUD ,贫血模型就足以应付这种业务开发

  • 当我们的业务比较简单的时候,充血模型包含的业务逻辑很简单,领域模型比较薄,跟贫血模型相差不大。

  • 设计风格不同

    面向过程编程风格违反了 OOP 的封装特性,会使得数据和操作不受限制。充血模型的面向对象编程风格会使得我们在设计之初就确定好了对数据操作暴露的操作,在 Service 层定义操作即可,不需要设计数据的CRUD。

    • 充血模型:面向对象的编程风格
    • 贫血模型:面向过程的编程风格
  • 贫血模型的思维已普及固化,开发人员从贫血模型到充血模型的思想转变是很难的,学习成本也高。

下面我们通过一个案例来具体看一下

案例实现:

以一个转账的案例,A给B转账100元,A账户扣减100,B账户加100

  • 账户业务对象
package com.org.edwin.model;
import lombok.Data;
@Data
public class Account {

private String accountId;

private Long balance;
//....其他字段省略
}

  • 转账业务实现
import com.org.edwin.model.Account;
import com.org.edwin.service.AccountService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class AccountServiceImpl implements AccountService {
private AccountDao accountDao;

@Override
@Transactional
public boolean transfer(String fromAccountId, String toAccountId, Long amount) {
Account fromAccount = accountDao.getAccountById(fromAccountId);
Account toAccount = accountDao.getAccountById(toAccountId);

// 检查转出账户余额是否大于转出金额
if (fromAccount.getBalance() < amount) {
return Boolean.FALSE;
}
// 转出账户减少金额
fromAccount.setBalance(fromAccount.getBalance() - amount);

// 转入账户增加金额
toAccount.setBalance(toAccount.getBalance() + amount);

/** 更新数据库 **/
accountDao.updateAccount(fromAccount);
accountDao.updateAccount(toAccount);
return Boolean.TRUE;
}
}

从以上部分伪代码可以看出在贫血模型中:

  • 各层单向依赖,代码结构清楚,易于实现和维护。
  • 设计简单,底层模型稳定。

基于此,它的不足之处也就暴露了出来:

  • 领域对象的领域事件被分离到Service层,在一定程度上违反了OOP特性的封装性
  • service上的业务过于厚重,依赖性强,不利于扩展与维护。

2充血模型

2.1 充血模型

充血模型是指数据和业务逻辑被封装到同一个类中。即领域对象拥有此领域相关行为,包含此领域相关的业务逻辑,同时也包含对领域对象的持久化操作。充血模型满足面向对象的封装特性,是典型的面向对象编程风格。

同样我们将上述案例从贫血模型改造成充血模型来看

  • 账户业务对象
package com.org.edwin.model;

import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;

@Data
public class Account {
private String accountId;

private Long balance;
//....其他字段省略

/**
* 转出方法
* @author Edwin
* @date 2022/2/14 14:24
*/

public void transferOut(Long amount) {
this.balance -= amount;
}
/**
* 转入方法
* @author Edwin
* @date 2022/2/14 14:24
*/

public void transferIn(Long amount) {
this.balance += amount;
}

/**
* 检查转出账户余额是否大于转出金额
* @author Edwin
* @date 2022/2/14 14:24
*/

public void check(Long amount) throws Exception {
if (this.balance < amount) {
throw new Exception("余额不足");
}
}

}

  • 转账业务实现
import com.org.edwin.model.Account;
import com.org.edwin.service.AccountService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class AccountServiceImpl implements AccountService {
private AccountDao accountDao;

@Override
@Transactional
public boolean transfer(String fromAccountId, String toAccountId, Long amount) throws Exception {
Account fromAccount = accountDao.getAccountById(fromAccountId);
Account toAccount = accountDao.getAccountById(toAccountId);
// 检查转出账户余额是否大于转出金额
fromAccount.check(amount);
// 转出账户减少金额
fromAccount.transferOut(amount);
// 转入账户增加金额
toAccount.transferIn(amount);
/** 更新数据库 **/
accountDao.updateAccount(fromAccount);
accountDao.updateAccount(toAccount);
return Boolean.TRUE;
}
}

这里只是一个非常简单的案例,可能看起来区别不是很大,当我们的领域业务比较复杂时,这种模式的优势就会很明显。通过上面的案例改造,可以看出充血模型中:

  • 符合单一职责,领域对象处理与自己相关的所有行为。不像在贫血模型所有的业务逻辑都在领域service中,沉重,臃肿。
  • 将我们的业务逻辑与其他动作(控制事务、权限等)分离。

但是缺点也很明显:

  • 很多时候,我们并不能很清晰的区分什么逻辑应该放在领域服务中,什么样的逻辑应该放在领域对象中。这需要我们根据业务以及自身对DDD的理解加以权衡。

2.2 DDD为什么使用充血模型

  • 在我们MVC的开发模式中,都是SQL 驱动的开发模式先确定业务涉及到的数据库表,根据表结构编写 SQL 语句来CRUD,然后在Service中添加调用,往往很小业务的区别,我们要写不同SQL来实现,SQL的复用性很差。对于复杂业务系统,这种开发方式会让代码越来越混乱,最终导致无法维护。

  • 在充血模型的 DDD 的开发模式中,我们在领域建模的时候就会先理清楚所有的业务,定义领域模型所包含的属性和方法,新功能需求的开发是基于领域模型来完成。领域模型相当于的业务中间层,提供代码的可复用性。


文章转载自星河之码,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论