反模式警示

本文档列举 rust-dix 开发中的常见反模式及其修正方案。

> 本文示例采用简短路径风格,假定文件顶部已 `use rust_dix::*;`。导入约定详见 [用法指南](usage-guide.md#1-导入约定路径简化)。

反模式 1:依赖具体类型

❌ 错误做法

// Handler 依赖具体实现类型
pub struct CreateUserHandler {
    user_svc: Arc<UserService>,  // 暴露具体类型
    logger: Arc<FileLogger>,      // 暴露具体类型
}

**问题**: - 无法在测试中替换 mock - 替换实现需修改 Handler 代码 - 违反依赖倒置原则

✅ 正确做法

// Handler 依赖接口:struct 用 #[derive(Inject)] 仅生成构造函数,impl 用 #[inject] 注册为 trait
#[derive(Inject)]
pub struct CreateUserHandler {
    user_svc: Arc<dyn IUserService>,  // 接口
    logger: Arc<dyn ILogger>,         // 接口
}

#[inject]
impl IRequestHandler<CreateUserRequest, User> for CreateUserHandler { /* ... */ }

---

反模式 2:注册为具体类型

❌ 错误做法

ServiceCollection::new()
    .singleton(|_| Arc::new(UserService))  // 注册为 UserService
    .singleton(|_| Arc::new(FileLogger))    // 注册为 FileLogger
    .build()
    .unwrap();

// 无法按 trait 解析
let logger: Arc<dyn ILogger> = provider.get()?;  // Err(ServiceNotFound)

✅ 正确做法

ServiceCollection::new()
    .singleton::<dyn IUserService>(|_| Arc::new(UserService))
    .singleton::<dyn ILogger>(|_| Arc::new(FileLogger))
    .build()
    .unwrap();

let logger: Arc<dyn ILogger> = provider.get();  // OK

---

反模式 3:trait 放在实现模块

❌ 错误做法

// handlers/user.rs
pub trait IUserService: Send + Sync {  // trait 定义在实现模块
    fn create(&self, name: &str) -> Result<User, String>;
}

pub struct UserService;
impl IUserService for UserService { ... }

**问题**: - 接口暴露实现细节 - contracts 层无法引用接口 - 分层边界模糊

✅ 正确做法

// contracts/user.rs
pub trait IUserService: Send + Sync {
    fn create(&self, name: &str) -> Result<User, String>;
}

// handlers/user.rs
use contracts::user::IUserService;

pub struct UserService;
impl IUserService for UserService { ... }

---

反模式 4:main.rs 逐个注册

❌ 错误做法

// main.rs
fn main() {
    let provider = ServiceCollection::new()
        .singleton::<dyn IUserService>(|_| Arc::new(UserService))
        .singleton::<dyn ILogger>(|_| Arc::new(FileLogger))
        .singleton::<dyn IDbContext>(|_| Arc::new(DbContext))
        // ... 逐个注册所有服务
        .build()
        .unwrap();
}

**问题**: - 新增服务需修改 main.rs - 组合根臃肿难维护 - 编译期无法自动发现

✅ 正确做法

// handlers/user.rs
// struct 用 #[derive(Inject)] 仅生成构造函数(不注册),impl 用 #[inject] 注册为 dyn IUserService
#[derive(Inject)]
pub struct UserService { ... }

#[inject]
impl IUserService for UserService { /* ... */ }

// main.rs
fn main() {
    let provider = Arc::new(
        ServiceCollection::from_injected()
            .singleton(|_| Arc::new(AppPaths::resolve()))  // 只注册基础设施
            .build()
            .unwrap()
    );
}

---

反模式 5:传递 Provider 到处使用

❌ 错误做法

// 业务代码直接使用 Provider
pub struct OrderService {
    provider: Arc<ServiceProvider>,  // Provider 作为字段
}

impl OrderService {
    fn process(&self, order: Order) {
        let user_svc = self.provider.get::<dyn IUserService>();  // 服务定位器模式
        let pay_svc = self.provider.get::<dyn IPayGateway>();
        // ...
    }
}

**问题**: - 依赖不透明(隐藏了真正依赖什么) - 运行时才发现依赖缺失 - 违反显式依赖原则

✅ 正确做法

// 显式声明依赖
#[inject(singleton)]
pub struct OrderService {
    user_svc: Arc<dyn IUserService>,  // 显式依赖
    pay_svc: Arc<dyn IPayGateway>,    // 显式依赖
}

impl OrderService {
    fn process(&self, order: Order) {
        self.user_svc.check_user(order.user_id);
        self.pay_svc.pay(order.amount);
    }
}

---

反模式 6:Singleton 依赖 Scoped

❌ 错误做法

// Session 注册为 Scoped
#[inject(scoped)]
pub struct Session { ... }

// GlobalService 注册为 Singleton
#[inject(singleton)]
pub struct GlobalService {
    session: Arc<Session>,  // Singleton 依赖 Scoped
}

**问题**: - 0.4.0 起 build 阶段直接拒绝,返回 `RdiError::SingletonDependsOnScoped` - Singleton 会永久持有 Scoped 实例(captive dependency),破坏 scope 隔离语义 - 间接依赖链(`Singleton → Transient → Scoped`)同样被检测拒绝

✅ 正确做法

// Scoped 服务只依赖 Singleton 或 Scoped
#[inject(scoped)]
pub struct RequestHandler {
    config: Arc<AppConfig>,       // Singleton → OK
    session: Arc<Session>,        // Scoped → OK
}

// Singleton 只依赖 Singleton
#[inject(singleton)]
pub struct GlobalService {
    config: Arc<AppConfig>,       // Singleton → OK
}

---

反模式 7:每次重建容器

❌ 错误做法

// 每次请求重建容器
fn handle_request(req: Request) -> Response {
    let provider = ServiceCollection::new()
        .singleton(|_| Arc::new(UserService))
        .build()
        .unwrap();  // 每次重建
    
    let handler = provider.get::<dyn IRequestHandler>();
    handler.handle(req)
}

**问题**: - 性能开销巨大(构建、初始化) - Singleton 语义丢失(每次重建) - 浪费资源

✅ 正确做法

// 构建一次,全局持有
fn main() {
    let provider = Arc::new(ServiceCollection::from_injected().build().unwrap());
    
    // 请求处理复用容器
    for req in requests {
        let scope = provider.scope();
        let handler = scope.get::<dyn IRequestHandler>();
        handler.handle(req);
    }
}

---

反模式 8:根 Provider 解析 Scoped 期望每请求独立

⚠️ 语义澄清

0.4.0 起,`ServiceProvider` 本身即 root scope。从根 Provider 解析 Scoped 服务时,实例缓存在 `root_scoped_cache` 中,**后续所有根级解析返回同一实例**(等同于应用级单例)。

// Session 注册为 Scoped
ServiceCollection::new()
    .scoped(|_| Arc::new(Session))
    .build()
    .unwrap();

// 从根 Provider 解析 — 缓存在 root_scoped_cache
let session: Arc<Session> = provider.get();
let session2: Arc<Session> = provider.get();
// session 与 session2 是同一实例(根 scope 缓存)

**反模式**:期望根 Provider 每次 `get::<Scoped>()` 返回不同实例(误以为是 Transient 语义)。 **问题**:根 scope 缓存永远不会释放,请求级数据被永久持有,无法实现"每请求一个 Session"。

✅ 正确做法

需要请求级隔离时,必须从子 `Scope` 解析:

// 从子 Scope 解析 Scoped 服务
let scope = provider.scope();
let session1: Arc<Session> = scope.get();
let session2: Arc<Session> = scope.get();
// session1 与 session2 是同一实例(子 Scope 内缓存)

let scope2 = provider.create_scope();
let session3: Arc<Session> = scope2.get();
// session3 与 session1 不同(跨 Scope 独立)

---

反模式 9:Handler 内直接调用

❌ 错误做法

// Handler 直接调用其他 Handler
impl IRequestHandler<CreateOrderRequest, Order> for CreateOrderHandler {
    fn handle(&self, req: CreateOrderRequest) -> Result<Order, String> {
        // 直接调用其他 Handler
        let user_handler = CreateUserHandler { ... };
        user_handler.handle(CreateUserRequest { ... });  // 绕过 DI
    }
}

**问题**: - 绕过 DI 容器 - 无法注入依赖 - 无法替换 mock

✅ 正确做法

// Handler 通过 Mediator 或 DI 调用
impl IRequestHandler<CreateOrderRequest, Order> for CreateOrderHandler {
    fn handle(&self, req: CreateOrderRequest) -> Result<Order, String> {
        // 通过注入的 Mediator 发送请求
        self.mediator.send(CreateUserRequest { ... })?;
        // 或注入依赖的服务
        self.user_svc.create(...)?;
    }
}

---

反模式 10:struct 与 impl 同时用 `#[inject]`(双重注册)

❌ 错误做法

// struct 和 impl 都用 #[inject] → 容器里既有 UserHandler(具体类型)又有 dyn IUserHandler
#[inject(transient)]
struct UserHandler {
    #[inject]
    user_svc: Arc<dyn IUserService>,
}

#[inject]
impl IUserHandler for UserHandler { /* ... */ }  // 消费者可 get::<UserHandler>() 绕过接口

**问题**: - 同一 struct 被注册两次(具体类型 + `dyn Trait`),通常非预期 - 消费者能绕过 trait 接口直接 `get::<具体类型>()`,破坏面向接口原则 - 破坏可 mock 性——测试中替换 `dyn IUserHandler` 后,`get::<UserHandler>()` 仍返回真实实现

✅ 正确做法

`#[inject]` 放置位置二选一:面向接口时 struct 上用 `#[derive(Inject)]`(仅生成构造函数、不注册),impl 上用 `#[inject]` 注册为 `dyn Trait`。

// Handler 推荐放 impl 上:struct 用 #[derive(Inject)] 仅生成构造函数,impl 用 #[inject] 注册为 trait
#[derive(Inject)]
struct UserHandler {
    #[inject]
    user_svc: Arc<dyn IUserService>,
}

#[inject]  // 仅注册为 dyn IUserHandler
impl IUserHandler for UserHandler {
    fn handle(&self, req: Req) -> Resp { /* ... */ }
}

| 目标身份 | struct 上 | impl 上 | 结果 | |---------|----------|---------|------| | 仅具体类型(无 trait,如配置/值对象) | `#[inject]` | *不放* | ✅ | | 仅 `dyn Trait`(面向接口,**Handler/Service/Repository 推荐**) | `#[derive(Inject)]` | `#[inject]` | ✅ | | 两者都要(罕见,确需同时暴露) | `#[inject]` | `#[inject]` | ⚠️ 需注释说明意图 | | 仅 impl、struct 无构造函数 | *不放* | `#[inject]` | ❌ 编译失败 |

**何时放 struct 上**:服务作为具体类型对外暴露(配置、值对象,无 trait 契约)。 **何时放 impl 上**:服务有 trait 契约,面向接口便于 mock(Handler / Service / Repository,**推荐**)。

为什么宏无法编译时检测双重注册?

双重注册只能靠门禁 A9/B6 在代码生成阶段拦截,过程宏本身**无法**编译时检测,原因有三:

1. **过程宏跨调用无状态**:`#[inject]` 标在 struct 上和标在 impl 上是两次独立的宏调用,彼此看不见对方,Rust 过程宏按设计即无共享状态。 2. **TypeId 不冲突**:struct 放置注册为 `TypeId::of::<T>()`(具体类型),impl 放置注册为 `TypeId::of::<dyn Trait>()`(trait 对象)—— 两者 TypeId 不同,在 `ServiceStore` 中落入不同桶,无运行时冲突可检测。 3. **双重注册是被支持的特性**:测试 `inject_combined_concrete_and_trait` 明确验证同一 struct 同时注册为具体类型 + `dyn Trait` 两者均可解析("Concrete is also usable as the trait")。加检测会破坏此合法用例(罕见但确需同时暴露)。

因此,双重注册的防范完全依赖**文档约束 + 门禁 A9/B6**,而非编译期检查。

> 详见 [用法指南 · 放置位置决策](usage-guide.md#放置位置struct-还是-impl二选一避免双重注册) 与 [常见陷阱 · 陷阱 10](common-pitfalls.md#陷阱-10struct-与-impl-同时用-inject双重注册)。

---

反模式速查表

| 反模式 | 问题 | 正确做法 | |--------|------|---------| | 依赖具体类型 | 无法替换 mock | `Arc<dyn Trait>` | | 注册为具体类型 | 无法按 trait 解析 | `singleton::<dyn Trait>(...)` | | trait 在实现模块 | 分层边界模糊 | contracts 层放 trait | | main.rs 逐个注册 | 组合根臃肿 | `from_injected()` | | 传递 Provider | 依赖不透明 | 显式依赖字段 | | Singleton 依赖 Scoped | 生命周期混乱 | Singleton 只依赖 Singleton | | 每次重建容器 | 性能开销 | 构建一次 Arc 持有 | | 根 Provider 解析 Scoped | Scope 语义丢失 | 从 Scope 解析 | | Handler 直接调用 | 绕过 DI | 通过 Mediator 或注入服务 | | struct 与 impl 同时用 `#[inject]` | 双重注册,可绕过接口 | struct 用 `#[derive(Inject)]`,impl 用 `#[inject]` |

---

下一节:[性能优化建议](performance-tips.md)