跳转到正文
zeno's blog
返回

整洁架构(一):概览-依赖方向与层级职责

专题: 整洁架构

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 / 原型 / 一次性脚本
  团队就两三个人、业务简单

分享这篇文章:

上一篇
mio(三):平台后端-epoll、kqueue、IOCP
下一篇
mio(二):核心架构-Poll、Token、Interest、Events