基于DDD的项目结构

前言

本篇文章主要,通过《基础项目结构(二)》和《基础项目结构(一)》技术基础,再结合现今DDD驱动模型。表述一下自己的代码结构思路。

架构

DDD系统采用传统的分层架构。其中核心域只位于架构中的其中一层,其上为用户接口层应用层,其下是基础设施层。在这里插入图片描述
再从另一个角度看我们的系统
在这里插入图片描述
从上面2个角度分析得:

  • 接口层主要为了爆露端口,适配数据给应用程序
  • 基础服务层主要为应用程序供服务(存储,外部)
  • 应用+领域层则是我们真正处理业务的地方
    在这里插入图片描述
    再看这CQRS模型图,描述出了领域层和应用层的关系。
  1. 领域层不适用查询请求,因为查询请求具有多样性。而领域约束性较强。所以对于查询请求可直接
  2. 对于写操作,领域模型完全可以适应。
  • 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);

    }
}
  1. 实体对象的成员用值对象描述
  2. 实体对象有唯一性标识AccountId
  3. 实体具有可变性,提供withdraw(转出)和deposit(转入)两大方法

聚合根 Aggregate

生活中存在一些实体不仅仅由值对象形成,他不可以由其它entity组成。我们把此类实体称为聚合根。以订单为例子
在这里插入图片描述

领域服务 Domain Service

领域服务主要提供此领域的相关操作,此操作不属于实体和值对象,主要特性如下

  1. 执行一个显著的业务操作过程
  2. 对领域对象进行转换
  3. 以多个领域对象作为输入进行计算(重要)
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);
    }
}

领域管理

领域实体的创建主要分为以下二种情况:

  1. 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);
    }
}
  1. 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;
    }
}
  1. 从TransferCommand获取参数
  2. 根据参数使用accountRepository调用相应领域实体
  3. 根据外部接口获取领域ExchangeRate
  4. 使用领域服务accountTransferDmService处理转帐业务
  5. 调用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模型主即读写分离

  1. 读直接普通service
  2. 写直接领域模块

不排除简单读走领域
不排除高并发写场景,直接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

猜你喜欢

转载自blog.csdn.net/y3over/article/details/118312494
ddd