精读-端口和适配器架构(六边形架构)端口和适配器架构(又名六边形架构)由 Alistair Cockburn 提出,旨在
原文:herbertograca.com/2017/09/14/…
端口和适配器架构(又名六边形架构)由 Alistair Cockburn 提出,并于 2005 年写在他的博客上。
其大概意思我们要设计一个应用程序,使得它能够被不同的驱动方式(如用户输入、其他程序、自动化测试或者批处理脚本)同样有效地驱动。而且,这个应用程序应该能够在不依赖最终运行环境(如实际使用的外部服务和数据库)的情况下进行开发和测试。
这样做带来以下几个好处:
- 隔离业务逻辑。避免了传统的分层架构中,业务逻辑与输入/输出(列入用户接口、数据库访问、外部服务调用)耦合
- 提高可测试性。业务逻辑依赖接口,而不是具体的实现,可轻松的替换外部实现,使单元测试、集成测试更加便捷
- 增强可维护性和可拓展性。在一些复杂的系统中,需求和外部系统可能会频繁变动。业务逻辑依赖接口,而不是具体的实现。使得业务逻辑可以在不修改的情况下适应外部系统的改变。
端口/适配器架构,从分层架构演变而来
到 2005 年,通过 EBI 和 DDD,我们明确了系统中最重要的部分是内层,内层是所有业务逻辑存在的地方,是我们与竞争对手真正的区别。
Alistair Cockburn 也意识到,从另一方面来看,顶层和底层只是应用程序的入口/出口。虽然他们不同,但他们的目标是相似的,且在设计上具有对称性。如果我们想隔离应用程序内层,可以通过这些入口/出口点实现类似的效果。
以左右视图来展示:
可以看到每一次都有多个入口/出口点。例如 API 和 UI 是左侧的两个入口点,而 ORM 和搜索引擎是右侧的两个出口点。
最终我们使用具有多个侧面的应用程序图,最终选择了六边形,所以该架构又名六边形架构。
什么是端口(What is Port)
“端口”就是应用程序与外界沟通的通用接口。
这个接口可以让任意消费者(如用户、其他程序等)通过统一的方式与应用程序交互,而不必知道具体实现细节。我们只需定义好接口,具体的实现可以在需要的时候动态注入。这样做的好处是可以使代码更灵活,便于替换不同的实现而不影响整体结构。
比如举个例子:假设我们在开发一个应用程序,需要通过搜素引擎查询一些数据,你可以定义一个名为SearchEngine的接口,这个接口里面有一个search方法:
public interface SearchEngine {
SearchResult search(String query);
}
然后,在应用程序的某处使用这个接口:
public class SearchService {
private SearchEngine searchEngine;
public SearchService(SearchEngine searchEngine) {
this.searchEngine = searchEngine;
}
public SearchResult performSearch(String query) {
return searchEngine.search(query);
}
}
在这个例子中,SearchEngine就是一个端口。你并不关心使用的是哪个具体的搜索引擎,比如Google还是Bing,只要它实现了SearchEngine接口,你就可以使用它。这使得你的代码非常灵活,可以方便地替换不同的搜索引擎实现,而不用修改SearchService类。具体的搜索引擎实现会在运行时被注入:
SearchEngine googleSearchEngine = new GoogleSearchEngine();
SearchService searchService = new SearchService(googleSearchEngine);
什么是适配器(What is Adapter)?
适配器是将一个接口转换(适配)为另一个接口的类。
例如,适配器实现接口 A 并注入接口 B。当适配器实例化时,它会在其构造函数中注入实现接口 B 的对象。然后,在需要接口 A 的任何地方注入该适配器,并接收它的方法请求。转换并代理实现接口 B 的内部对象。
两种不同类型的适配器
在六边形架构(Hexagonal Architecture)中,根据角色的位置和职责,适配器分为两类:
- 主驱动适配器(Primary or Driving Adapters) :
- 位置:在架构的左侧,通常代表用户界面(UI)。
- 职责:这些适配器负责启动应用程序中的某些操作。例如,用户点击按钮或提交表单时,主驱动适配器会被触发,进而调用业务逻辑。
- 依赖关系:主驱动适配器依赖于端口,并注入端口的具体实现(用例)。在这一侧,端口及其具体实现(用例)都在应用程序内部。
- 被驱动适配器(Secondary or Driven Adapters) :
- 位置:在架构的右侧,通常代表连接后台工具(如数据库、外部服务等)。
- 职责:这些适配器对主驱动适配器的操作作出反应,例如,存储数据或调用外部API。
- 依赖关系:被驱动适配器是端口的具体实现,并被注入业务逻辑中。业务逻辑只知道端口接口,而具体实现则包裹在一些外部工具中并位于应用程序外部。
具体例子:让我们结合上面的解释,通过一个具体的例子进一步理解。
假设我们有一个TODO应用,用户可以添加任务,任务数据需要存储在外部数据库中。
// 应用程序用例端口
public interface AddTaskUseCase {
void addTask(String taskName);
}
// 存储端口
public interface TaskRepository {
void saveTask(String taskName);
}
// 应用程序用例实现
public class AddTaskUseCaseImpl implements AddTaskUseCase {
private TaskRepository taskRepository;
public AddTaskUseCaseImpl(TaskRepository taskRepository) {
this.taskRepository = taskRepository;
}
@Override
public void addTask(String taskName) {
// 执行业务逻辑
taskRepository.saveTask(taskName);
}
}
// 输入适配器,UI适配器,实际的业务中就是 api
public class TaskController {
private AddTaskUseCase addTaskUseCase;
public TaskController(AddTaskUseCase addTaskUseCase) {
this.addTaskUseCase = addTaskUseCase;
}
public void handleAddTaskRequest(String taskName) {
addTaskUseCase.addTask(taskName);
}
}
// 被驱动适配器 输出适配器 存储实现
public class DatabaseTaskRepository implements TaskRepository {
@Override
public void saveTask(String taskName) {
// 实现存储任务到数据库的逻辑
System.out.println("Saving task: " + taskName + " to the database.");
}
}
public class Main {
public static void main(String[] args) {
// 创建被驱动适配器
TaskRepository taskRepository = new DatabaseTaskRepository();
// 注入被驱动适配器及创建用例实现
AddTaskUseCase addTaskUseCase = new AddTaskUseCaseImpl(taskRepository);
// 创建主驱动适配器并注入用例
TaskController taskController = new TaskController(addTaskUseCase);
// 处理请求
taskController.handleAddTaskRequest("Learn Hexagonal Architecture");
}
}
转载自:https://juejin.cn/post/7426258025483386943