标签(空格分隔): 架构设计
还在单体应用的时候就是分层架构一说,我们用得最多的就是三层架构。而现在已经是微服务时代,在微服务架构模型比较常用的有几个,例如:整洁架构,CQRS(命令查询分离)以及六边形架构。每种架构模型都有自己的应用场景,但其核心都是“高内聚低耦合”原则。而运用领域驱动设计
(DDD)理念以应对日常加速的业务变化对架构的影响,架构的边界越业越清晰,各施其职,这也符合微服务架构的设计思想。以领域驱动设计(DDD)为理念的分层架构已经成为微服务架构实践的最佳实践方法。
要了解DDD分层架构,首页先了解传统的三层架构。
传统三层架构流程:
为了解决高耦合问题并轻松应对以后的系统变化,我们提出了运用领域驱动设计
的理念来设计架构。
此段落部分总结来源于欧创新《DDD实践课》的《07 | DDD分层架构:有效降低层与层之间的依赖》读后感
首先我们抛开数据库的困扰,先从业务逻辑入手开始,设计时不再考虑数据库的实现。将以前的业务逻辑层(BLL)拆分成了领域层
和应用层
。
领域层聚焦业务对象的业务逻辑实现,体现现实世界业务的逻辑变化。它用来表达业务概念、业务状态和业务规则,对于业务分析可参照:《使用领域驱动设计分析业务》
应用层是领域层的上层,依赖领域层,是各聚合的协调和编排,原则上是不包括任何业务逻辑。它以较粗粒度的封闭为前端接口提供支持。除了提供上层调用外,还可以包括事件和消息的订阅。
用户接口层面向用户访问的数据入向接口,可按不同场景提供不一样的用户接口实现。面向Web的可使用http restful的方式提供服务,可增加安全认证、权限校验,日志记录等功能;面向微服务的可使用RPC方式提供服务,可增加限流、熔断等功能。
基础设施层是数据的出向接口,封装数据调用的技术细节。可为其它任意层提供服务,但为了解决耦合的问题采用了依赖倒置原则。其它层只依赖基础设施的接口,于具体实现进行分离。
.
├── 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
此结构为单一微服务的简单结构,各层在同一个模块中。
在大型项目开发过程中,为了达到核心模块的权限控制或更好的灵活性可适当调整结构,可参考《 数字钱包系统》系统结构
在业务分析(《使用领域驱动设计分析业务》)之后,开始编写代码,首先就是写领域层,创建领域对象
和领域服务接口
这里的领域对象包括实体对象、值对象。
实体对象:具有唯一标识,能单独存在且可变化的对象
值对象:不能单独存在或在逻辑层面单独存在无意义,且不可变化的对象
聚合:多个对象的集合,对外是一个整体
聚合根:聚合中可代表整个业务操作的实体对象,通过它提供对外访问操作,它维护聚合内部的数据一致性,它是聚合中对象的管理者
代码示例:
// 交易
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),这里采用了JPA
做ORM架构,更多JPA实践请参考>>
这里的领域建模使用的是贫血模型
,结构简单,职责单一,相互隔离性好但缺乏面向对象设计思想,关于领域建模可参考《领域建模的贫血模型与充血模型》
domainEvents()为领域事件
发布的一种实现,作用是交易对象任何的数据操作都将触发事件的发布,再配合事件订阅实现事件驱动设计模型,当然也可以有别的实现方式
/**
* 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);
}
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();
}
// 交易服务
@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);
}
}
应用服务:
事件订阅:
消息订阅:
@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();
}
}
基础设施层是数据的输出向,主要包含数据库、缓存、消息队列、远程访问等的技术实现
基础设计层对外隐藏技术实现细节,提供粗粒度的数据输出服务
数据库操作:领域层传递的是数据对象,在这里可以按数据表的实现方式进行拆分实现
@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));
}
}
以上可见并没有涉及复杂数据查询问题,此问题不涉及业务逻辑处理所以不应该放在领域层处理。
CQRS
模式,将查询单独一个模块处理。如果较少可由基础设施层做数据查询,应用层做数据封装,用户接口层做数据调用DDD分层主要解决各层之间耦合度问题,做到各层各施其职互不影响。各层中领域层的设计是整个系统的中枢,最能体现领域驱动设计
的核心思想。它的良好设计是保证往后架构的可持续性,可维护性。
文中代码由于篇幅原因有一定省略并不是完整逻辑,如有兴趣请Fork源代码 https://gitee.com/hypier/barry-ddd
应用层:聚合各个业务,流程定义的,例如下单,需要把领域:1生成订单、2扣减库存、3奖励积分等等
领域层:完成某个业务,包含业务逻辑什么的。