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\nRust项目结构
\n\n首先,我们创建一个文件夹结构,从视觉上将领域与基础设施分开:
\n\nsrc/\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中,这对于复杂应用程序是正常的。对于简单情况,可以使用泛型。