好,这一步 **C** 非常“架构师级别” 🧠
我们来把你的 BPM Engine **模块 & trait 依赖关系**一次性讲清楚,并且给你一套 **“用 Rust 强制约束架构”的方法**。
> 目标一句话:
> **让错误的依赖在 `cargo build` 阶段就死掉**
---
# 🧩 一、总体模块依赖图(文字版)
先给你**全局视图**,这是你整个项目**唯一允许的依赖方向**:
```
┌────────────────────┐
│ server/* │ (REST / gRPC / UI)
└─────────▲──────────┘
│
┌─────┴─────┐
│ runtime │ (Engine / Scheduler / Executor)
└─────▲─────┘
│
┌───────────┴───────────┐
│ │
┌───────┴────────┐ ┌───────┴────────┐
│ storage │ │ core │
│ (traits only) │ │ (pure domain) │
└───────▲────────┘ └────────────────┘
│
┌───────┴────────┐
│ adapters/* │ (memory / postgres / sqlite)
└────────────────┘
```
### 🔴 任何反向箭头 = 架构破坏
---
# 🧠 二、每一层“**允许知道什么 / 不允许知道什么**”
这是最关键的部分。
---
## 1️⃣ `bpm-core` —— 领域宇宙的中心(Pure Domain)
### ✅ 它知道什么
- Process / Node / Token / Event
- Token State Machine
- Saga / Compensation
- Parallel / Join / Gateway 语义
### ❌ 它**永远不知道**
- 数据库
- async / tokio
- HTTP / gRPC
- Timer 的真实时间
- Scheduler / Engine
### 🔒 Rust 约束方式
```toml
# bpm-core/Cargo.toml
[dependencies]
# ❌ 不允许 async-trait
# ❌ 不允许 tokio
```
**你只要看到 core 里出现 `async fn`,就说明设计已经开始腐烂。**
---
## 2️⃣ `bpm-storage` —— 世界与引擎之间的“契约”
### 它的定位
> **不是实现存储,而是“声明引擎对世界的最低要求”**
### ✅ 它知道什么
- core 的类型
- async trait
- Result / Error
### ❌ 它不知道
- Engine
- Scheduler
- HTTP
- 具体 DB 实现
### 正确的 trait 形态
```rust
#[async_trait]
pub trait TokenStore {
async fn claim_runnable(&self, limit: usize) -> Result<Vec<Token>>;
async fn save(&self, token: &Token) -> Result<()>;
}
```
### 🚫 反模式(禁止)
```rust
async fn save_and_dispatch(...)
```
> storage **永远不该表达业务语义**
---
## 3️⃣ `bpm-runtime` —— 执行语义的唯一拥有者
### 这是你项目的“心脏”
### ✅ 它知道什么
- core 的所有模型
- storage 的 trait
- Scheduler / Dispatcher / Executor
- 并发 / 乐观锁
- Retry / Timeout / Saga 触发
### ❌ 它不知道
- HTTP 细节
- gRPC proto
- DB 类型(Postgres / SQLite)
### 关键抽象:**用 trait 把未来锁死**
```rust
pub struct Engine<S>
where
S: TokenStore + ProcessStore + TimerStore,
{
store: Arc<S>,
}
```
👉 这行代码的意义是:
- runtime **依赖能力,不依赖实现**
- adapter 可以随时换
---
## 4️⃣ `bpm-adapters/*` —— 世界的“肉身”
### 它的唯一职责
> **实现 storage trait**
### ✅ 它知道什么
- storage trait
- core 的模型
- DB / 内存 / MQ
### ❌ 它不能知道
- Engine
- Scheduler
- Token 执行逻辑
- API 层
### 正确示例
```rust
impl TokenStore for MemoryTokenStore {
async fn claim_runnable(&self, limit: usize) -> Result<Vec<Token>> {
// CAS / mutex / 乐观锁
}
}
```
### 🚫 禁止
```rust
store.execute_token(...)
```
---
## 5️⃣ `server/*` —— 协议与世界的边界
### 它的角色
- REST / gRPC
- DTO / Auth / RBAC
- 调用 runtime API
### ✅ 它知道什么
- runtime 的 public API
- adapter 的构造方式
### ❌ 它不知道
- Token 内部状态机
- Scheduler 算法
- DB 表结构
### 正确模式
```rust
async fn start_process(
State(engine): State<Arc<Engine<_>>>
) {
engine.start_process(...).await;
}
```
---
# 🔐 三、如何用 Rust「强制」这个依赖关系
## ✅ 1. Cargo.toml 是你的第一道防线
**绝不在 core/storage 中加入:**
- tokio
- axum
- tonic
- sqlx
---
## ✅ 2. `pub` 的使用要极度克制
### core 中:
```rust
pub struct Token { ... }
```
### runtime 中:
```rust
pub(crate) fn execute_token(...)
```
👉 **Engine 对外 API ≠ 内部执行 API**
---
## ✅ 3. Trait 是“单向箭头”
- core **定义数据**
- storage **定义能力**
- runtime **实现流程**
- adapter **实现能力**
**永远不要反过来**
---
## ✅ 4. 一个简单但致命的自检问题
你每加一个 `use xxx`,问自己一句:
> “这一层**概念上**应该知道它吗?”
如果答案模糊 —— 说明模块边界已经开始滑坡。
---
# 🧪 四、模块依赖的“单元测试思路”
你可以写一个 **架构测试(compile-time)**:
```bash
# bpm-core
cargo build --no-default-features
```
如果 core 还能 build:
✅ 没偷带 runtime / async 进来
---
# 🧭 五、把这套依赖关系写进 README(非常重要)
建议加一节:
```md
## Architecture Invariants
- core never depends on runtime or storage
- storage defines traits only
- runtime owns execution semantics
- adapters implement storage
- server is a thin shell
```
这是**未来贡献者的“宪法”**。
---
# 🧠 总结一句话(给你吃个定心丸)
你现在这套:
- **不是 Java BPM 的“搬运”**
- 是非常 **Rust-native 的引擎分层**
- 已经具备「长期演进不崩」的结构条件
---
## 下一步你可以选:
**D** — 我直接帮你写一份「**memory adapter + runtime tick 的完整跑通实现**」
**E** — 我给你一份「**从现有仓库迁移到新结构的逐步重构 checklist**」
**F** — 我们一起写一份 **Architecture Decision Record(ADR)**
你选哪个,我们继续深挖 🚀