关于架构设计:DDD分层架构最佳实践

32次阅读

共计 8137 个字符,预计需要花费 21 分钟才能阅读完成。

还在单体利用的时候就是分层架构一说,咱们用得最多的就是三层架构。而当初曾经是微服务时代,在微服务架构模型比拟罕用的有几个,例如:整洁架构,CQRS(命令查问拆散)以及六边形架构。每种架构模型都有本人的利用场景,但其外围都是“高内聚低耦合 ”准则。而使用 畛域驱动设计(DDD)理念以应答日常减速的业务变动对架构的影响,架构的边界越业越清晰,各施其职,这也合乎微服务架构的设计思维。以畛域驱动设计(DDD)为理念的分层架构曾经成为微服务架构实际的最佳实际办法。

一、什么是 DDD 分层架构

1. 传统三层架构

要理解 DDD 分层架构,首页先理解传统的三层架构。

传统三层架构流程:

  • 第一步思考的是数据库设计,数据表如何建,表之间的关系如何设计
  • 第二步就是搭建数据拜访层,如选一个 ORM 框架或者拼接 SQL 操作
  • 第三步就是业务逻辑的实现,因为咱们先设计了数据库,咱们整个的思考都会围绕着数据库,想着怎么写能力把数据正确地写入数据库中,这时 CRUD 的规范作法就呈现了,也就没有太多思考面向对象,解耦的事件了,这样的代码对日常的保护天然是越来越艰难的
  • 第四步表示层次要面向用户的输入

2. DDD 分层架构

为了解决高耦合问题并轻松应答当前的零碎变动,咱们提出了使用 畛域驱动设计 的理念来设计架构。

此段落局部总结来源于欧翻新《DDD 实际课》的《07 | DDD 分层架构:无效升高层与层之间的依赖》读后感

1)畛域层

首先咱们抛开数据库的困扰,先从业务逻辑动手开始 ,设计时不再思考数据库的实现。将以前的业务逻辑层(BLL)拆分成了 畛域层 应用层

畛域层聚焦业务对象的业务逻辑实现,体现事实世界业务的逻辑变动。它用来表白业务概念、业务状态和业务规定,对于业务剖析可参照:《应用畛域驱动设计剖析业务》

2)应用层

应用层是畛域层的下层,依赖畛域层,是各聚合的协调和编排,原则上是不包含任何业务逻辑。它以较粗粒度的关闭为前端接口提供反对。除了提供下层调用外,还能够包含事件和音讯的订阅。

3)用户接口层

用户接口层面向用户拜访的数据入向接口,可按不同场景提供不一样的用户接口实现。面向 Web 的可应用 http restful 的形式提供服务,可减少平安认证、权限校验,日志记录等性能;面向微服务的可应用 RPC 形式提供服务,可减少限流、熔断等性能。

4)基础设施层

基础设施层是数据的出向接口,封装数据调用的技术细节。可为其它任意层提供服务,但为了解决耦合的问题采纳了依赖倒置准则。其它层只依赖基础设施的接口,于具体实现进行拆散。

二、DDD 分层代码实现

1. 构造模型

2. 目录构造

.
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── fun
    │   │       └── barryhome
    │   │           └── ddd
    │   │               ├── WalletApplication.java
    │   │               ├── application
    │   │               │   ├── TradeEventProcessor.java
    │   │               │   ├── TradeMQReceiver.java
    │   │               │   └── TradeManager.java
    │   │               ├── constant
    │   │               │   └── MessageConstant.java
    │   │               ├── controller
    │   │               │   ├── TradeController.java
    │   │               │   ├── WalletController.java
    │   │               │   └── dto
    │   │               │       └── TradeDTO.java
    │   │               ├── domain
    │   │               │   ├── TradeService.java
    │   │               │   ├── TradeServiceImpl.java
    │   │               │   ├── enums
    │   │               │   │   ├── InOutFlag.java
    │   │               │   │   ├── TradeStatus.java
    │   │               │   │   ├── TradeType.java
    │   │               │   │   └── WalletStatus.java
    │   │               │   ├── event
    │   │               │   │   └── TradeEvent.java
    │   │               │   ├── model
    │   │               │   │   ├── BaseEntity.java
    │   │               │   │   ├── TradeRecord.java
    │   │               │   │   └── Wallet.java
    │   │               │   └── repository
    │   │               │       ├── TradeRepository.java
    │   │               │       └── WalletRepository.java
    │   │               └── infrastructure
    │   │                   ├── TradeRepositoryImpl.java
    │   │                   ├── WalletRepositoryImpl.java
    │   │                   ├── cache
    │   │                   │   └── Redis.java
    │   │                   ├── client
    │   │                   │   ├── AuthFeignClient.java
    │   │                   │   └── LocalAuthClient.java
    │   │                   ├── jpa
    │   │                   │   ├── JpaTradeRepository.java
    │   │                   │   └── JpaWalletRepository.java
    │   │                   └── mq
    │   │                       └── RabbitMQSender.java
    │   └── resources
    │       ├── application.properties
    │       └── rabbitmq-spring.xml
    └── test
        └── java


此构造为繁多微服务的简略构造,各层在同一个模块中。

在大型项目开发过程中,为了达到外围模块的权限管制或更好的灵活性可适当调整结构,可参考《数字钱包零碎》系统结构

3. 畛域层实现(domain)

在业务剖析(《应用畛域驱动设计剖析业务》)之后,开始编写代码,首先就是写畛域层,创立 畛域对象 畛域服务接口

1)畛域对象

这里的畛域对象包含实体对象、值对象。

实体对象:具备惟一标识,能独自存在且可变动的对象

值对象:不能独自存在或在逻辑层面独自存在无意义,且不可变动的对象

聚合:多个对象的汇合,对外是一个整体

聚合根:聚合中可代表整个业务操作的实体对象,通过它提供对外拜访操作,它保护聚合外部的数据一致性,它是聚合中对象的管理者

代码示例:

// 交易
public class TradeRecord extends BaseEntity {
    /**
     * 交易号
     */
    @Column(unique = true)
    private String tradeNumber;
    /**
     * 交易金额
     */
    private BigDecimal tradeAmount;
    /**
     * 交易类型
     */
    @Enumerated(EnumType.STRING)
    private TradeType tradeType;
    /**
     * 交易余额
     */
    private BigDecimal balance;
    /**
     * 钱包
     */
    @ManyToOne
    private Wallet wallet;

    /**
     * 交易状态
     */
    @Enumerated(EnumType.STRING)
    private TradeStatus tradeStatus;

      @DomainEvents
    public List<Object> domainEvents() {return Collections.singletonList(new TradeEvent(this));
    }
}

// 钱包
public class Wallet extends BaseEntity {

    /**
     * 钱包 ID
     */
    @Id
    private String walletId;
    /**
     * 明码
     */
    private String password;
    /**
     * 状态
     */
    @Enumerated(EnumType.STRING)
    private WalletStatus walletStatus = WalletStatus.AVAILABLE;
    /**
     * 用户 Id
     */
    private Integer userId;
    /**
     * 余额
     */
    private BigDecimal balance = BigDecimal.ZERO;

}


  • 钱包交易 例子的零碎设计中,钱包的任何操作如:充值、音讯等都是通过交易对象驱动钱包余额的变动
  • 交易对象 钱包对象 均为 实体对象 且组成 聚合 关系,交易对象 是钱包交易业务模型的 聚合根,代表聚合向外提供调用服务
  • 通过剖析 交易对象 钱包对象 为 1 对多关系(@ManyToOne),这里采纳了 JPAORM架构,更多 JPA 实际请参考 >>
  • 这里的领域建模应用的是 贫血模型,构造简略,职责繁多,互相隔离性好但不足面向对象设计思维,对于领域建模可参考《领域建模的贫血模型与充血模型》
  • domainEvents()为 畛域事件 公布的一种实现,作用是 交易对象 任何的数据操作都将触发事件的公布,再配合 事件订阅 实现 事件驱动设计 模型,当然也能够有别的实现形式

2)畛域服务

/**
 * Created on 2020/9/7 11:40 上午
 *
 * @author barry
 * Description: 交易服务
 */
public interface TradeService {

    /**
     * 充值
     *
     * @param tradeRecord
     * @return
     */
    TradeRecord recharge(TradeRecord tradeRecord);

    /**
     * 生产
     *
     * @param tradeRecord
     * @return
     */
    TradeRecord consume(TradeRecord tradeRecord);
}


  • 先定义服务接口,接口的定义须要遵循 事实业务的操作,切勿以程序逻辑或数据库逻辑来设计定义出增删改查
  • 次要的思考方向是交易对象对外可提供哪些服务,这种服务的定义是 粗粒度 高内聚 的,切勿将某些具体代码实现层面的办法定义进去
  • 接口的输入输出参数尽量思考以对象的模式,充沛兼容各种场景变动
  • 对于前端须要的简单查询方法可不在此定义,个别状况下查问并非是一种畛域服务且没有数据变动,可独自解决
  • 畛域服务的实现次要关注逻辑实现,切勿蕴含技术根底类代码,比方缓存实现,数据库实现,近程调用等

3)基础设施接口

public interface TradeRepository {
    /**
     * 保留
     * @param tradeRecord
     * @return
     */
    TradeRecord save(TradeRecord tradeRecord);

    /**
     * 查问订单
     * @param tradeNumber
     * @return
     */
    TradeRecord findByTradeNumber(String tradeNumber);

    /**
     * 发送 MQ 事件音讯
     * @param tradeEvent
     */
    void sendMQEvent(TradeEvent tradeEvent);

    /**
     * 获取所有
     * @return
     */
    List<TradeRecord> findAll();}


  • 基础设施接口放在畛域层次要的目标是缩小畛域层对基础设施层的依赖
  • 接口的设计是不可裸露实现的技术细节,如不能将拼装的 SQL 作为参数

4. 应用层实现(application)

// 交易服务
@Component
public class TradeManager {

    private final TradeService tradeService;
    public TradeManager(TradeService tradeService) {this.tradeService = tradeService;}


    // 充值
    @Transactional(rollbackFor = Exception.class)
    public TradeRecord recharge(TradeRecord tradeRecord) {return tradeService.recharge(tradeRecord);
    }


     // 生产
    @Transactional(rollbackFor = Exception.class)
    public TradeRecord consume(TradeRecord tradeRecord) {return tradeService.consume(tradeRecord);
    }
}

// 交易事件订阅
@Component
public class TradeEventProcessor {

    @Autowired
    private TradeRepository tradeRepository;

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, condition = "# tradeEvent.tradeStatus.name() =='SUCCEED'")
    public void TradeSucceed(TradeEvent tradeEvent) {tradeRepository.sendMQEvent(tradeEvent);
    }
}

// 交易音讯订阅
@Component
public class TradeMQReceiver {@RabbitListener(queues = "ddd-trade-succeed")
    public void receiveTradeMessage(TradeEvent tradeEvent){System.err.println("========MQ Receiver============");
        System.err.println(tradeEvent);
    }
}

应用服务

  • 应用层是很薄的一层,次要用于调用和组合畛域服务,切勿蕴含任何业务逻辑
  • 可包含大量的流程参数判断
  • 因为可能是多个畛域服务组合操作调用,如果存在原子性要求能够减少 @Transactional 事务管制

事件订阅

  • 事件订阅是过程内多个畛域操作合作解耦的一种实现形式,它也是过程内所有后续操作的接入口
  • 它与应用服务的组合操作用处不一样,组合是依据场景需要可增可减,但事件订阅后的操作是绝对固化的,次要是满足逻辑的一致性要求
  • TransactionPhase.AFTER_COMMIT配置是在前一操作事务实现后再调用,从而缩小后续操作对前操作的影响
  • 事件订阅可能会有多个音讯主体,为了方便管理最好对立在一个类里解决
  • MQ 音讯公布个别放在事件订阅中

音讯订阅

  • 音讯订阅是多个微服务间合作解耦的异步实现形式
  • 音讯体尽量以对立的对象包装进行传递,升高对象异构带来的解决难度

5. 基础设施层(infrastructure)

@Repository
public class TradeRepositoryImpl implements TradeRepository {

    private final JpaTradeRepository jpaTradeRepository;
    private final RabbitMQSender rabbitMQSender;
    private final Redis redis;

    public TradeRepositoryImpl(JpaTradeRepository jpaTradeRepository, RabbitMQSender rabbitMQSender, Redis redis) {
        this.jpaTradeRepository = jpaTradeRepository;
        this.rabbitMQSender = rabbitMQSender;
        this.redis = redis;
    }

    @Override
    public TradeRecord save(TradeRecord tradeRecord) {return jpaTradeRepository.save(tradeRecord);
    }

    /**
     * 查问订单
     */
    @Override
    public TradeRecord findByTradeNumber(String tradeNumber) {TradeRecord tradeRecord = redis.getTrade(tradeNumber);
        if (tradeRecord == null){tradeRecord = jpaTradeRepository.findFirstByTradeNumber(tradeNumber);
            // 缓存
            redis.cacheTrade(tradeRecord);
        }

        return tradeRecord;
    }

    /**
     * 发送事件音讯
     * @param tradeEvent
     */
    @Override
    public void sendMQEvent(TradeEvent tradeEvent) {
        // 发送音讯
        rabbitMQSender.sendMQTradeEvent(tradeEvent);
    }

    /**
     * 获取所有
     */
    @Override
    public List<TradeRecord> findAll() {return jpaTradeRepository.findAll();
    }
}
  • 基础设施层是数据的输入向,次要蕴含数据库、缓存、音讯队列、近程拜访等的技术实现
  • 根底设计层对外暗藏技术实现细节,提供粗粒度的数据输入服务
  • 数据库操作:畛域层传递的是数据对象,在这里能够按数据表的实现形式进行拆分实现

6. 用户接口层(controller)

@RequestMapping("/trade")
@RestController
public class TradeController {

    @Autowired
    private TradeManager tradeManager;

    @Autowired
    private TradeRepository tradeRepository;

    @PostMapping(path = "/recharge")
    public TradeDTO recharge(@RequestBody TradeDTO tradeDTO) {return TradeDTO.toDto(tradeManager.recharge(tradeDTO.toEntity()));
    }

    @PostMapping(path = "/consume")
    public TradeDTO consume(@RequestBody TradeDTO tradeDTO) {return TradeDTO.toDto(tradeManager.consume(tradeDTO.toEntity()));
    }

    @GetMapping(path = "/{tradeNumber}")
    public TradeDTO findByTradeNumber(@PathVariable("tradeNumber") String tradeNumber){return TradeDTO.toDto(tradeRepository.findByTradeNumber(tradeNumber));
    }

}
  • 用户接口层面向终端提供服务反对
  • 可依据不同的场景独自一个模块,面向 Web 提供 http restful,面向服务间 API 调用提供 RPG 反对
  • 为 Web 端提供身份认证和权限验证服务,VO 数据转换
  • 为 API 端提供限流和熔断服务,DTO 数据转换
  • 将数据转换从应用层提到用户接口层更不便不同场景之前的需要变动,同时也保障应用层数据格式的统一性

7. 简单数据查问

以上可见并没有波及简单数据查问问题,此问题不波及业务逻辑解决所以不应该放在畛域层解决。

  • 如果简单数据查问需要较多可采纳 CQRS 模式,将查问独自一个模块解决。如果较少可由基础设施层做数据查问,应用层做数据封装,用户接口层做数据调用
  • JPA 不太适宜做多表关联的数据库查问操作,可应用其它的灵活性较高的 ORM 架构
  • 在大数据大并发状况下,多表关联会重大影响数据库性能,能够思考做 宽表查问

三、综述

DDD 分层次要解决各层之间耦合度问题,做到各层各施其职互不影响。各层中畛域层的设计是整个零碎的中枢,最能体现 畛域驱动设计 的核心思想。它的良好设计是保障往后架构的可持续性,可维护性。

四、源代码

文中代码因为篇幅起因有肯定省略并不是残缺逻辑,如有趣味请 Fork 源代码 https://gitee.com/hypier/barry-ddd

五、请关注我的公众号

正文完
 0