# 0x00、什么是六边形架构?
对于六边形在全网的解释已经有很多了。对于六边形架构本身是什么,他解决了什么问题在这篇文章里就不多赘述了。这边选了两篇我觉得值得一读的文章:
- Hexagonal architecture
[Hexagonal architecture](https://alistair.cockburn.us/hexagonal-architecture/)
六边形架构出处原文,认真通读一下这篇文章会发现,原文本身就已经把六边形架构是什么、解决了什么问题、怎么去实现、有哪些具体使用场景都给讲清楚了。
- 六边形架构[双语]
[六边形架构[双语]](https://blog.csdn.net/zhongjinggz/article/details/43889821)
这篇是原文的译文,有中英文对照,如果懒得慢慢读可以读这一篇
- 深入理解六边形架构
[深入理解六边形架构](https://www.cnblogs.com/zhongpan/p/7606430.html)
这篇文章简明地说明了六边形是什么、解决了什么问题,最后给出了基于C++的示例。示例比较简单,而我们这篇文章就是要讨论六边形架构的具体实例是怎么样的。
# 0x01、六边形架构的Java示例代码
自己根据理解和经验写了一个六边形架构的示例,下面讲的东西都基于这份示例。这份SpringBoot代码不能直接跑起来,需要做一些数据库的配置等。
[ZwiebelnX/hexagonal-architecture-java-example](https://github.com/ZwiebelnX/hexagonal-architecture-java-example)
## 1)项目示意图
自上而下地说明一下示例的结构。
下面这副图的样式随便搜一下六边形架构都可以看到各式各样的图解。这张图我根据Java和示例的特性做了一些小改动,使得图解和示例代码更具有映射性。
对图解的一些概念进行一些解释
![HexArch.png](https://cong-onion.cn/upload/2021/05/image-550ec1a0f55c4f91bbd74037f6b63c73.png)
### 箭头方向
箭头方向实际上指代的是**驱动**的方向。**驱动**指的是使得某个应用或者实体从不活跃的状态变成活跃的状态。
而基于上面的概念,箭头的起始代表驱动方(又名**主参与者,primary actors**),而所指则代表被驱动方(又名**次参与者,secondary actor**)。
取图图解中的**红色路径**,可以以文字表述为:
前端的HTTP请求驱动RESTFul Controller适配器转换数据 **——>** RESTFul Controller驱动Use Cases执行业务逻辑 **——>** Use Cases根据需要驱动Repository适配器进行查询数据的转换 **——>** Repository适配器驱动数据库等持久化基础设施完成数据库查询。
### “主适配器”和“从适配器”
虽然在架构设计上把不同功能的适配器不做区分是很有用的,但是在实现的过程会有两大类的适配器,Cockburn大佬把他命名成为**主适配器**和**从适配器。**
> The ports and adapters pattern is deliberately written pretending that all ports are fundamentally similar. That pretense is useful at the architectural level. In implementation, ports and adapters show up in two flavors, which I will call ‘’primary’’ and ‘’secondary’’, for soon-to-be-obvious reasons. They could be also called ‘’driving’’ adapters and ‘’driven’’ adapters.
他们具体的区别依赖于上一小节所说的驱动的概念。
**主适配器是驱动应用活跃的适配器,而从适配器则是被应用所驱动而活跃的适配器。**
> This is related to the idea from use cases of “primary actors” and “secondary actors”. A ‘’primary actor’’ is an actor that drives the application (takes it out of quiescent state to perform one of its advertised functions). A ‘’secondary actor’’ is one that the application drives, either to get answers from or to merely notify. The distinction between ‘’primary ‘’and’’ secondary ‘’lies in who triggers or is in charge of the conversation.
### “内部”和“外部”
“内部”和“外部”的概念,是为了解决:
> + First, the system can’t neatly be tested with automated test suites because part of the logic needing to be tested is dependent on oft-changing visual details such as field size and button placement;
+ For the exact same reason, it becomes impossible to shift from a human-driven use of the system to a batch-run system;
+ For still the same reason, it becomes difficult or impossible to allow the program to be driven by another program when that becomes attractive.
总结来说就是:
- 使用自动化测试很困难
- 很难替代用户(操作参与方)
- 很难替代原有框架或逻辑
内部和外部在我一开始理解六边形架构的时候令我有些困惑。
需要提前说明的是,这篇文章讨论的六边形架构的边界是一个可独立部署的Web应用,例如在Java中可以是一个SpringBoot的Jar包。
**而六边形架构基于上述前提,将一个可独立部署的Web应用分成了内部和外部。内部被称为应用层(Application)**
他们同属于一个Jar包内,但是将这个应用分为了内部和外部。
**内部,则是应用本身具体的业务逻辑,我们希望内部不去依赖外部的实现。**这样当我们想要新增或者替换外部的基础设施时,不需要对内部的业务逻辑进行变更。这样使得修改或替换和业务无关的基础设施变得十分容易。
**外部,则是数据入口或者出口的适配器及相关的基础设施。**我们可以在外部对这些结构轻松地修改和替换,却不影响内部的核心业务逻辑。
## 2)包详解
基于上述的示意图,我将六边形架构的包结构进行了如下的划分
```json
├── HexArchApplication.java // Spring Boot Application Runner
├── adapter // **适配器层** 处于Application层的外面,用来适配不同的出入端口
│ ├── inbound // **“主适配器”**
│ │ ├── rest // 适配器1:RESTFul适配器(SpringMVC Controller等)
│ │ │ └── HexArchController.java
│ │ └── rpc // 适配器2:RPC适配器(Dubbo等)
│ │ └── RpcController.java
│ └── outbound // **“从适配器”**
│ ├── gateway // 适配器3:其他服务的Gatway出口适配器
│ └── persistance // 适配器4:数据持久化(数据库等)相关的适配器
│ └── hexarch
│ ├── HexArchJpaRepository.java // SpringData预定义接口
│ ├── HexArchPO.java // 数据库实体
│ └── HexArchRepositoryImpl.java // HexArchRepository interface的实现类
└── application // **应用层**
├── domain // **DDD中的领域层**
│ ├── core
│ │ ├── Aggregate.java
│ │ ├── DomainService.java
│ │ ├── Entity.java
│ │ ├── Gateway.java
│ │ └── Repository.java
│ └── hexarch
│ ├── HexArchRepository.java // interface,通过控制反转实现细节依赖抽象
│ └── HexArchService.java
└── usecase // **用例**
└── HexArchUseCase.java
```
## 3)代码详解
结合上面的包结构来看哦~
### HexArchController
主适配器,是一个RESTFul的适配器,在Spring应用中非常常见。
依赖链:HexArchController → HexArchUseCase,向内部依赖
我自己有想过UseCase的依赖需不需要通过接口进行解耦,思考了一下觉得没有必要。因为适配变更本来就是适配器的职责。
```java
@RestController
@RequiredArgsConstructor
public class HexArchController {
private final HexArchUseCase hexArchUseCase;
@GetMapping("/hello-hex-arch")
private String hello() {
return hexArchUseCase.helloHexArch();
}
@PostMapping("/values/{value}")
private void saveValues(@PathVariable String value) {
hexArchUseCase.saveValue(value);
}
}
```
### HexArchUseCase
用例,是Application中业务逻辑的核心类。
依赖链:HexArchUseCase → HexArchService,HexArchUseCase → HexArchRepository
注意:HexArchRepository是一个接口,定义在Application层的Domain层内。通过控制反转,使得细节依赖抽象,实现与从适配器解耦。
```java
@Component
@RequiredArgsConstructor
public class HexArchUseCase {
private final HexArchRepository hexArchRepository;
private final HexArchService hexArchService;
public String helloHexArch() {
return hexArchJpaRepository.getHello();
}
public void saveValue(String value) {
hexArchService.saveValue(value);
}
}
```
### HexArchService
领域服务,和DDD相关的概念有关,这里不展开介绍。同样是定义在Application层内。
依赖链:HexArchService → HexArchRepository
```java
@Service
@RequiredArgsConstructor
public class HexArchService implements DomainService {
private final HexArchRepository hexArchRepository;
public void saveValue(String value) {
hexArchRepository.saveValue(value);
}
}
```
### HexArchRepository
持久层接口,他仅仅是一个接口,没有任何实现细节。他的实现类 `HexArchRepositoryImpl` 定义在适配器层中。
```java
public interface HexArchRepository extends Repository {
String getHello();
void saveValue(String value);
}
```
### HexArchRepositoryImpl
从适配器, `HexArchRepository` 的实现。
这里就引入了持久化相关的具体类,SpringData的预定义接口 `HexArchJpaRepository`
```java
@Component
@RequiredArgsConstructor
public class HexArchRepositoryImpl implements HexArchRepository {
private final HexArchJpaRepository hexArchJpaRepository;
@Override
public String getHello() {
return hexArchJpaRepository.findById("hello").orElseThrow(RuntimeException::new).getValue();
}
@Override
public void saveValue(String value) {
HexArchPO hexArchPO = HexArchPO.builder().id(value).value(value).build();
hexArchJpaRepository.save(hexArchPO);
}
}
```
## 4)ArchUnit
[Unit test your Java architecture](https://www.archunit.org/)
Arch Unit是一个Java架构检查工具。可以检查引用、循环调用等问题。
基于六边形架构,可以定义如下的检查规则:
```java
@AnalyzeClasses(packages = "com.csc.hexarch", importOptions = ImportOption.DoNotIncludeTests.class)
class LayerArchTest {
@ArchTest
static ArchRule layer_dependencies_are_respected = layeredArchitecture()
.layer("Inbound").definedBy("com.csc.hexarch.adapter.inbound..")
.layer("Application").definedBy("com.csc.hexarch.application..")
.layer("Domain").definedBy("com.csc.hexarch.application.domain..")
.layer("Outbound").definedBy("com.csc.hexarch.adapter.outbound..")
.whereLayer("Inbound").mayNotBeAccessedByAnyLayer()
.whereLayer("Outbound").mayNotBeAccessedByAnyLayer()
.whereLayer("Application").mayOnlyBeAccessedByLayers("Inbound", "Outbound")
.whereLayer("Domain").mayOnlyBeAccessedByLayers("Application", "Outbound", "Inbound");
}
```
# 0xFF 结语
我相信六边形架构在Java上肯定会有多种实现方式,本篇文章仅仅是根据我的个人理解和经验所构建的一个示例。
六边形架构所带来的好处需要实践一点一点去体会。我在刚开始接触的时候对六边形架构的体会不深,仅仅是因为项目上在遵循这个架构规范而去对他进行实践。
刚开始我是站在巨人的肩膀上,没有办法体会存在多个外部系统时所带来的的切换、变更和维护成本。但是等我深入项目,集成的外部系统慢慢变多之后,自然而然对六边形的架构理解就变深刻了。
虽然六边形架构本身不复杂,但是在不断的开发中去遵循他并且和其他的开发范式比如DDD落地、TDD相结合的时候还是颇有难度的。
慢慢地回看自己写的代码,其实有很多写的不好的地方,最近也在慢慢对其进行重构。
希望在未来自己能够对六边形架构有着更加独特的见解吧~
【小记】六边形架构 Java示例