前言
本篇文章主要,通过《基础项目结构(二)》和《基础项目结构(一)》技术基础,再结合现今DDD驱动模型。表述一下自己的代码结构思路。
架构
DDD系统采用传统的分层架构。其中核心域只位于架构中的其中一层,其上为用户接口层和应用层,其下是基础设施层。
再从另一个角度看我们的系统
从上面2个角度分析得:
- 接口层主要为了爆露端口,适配数据给应用程序
- 基础服务层主要为应用程序供服务(存储,外部)
- 应用+领域层则是我们真正处理业务的地方
再看这CQRS模型图,描述出了领域层和应用层的关系。
- 领域层不适用查询请求,因为查询请求具有多样性。而领域约束性较强。所以对于查询请求可直接
- 对于写操作,领域模型完全可以适应。
- CQRS模式成熟应用在mysql的《快照读和当前读》
- 领域层不处理事务等问题,事务作用在应用层
DDD领域
理解DDD,我们要先理解什么是领域
领域是有范围的,我们能够根据领域范围的不同来定义界限,定义边界
我们举个例子,比如我们要研究轿车,首先我们先确定领域为轿车,再把“轿车”如下图拆解
如图我们拆分 发动机、离合器、变速箱、车轮、气囊、内饰,这些是轿车的子域
一个领域是由一个或者多个子域构成的,子域还可以再进行拆分,也就是子子域.
根据子域不同功能属性和重要性,将领域分为
- 核心域
指的是这个业务的核心功能,核心模块。比如,轿车主打的是动力充沛的话,那么发动机一定是核心域,比如说主打的是操控的话,那么变速箱、离合器一定是核心域。 - 通用域
没有太定制化功能, 对于汽车来说我们可以把内饰理解为通用域,因为比如说坐垫,化妆镜,这类不一定是只能给某一辆单独型号的车来使用的东西,所以具有一些通用的属性。对于系统来说的话,通用域则是你需要用到的通用系统,比如认证、权限等等 - 支撑域
处于通用和核心之间。功能较通用,但是要定制。以汽车为例,我们可以把车轮和气囊作为支撑域来看待,因为对于车轮和气囊来说,它们的大小尺寸是严格和车辆保持一致的,也就是说不具备通用性,是极具有车厂风采的个性化产品
确定了域后定位后,继续向下细分。比如“发动机” 拆成 “发动曲柄栏杆机构” 和“配气机构”,“配气机构” 再拆成 “气门组”和“气门传动组”。一直可拆分到进气门、排气门、气门导管、气门座及气门弹簧等零件。
那么这些零件就对应着聚合根、实体、值对象
值对象Domain Primitive(DP)
不从任何其他事物发展而来,初级的形成或生长的早期阶段。类比Java中的 Integer、String对象都是从byte[]引变过来成为基础对象。在领域中,我们也会创造一些基础对象PhoneNumber,Name,Address。
public class PhoneNumber {
private final String number;
public String getNumber() {
return number;
}
public PhoneNumber(String number) {
if (number == null) {
throw new ValidationException("number不能为空");
} else if (isValid(number)) {
throw new ValidationException("number格式错误");
}
this.number = number;
}
public String getAreaCode() {
for (int i = 0; i < number.length(); i++) {
String prefix = number.substring(0, i);
if (isAreaCode(prefix)) {
return prefix;
}
}
return null;
}
private static boolean isAreaCode(String prefix) {
String[] areas = new String[]{
"0571", "021", "010"};
return Arrays.asList(areas).contains(prefix);
}
public static boolean isValid(String number) {
String pattern = "^0?[1-9]{2,3}-?\\d{8}$";
return number.matches(pattern);
}
}
从技术角度讲,他们都是String.但这么一包装则成为领域不可拆分的值。
实体 entity
实体是一个唯一的东西。并且可以持续地变化。它和值对象的区分点是唯一身份标识和可变性。
/**
* 用户帐号实体
*/
@Data
public class Account {
private AccountId id;
private AccountNumber accountNumber;
private UserId userId;
private Money available;
private Money dailyLimit;
private Currency currency;
/**
* 转出操作,因为此方法只改变Account的值,所以写在方法内
* @param money 转出金额
*/
public void withdraw(Money money) throws InvalidCurrencyException, DailyLimitExceededException {
if (this.available.compareTo(money) < 0){
throw new InsufficientFundsException();
}
if (this.dailyLimit.compareTo(money) < 0){
throw new DailyLimitExceededException();
}
if (!this.getCurrency().equals(money.getCurrency())){
throw new InvalidCurrencyException();
}
this.available = this.available.subtract(money);
}
// 转入
public void deposit(Money money) throws InvalidCurrencyException {
if (!this.getCurrency().equals(money.getCurrency())){
throw new InvalidCurrencyException();
}
this.available = this.available.add(money);
}
}
- 实体对象的成员用值对象描述
- 实体对象有唯一性标识AccountId
- 实体具有可变性,提供withdraw(转出)和deposit(转入)两大方法
聚合根 Aggregate
生活中存在一些实体不仅仅由值对象形成,他不可以由其它entity组成。我们把此类实体称为聚合根。以订单为例子
领域服务 Domain Service
领域服务主要提供此领域的相关操作,此操作不属于实体和值对象,主要特性如下
- 执行一个显著的业务操作过程
- 对领域对象进行转换
- 以多个领域对象作为输入进行计算(重要)
public class AccountTransferDmServiceImpl implements AccountTransferDmService {
/**
* 两帐户相互汇款,涉及两entity的变动,所以放在domainService处理
* domainService方法同entity方法一样,只算对象状态的变化,不写外界交互
* domainService只是对entity方法的补足
*/
@Override
public void transfer(Account sourceAccount, Account targetAccount, Money targetMoney, ExchangeRate exchangeRate) throws DailyLimitExceededException {
Money sourceMoney = exchangeRate.exchageTo(targetMoney);
sourceAccount.withdraw(sourceMoney);
targetAccount.deposit(targetMoney);
}
}
领域管理
领域实体的创建主要分为以下二种情况:
- Repository,从数据库获取与存储
public class AccountRepositoryImpl implements AccountRepository {
@Autowired
private AccountMapper accountDAO;
@Autowired
private AccountBuilder accountBuilder;
@Override
public Account find(AccountId id) throws Exception {
AccountDO accountDO = accountDAO.selectById(id.getValue());
return accountBuilder.toAccount(accountDO);
}
@Override
public Account find(AccountNumber accountNumber) throws Exception {
AccountDO accountDO = accountDAO.selectByAccountNumber(accountNumber.getValue());
if (accountDO == null){
throw new BusinessException(String.format("账户[%s]不存在", accountNumber.getValue()));
}
return accountBuilder.toAccount(accountDO);
}
@Override
public Account find(UserId userId) throws Exception {
AccountDO accountDO = accountDAO.selectByUserId(userId.getId());
if (accountDO == null){
throw new BusinessException("账户不存在");
}
return accountBuilder.toAccount(accountDO);
}
@Override
public Account save(Account account) throws Exception {
AccountDO accountDO = accountBuilder.fromAccount(account);
if (accountDO.getId() == null) {
accountDAO.insert(accountDO);
} else {
accountDAO.update(accountDO);
}
return accountBuilder.toAccount(accountDO);
}
}
- factory/converter 根据外部参数,内存中创建
public interface ExchangeRateConverter {
ExchangeRateConverter CONVERTER = Mappers.getMapper(ExchangeRateConverter.class);
default ExchangeRate toExchangeRate(ExchangeRateEo exchangeRateEo){
return new ExchangeRate(exchangeRateEo.getRage(),
new Currency(exchangeRateEo.getSourceCurrency()),
new Currency(exchangeRateEo.getTargetCurrency()));
}
}
界限上下文
串连起外部依赖与领域模型
public class TransferServiceImpl implements TransferService {
// private AuditMessageProducer auditMessageProducer;
private final ExchangeRateExService exchangeRateExService;
private final AccountTransferDmService accountTransferDmService;
private final AccountRepository accountRepository;
@Transactional
@Override
public Boolean transfer(TransferCommand transferCommand) throws DailyLimitExceededException {
Money targetMoney = new Money(transferCommand.getTargetAmount(), new Currency(transferCommand.getTargetCurrency()));
Account sourceAccount = accountRepository.find(new UserId(transferCommand.getSourceUserId()));
Account targetAccount = accountRepository.find(new AccountNumber(transferCommand.getTargetAccountNumber()));
// 通过Converter将外部的转为domain valueobject
ExchangeRateEo exchangeRateEo = exchangeRateExService.getExchangeRate(sourceAccount.getCurrency().getValue(), targetMoney.getCurrency().getValue());
ExchangeRate exchangeRate = ExchangeRateConverter.CONVERTER.toExchangeRate(exchangeRateEo);
accountTransferDmService.transfer(sourceAccount, targetAccount, targetMoney, exchangeRate);
accountRepository.save(sourceAccount);
accountRepository.save(targetAccount);
//
// // 发送审计消息
// AuditMessage message = new AuditMessage(sourceAccount, targetAccount, targetMoney);
// auditMessageProducer.send(message);
return true;
}
}
- 从TransferCommand获取参数
- 根据参数使用accountRepository调用相应领域实体
- 根据外部接口获取领域ExchangeRate
- 使用领域服务accountTransferDmService处理转帐业务
- 调用accountRepository存储实体
项目实战
项目地址:《ddd-simple-demo》
层级划分
根据包层对其架构划分如下:
对应层级 | 包名 |
---|---|
接口层 | com.moyao.demo.interfaces |
应用层 | com.moyao.demo.application |
领域层 | com.moyao.demo.domain |
基础设施层 | com.moyao.demo.infra |
模块划分
六边形架构模式以模块方式体现如下:
对应模块 | 包名 | 说明 |
---|---|---|
打包模块 | demo-app-boot | 负责打包,环境隔离 |
外部依赖 | demo-app-dependon | 为了模拟外部rpc的包,可舍弃 |
基础设施提供接口 | demo-app-infra | 把基础设施所能提供的服务 |
jdbc基础设施层 | demo-infra-jdbc | 提供jdbc基础服务 |
rpc基础设施层 | demo-infra-rpc | 提供外部依赖基础服务 |
核心模块 | demo-application | 包括"应用层&领域层"两方面 |
rpc接口层 | demo-interfaces-rpc | 提供对外rpc接口 |
web接口层 | demo-interfaces-web | 提供对外web接口 |
CQRS实现
CQRS模型主即读写分离
- 读直接普通service
- 写直接领域模块
不排除简单读走领域
不排除高并发写场景,直接service
总结:CQ场景可具体情况具体考虑。
// 转帐业务直接走领域
@PostMapping("/transfer")
public Result transfer(@RequestBody TransferCommand cmd) throws DailyLimitExceededException {
cmd.setSourceUserId(UserHolder.get());
Boolean result = transferService.transfer(cmd);
return result? Result.success() : Result.fail();
}
// 用户查询直接普通service或者dao直接来
@GetMapping("/user")
public Result getUser() {
Long id = UserHolder.get();
UsersDo usersDo = userDao.selectById(id);
Preconditions.checkNotNull(usersDo);
LoginUserVo loginUserVo = UserConverter.CONVERTER.toLoginUserVo(usersDo);
return Result.success(loginUserVo);
}
主要参考
《实现领域驱动设计》
《领域驱动设计中的子域、核心域、通用域、支撑域》
《DDD 系列- Domain Primitive》
《DDD系列 第二弹 - 应用架构》
《DDD系列 第三讲 - Repository模式》
《DDD系列第四讲:领域层设计规范》
《DDD系列第五讲:聊聊如何避免写流水账代码》
《ddd-demo》
《dddbook》