六边形架构以及应用场景

背景

什么情况下适合用六边形架构?

外部系统的集成方式多变的软件系统,比如ISV等。例如我要开发一个智能体,刚开始在**孵化,后面可以跟SAP,Oracle,用友集成。 当我跟其他厂商集成的时候,无需修改业务逻辑代码,只需要增加适配器即可。
如果跟外部系统集成没有多变,用DDD四层架构或者整洁架构即可。

不同ERP厂商外部调用的差异

  • WEB端和后端集成差异: 金蝶苍穹使用0代码框架,后端通过Plugin类响应前端的请求,其他ERP可能有其他的集成形式
  • 分布式定时任务的框架不同厂商有不同的技术实现
  • 跨微服务集成差异:有些公司使用HTTP,有些使用DUBBO Hessian协议,等等。
  • 跨系统集成通信协议差异:不同公司会开发不同的集成网关。
  • 数据访问方式差异:金蝶苍穹使用自己封装的DB工具操纵数据库,有些公司使用Mybatis,其他公司可能使用其他产品。
  • 数据库存储介质差异:金蝶使用PostgreSQL,其他公司可能使用Oracle,Mysql等。
  • 日志差异:不同ERP的日志系统不同,需要抽象日志接口,适配各系统的日志机制。
  • 监控集成差异:集成Prometheus或自定义健康检查接口,确保Agent在各ERP中的运行状态可追踪。

六边形架构介绍

六边形架构的设计思想源于Alistair Cockburn在2005年提出的“六边形关系图”理论。在这个理论中,软件系统被视为一个六边形,其中有三组组件构成:核心业务逻辑(Domain),输入和输出端口(Ports)以及适配器(Adapters)。这些组件通过一系列接口进行交互,内部的业务逻辑(六边形中心),并通过端口和适配器与外部系统进行交互。
适配器(Adapters) 是连接核心业务逻辑与外部世界(如数据库、用户界面、第三方服务等)的桥梁。它们通过实现端口(Ports)定义的接口,将外部技术细节转化为核心层能理解的业务操作。

与传统分层架构的对比优势

分层

Model

封装核心业务逻辑和状态,无技术依赖(不依赖Spring等)。 按DDD规范要求,业务逻辑在领域层实现。(但是门槛比较高,从过去实施效果来看不太理想,运行业务逻辑写在application层)

Port

定义与外部交互的接口(输入/输出端口)
和外部交互的接口定义,分为输入端口(驱动端口)和输出端口(被驱动端口)。
驱动端口(Driving Ports)输入方向:
外部系统如何与核心业务交互:外部进来的HTTP请求、RPC请求、CLI命令、MQ订阅消息,Schedule(定时任务),在inbound适配器中实现。
被驱动端口(Driven Ports)输出方向:
核心业务需要外部资源支持。数据库访问、RPC调用(调用外部)、文件存储, ES存储,向量数据库等。在outbound适配器中实现。

Application

协调领域逻辑和外部服务,实现业务用例流程 依赖端口接口,不直接操作外部技术

Adapter

实现端口接口,适配具体技术(如 HTTP、数据库) 强技术依赖(Spring、JPA、Feign),定时任务

代码实现

总结:
外部请求 → [左适配器(如HTTP Controller)] → 调用左端口(OrderUseCase) → 应用核心逻辑

外部服务 ← [右适配器(如JPA Repository)] ← 实现右端口(OrderRepository) ← 依赖倒置

输入端口、输入适配器
应用服务(application 层)继承输入端口(接口)
输入适配器通过引用方式依赖输入端口
应用服务通过引用依赖输出端口

输出端口,输出适配器
输出适配器继承输出端口

输入端口

package com.example.demo.port.inbound;

import com.example.demo.port.inbound.dto.OrderRequestDTO;
import com.example.demo.port.inbound.dto.OrderResponseDTO;

public interface OrderUseCase {
    OrderResponseDTO placeOrder(OrderRequestDTO request);
}

收入端口实现类--应用服务

package com.example.demo.application;

import com.example.demo.port.inbound.OrderUseCase;
import com.example.demo.port.inbound.dto.OrderRequestDTO;
import com.example.demo.port.inbound.dto.OrderResponseDTO;
import com.example.demo.port.outbound.OrderRepository;

public class OrderServiceImpl implements OrderUseCase {
    OrderRepository orderRepository; //通过引用依赖右端口
    @Override


    public OrderResponseDTO placeOrder(OrderRequestDTO request) {
         orderRepository.save(null);

         return null;
    }
}

输入适配器

完全解耦:OrderController 不直接实现接口,仅作为 HTTP 适配器调用接口。
​协议无关性:OrderInputPort 接口不包含任何 HTTP 注解,可复用其他适配器(如消息监听器)。
​符合单一职责原则:Controller 仅负责协议转换,业务逻辑集中在 OrderService。

package com.example.demo.adapter.inbound.http;

import com.example.demo.port.inbound.OrderUseCase;
import com.example.demo.port.inbound.dto.OrderRequestDTO;
import com.example.demo.port.inbound.dto.OrderResponseDTO;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

// adapter/web/OrderController.java
@RestController
public class OrderController {
    private OrderUseCase orderInputPort;

    @PostMapping("/orders")
    public OrderResponseDTO placeOrder(@RequestBody OrderRequestDTO request) {
        return orderInputPort.placeOrder(request);
    }
}

输出端口

package com.example.demo.port.outbound;

import com.example.demo.domain.Order;

public interface OrderRepository {
     void save(Order order);

}

输出适配器

package com.example.demo.adapter.outbound.db;

import com.example.demo.domain.Order;
import com.example.demo.port.outbound.OrderRepository;

public class JpaOrderRepository implements OrderRepository {
    @Override
    public void save(Order order) {

    }
}

项目结构

  • 如果项目复杂,建议 adapter,port,application,domain 拆成不同的模块
  • 建议每一层都有自己的枚举,常量类。 工具类按需,一般适配器层一定需要建工具类目录。
  • common :全局工具类,常量

代码示例

常见问题和解决

找不到Port的实现类

可能原因1
start模块没有依赖adapter模块

正确依赖
start->adapter
adapter->application
application->domain,port
port->domain,common
domain->common

可能原因2
AccountMapper 命名为AccountRepositoryImpl,被Spring误认为是Spring要实例化的bean

解决

Springboot启动类增加
@MapperScan("com.kingdee.pe.admin.adapter") // 指定 Mapper 接口所在包

AccountRepository实现修改

@Mapper
public interface AccountMapper extends AccountRepository {
}

可能原因3 Mybatis版本和Springboot版本不一致
可能原因4 Springboot版本和JDK版本不一致

参考资料

posted @ 2025-04-20 12:06  向着朝阳  阅读(94)  评论(0)    收藏  举报