Rust与六边形架构:无妥协的纯净代码

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

Rust与六边形架构:编写不怕修改的代码

\n\n

每位开发者都至少经历过这样的场景:数据库的一个小改动或HTTP框架的更换,演变成了一场噩梦般的重构。业务逻辑与实现细节纠缠在一起,代码变成了“意大利面条”。对于注重安全性和性能的Rust来说,这种混合简直是一种罪过。

\n\n

解决这个问题的方案是六边形架构(也称为端口与适配器)。在本文中,我们将探讨如何利用Rust的类型系统和trait来应用这种模式。您将学习如何将领域逻辑与外部世界隔离,编写可测试的代码,并轻松更换数据库驱动或Web服务器。这是任何希望在Rust上构建复杂、可维护系统的人必备的技能。

\n\n

什么是六边形架构?(非常简短)

\n\n

想象一个六边形。中心是您的业务逻辑(领域)。边缘是“端口”(接口),领域通过它们与外部世界通信。外部是“适配器”(具体实现),它们连接到端口。

\n\n

关键思想:领域完全不知道数据是如何保存的或请求是如何发送的。它只知道端口——带有方法的trait。适配器(例如PostgreSQL仓库或HTTP控制器)实现这些trait。

\n\n

在Rust中,这个概念由于trait(类似于接口)和泛型而完美契合。编译器在构建时而非运行时检查所有连接。

\n\n

Rust项目结构

\n\n

首先,我们创建一个文件夹结构,从视觉上将领域与基础设施分开:

\n\n
src/\n├── domain/          # 纯净的业务逻辑(无任何依赖!)\n│   ├── mod.rs\n│   ├── models.rs    # 实体和值对象\n│   └── services.rs  # 与端口协作的服务\n├── ports/           # 面向外部世界的trait(接口)\n│   ├── mod.rs\n│   └── repositories.rs\n├── adapters/        # 端口的实现(数据库、HTTP等)\n│   ├── mod.rs\n│   ├── postgres_repo.rs\n│   └── in_memory_repo.rs  # 用于测试\n├── main.rs          # 应用程序组装(Wiring)\n└── lib.rs           # 库的公共API
\n\n

步骤1:领域——系统的核心

\n\n

我们从简单的用户模型开始。领域中没有对外部库的任何依赖(即使业务逻辑不需要serde,也不使用)。

\n\n
// src/domain/models.rs\npub struct User {\n    pub id: String,\n    pub name: String,\n    pub email: String,\n}\n\nimpl User {\n    pub fn new(id: String, name: String, email: String) -> Self {\n        // 这里可以进行验证\n        Self { id, name, email }\n    }\n}
\n\n

现在定义领域服务。它将与用户打交道,但不知道用户来自哪里。

\n\n
// src/domain/services.rs\nuse crate::ports::repositories::UserRepository;\n\npub struct UserService {\n    repo: Box<dyn UserRepository>,\n}\n\nimpl UserService {\n    pub fn new(repo: Box<dyn UserRepository>) -> Self {\n        Self { repo }\n    }\n\n    pub fn create_user(&mut self, name: String, email: String) -> Result<User, String> {\n        let id = uuid::Uuid::new_v4().to_string(); // 已经有依赖?是的,但这是细节\n        let user = User::new(id, name, email);\n        self.repo.save(&user)?;\n        Ok(user)\n    }\n\n    pub fn get_user(&self, id: &str) -> Result<Option<User>, String> {\n        self.repo.find_by_id(id)\n    }\n}
\n\n

重要:我们使用Box<dyn UserRepository>进行动态分发。在Rust中,这对于复杂应用程序是正常的。对于简单情况,可以使用泛型。

\n\n

步骤2:端口——外部世界的契约

博客

书籍推荐