Table of contents
Open Table of contents
核心原则:依赖方向
Clean Architecture 只有一条铁律:依赖只能从外向内,内层不知道外层的存在。
┌──────────────────────────────────────────────────────────┐
│ │
│ 外层 │
│ ┌──────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ ┌──────────────────────────────────┐ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ Domain(领域层) │ │ │ │
│ │ │ │ 实体 + 业务规则 │ │ │ │
│ │ │ │ 无任何外部依赖 │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ └──────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ Application(应用层) │ │ │
│ │ │ 用例编排、定义 Repository trait │ │ │
│ │ │ 依赖 Domain 层 │ │ │
│ │ │ │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Infrastructure(基础设施层) │ │
│ │ 数据库实现、Redis、外部 API 客户端 │ │
│ │ 依赖 Application 层(实现其定义的 trait) │ │
│ │ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ Interface(接口层) │
│ HTTP handler、gRPC handler、CLI │
│ 依赖 Application 层(调用用例) │
│ │
└──────────────────────────────────────────────────────────┘
依赖方向: 外 → 内(永远不反过来)
为什么依赖方向这么重要
如果 Domain 层依赖了 PostgreSQL:
domain/player.rs:
use sqlx::PgPool; // ← Domain 依赖了基础设施
结果:
换数据库 → 改 Domain 层 → 业务逻辑跟着改 → 风险大
单元测试 → 必须启动 PostgreSQL → 慢
Domain 层变成了数据库的附庸
Clean Architecture 的做法:
domain/player.rs:
// 纯 Rust 结构体和方法,不 use 任何外部 crate
application/repository.rs:
trait PlayerRepository { ... } // 只定义接口
infrastructure/postgres.rs:
impl PlayerRepository for PgPlayerRepo { ... } // 实现细节
结果:
换数据库 → 只改 infrastructure → Domain 不动
单元测试 → 用 InMemory 实现替换 → 快
Domain 层是系统的核心,不受外部变化影响
四层详解
Domain 层(最内层)
// 纯业务逻辑,不依赖任何框架或基础设施
// 不 use sqlx、不 use redis、不 use axum、不 use tokio
// 只用标准库和领域内的类型
pub struct Player {
pub id: PlayerId,
pub name: String,
pub level: u32,
pub exp: u64,
pub inventory: Inventory,
pub wallet: Money,
}
#[derive(Debug)]
pub enum DomainError {
InventoryFull,
InsufficientFunds,
ItemNotFound(ItemId),
LevelTooLow { required: u32, current: u32 },
}
impl Player {
pub fn can_afford(&self, price: &Money) -> bool {
self.wallet.amount >= price.amount
}
pub fn add_item(&mut self, item: Item) -> Result<(), DomainError> {
if self.inventory.is_full() {
return Err(DomainError::InventoryFull);
}
self.inventory.items.push(item);
Ok(())
}
}
测试 Domain 层不需要任何外部依赖:
#[test]
fn player_cannot_add_item_when_inventory_full() {
let mut player = Player::new("Bear", 30); // 30 个满格
for i in 0..30 {
player.add_item(Item::new(format!("item_{i}"))).unwrap();
}
assert!(player.add_item(Item::new("one_more")).is_err());
}
Application 层
// 定义用例(use case)和仓储接口
// 依赖 Domain 层,不依赖具体基础设施
use crate::domain::*;
// 仓储接口(由 Infrastructure 层实现)
#[async_trait]
pub trait PlayerRepository: Send + Sync {
async fn find_by_id(&self, id: &PlayerId) -> Result<Option<Player>, AppError>;
async fn save(&self, player: &Player) -> Result<(), AppError>;
}
#[async_trait]
pub trait ItemRepository: Send + Sync {
async fn find_by_id(&self, id: &ItemId) -> Result<Option<Item>, AppError>;
}
// 用例:给玩家添加物品
pub struct AddItemUseCase<P: PlayerRepository, I: ItemRepository> {
player_repo: P,
item_repo: I,
}
impl<P: PlayerRepository, I: ItemRepository> AddItemUseCase<P, I> {
pub async fn execute(
&self,
player_id: PlayerId,
item_id: ItemId,
) -> Result<Player, AppError> {
let mut player = self.player_repo.find_by_id(&player_id)
.await?
.ok_or(AppError::NotFound("player not found".into()))?;
let item = self.item_repo.find_by_id(&item_id)
.await?
.ok_or(AppError::NotFound("item not found".into()))?;
// 调用 Domain 层的业务逻辑
player.add_item(item)
.map_err(|e| AppError::BadRequest(e.to_string()))?;
self.player_repo.save(&player).await?;
Ok(player)
}
}
Infrastructure 层
// 实现 Application 层定义的 trait
// 这里知道具体用的是 PostgreSQL、Redis 等
use crate::application::PlayerRepository;
use crate::domain::*;
pub struct PgPlayerRepository {
pool: PgPool,
}
#[async_trait]
impl PlayerRepository for PgPlayerRepository {
async fn find_by_id(&self, id: &PlayerId) -> Result<Option<Player>, AppError> {
let row = sqlx::query_as::<_, PlayerRow>(
"SELECT * FROM players WHERE id = $1"
)
.bind(id.0)
.fetch_optional(&self.pool)
.await?;
Ok(row.map(|r| r.into_domain())) // DB 行 → 领域对象
}
async fn save(&self, player: &Player) -> Result<(), AppError> {
let row = PlayerRow::from_domain(player); // 领域对象 → DB 行
sqlx::query("UPDATE players SET name=$1, level=$2, ... WHERE id=$3")
.bind(&row.name)
.bind(row.level)
.bind(row.id)
.execute(&self.pool)
.await?;
Ok(())
}
}
// DB 行结构(和 Domain 的 Player 不同——DB 有自己的表结构)
#[derive(sqlx::FromRow)]
struct PlayerRow {
id: Uuid,
name: String,
level: i32,
// ...
}
impl PlayerRow {
fn into_domain(self) -> Player {
Player {
id: PlayerId(self.id),
name: self.name,
level: self.level as u32,
// ...
}
}
}
Interface 层(最外层)
// HTTP/gRPC handler,调用 Application 层的用例
// 只做:解析请求 → 调用用例 → 转换响应
use crate::application::AddItemUseCase;
async fn add_item_handler(
State(state): State<AppState>,
Path(player_id): Path<Uuid>,
Json(body): Json<AddItemRequest>,
) -> Result<Json<PlayerResponse>, AppError> {
let use_case = AddItemUseCase {
player_repo: &state.player_repo,
item_repo: &state.item_repo,
};
let player = use_case.execute(
PlayerId(player_id),
ItemId(body.item_id),
).await?;
Ok(Json(PlayerResponse::from(player))) // Domain → DTO
}
// 请求/响应 DTO(和 Domain 的 Player 不同)
#[derive(Deserialize)]
struct AddItemRequest {
item_id: Uuid,
}
#[derive(Serialize)]
struct PlayerResponse {
id: Uuid,
name: String,
level: u32,
items: Vec<ItemResponse>,
}
依赖反转在 Rust 中的实现
Clean Architecture 的关键技巧是依赖反转(Dependency Inversion)——Application 层定义 trait,Infrastructure 层实现它。
没有依赖反转:
Application → 直接 use PostgreSQL → Application 依赖 Infrastructure
方向: 内 → 外(违反规则)
有依赖反转:
Application → 定义 trait PlayerRepository
Infrastructure → impl PlayerRepository for PgPlayerRepository
方向: 外(Infrastructure) → 内(Application)(正确)
Rust 中用 trait + 泛型或 trait object 实现:
// 泛型方式(编译期确定,零开销)
struct AddItemUseCase<R: PlayerRepository> {
repo: R,
}
// trait object 方式(运行期动态分发,灵活)
struct AddItemUseCase {
repo: Arc<dyn PlayerRepository>,
}
Clean Architecture 的代价
好处:
业务逻辑隔离,可独立测试
换数据库/框架只改外层
代码结构清晰,职责分明
代价:
更多的转换层(Domain ↔ DB Row ↔ DTO,同一个 Player 三个版本)
更多的 trait 和泛型(Repository trait + 实现 + 注入)
简单 CRUD 被迫走完整流程(过度设计)
适用:
业务逻辑复杂、需要长期维护的项目
多人协作、需要清晰边界的项目
不适用:
简单 CRUD / 原型 / 一次性脚本
团队就两三个人、业务简单