面向接口开发清单
本文档提供面向接口开发的规则速查表,供代码审查和开发参考。
> 本文示例采用简短路径风格,假定文件顶部已 `use rust_dix::*;`。导入约定详见 [用法指南](usage-guide.md#1-导入约定路径简化)。
一、分层规则
contracts 层(接口层)
| ✅ 应该 | ❌ 避免 | |--------|--------| | 定义 trait(如 `IUserService`) | 放具体实现 | | 定义 DTO(请求/响应结构体) | 放依赖 `Arc<dyn Trait>` 的字段 | | trait 方法签名简单清晰 | trait 方法依赖具体类型 | | 仅依赖标准库或框架 | 依赖 domain 或 handlers 模块 |
// ✅ contracts/user.rs
pub trait IUserService: Send + Sync {
fn create(&self, req: CreateUserRequest) -> Result<User, String>;
fn find(&self, id: u64) -> Result<User, String>;
}
pub struct CreateUserRequest {
pub name: String,
pub email: String,
}
pub struct User {
pub id: u64,
pub name: String,
}
handlers 层(实现层)
| ✅ 应该 | ❌ 避免 | |--------|--------| | 实现 trait | 定义 trait | | 在 `impl Trait for Type` 上加 `#[inject]` | 直接 `singleton` 注册为具体类型 | | 字段依赖 `Arc<dyn Trait>` | 字段依赖具体实现类型 | | 自动注册到容器 | 在 main.rs 手动逐个注册 | | struct 用 `#[derive(Inject)]` 仅生成构造函数 | struct 与 impl 同时用 `#[inject]`(双重注册) |
// ✅ handlers/user.rs
// struct 用 #[derive(Inject)] 仅生成构造函数(不注册),impl 用 #[inject] 注册为 dyn IUserService
#[derive(Inject)]
pub struct UserService {
db: Arc<dyn IDbContext>, // 接口依赖
logger: Arc<dyn ILogger>, // 接口依赖
}
#[inject]
impl IUserService for UserService {
fn create(&self, req: CreateUserRequest) -> Result<User, String> {
// ...
}
}
composition_root(组合根)
| ✅ 应该 | ❌ 避免 | |--------|--------| | 注册基础设施(配置、DbContext) | 注册业务 Service | | 注册第三方库适配器 | 注册 Handler | | 调用 `from_injected()` | 手动注册所有服务 | | 一次性构建 Provider | 多次重建容器 |
// ✅ composition_root/bootstrap.rs
pub fn configure(mut svc: ServiceCollection) -> ServiceCollection {
let paths = AppPaths::resolve();
svc = svc.singleton(move |_| Arc::new(paths));
svc = svc.singleton(move |_| Arc::new(DbContext::new()));
svc
}
// main.rs
let provider = Arc::new(
ServiceCollection::from_injected()
.singleton(|_| Arc::new(AppPaths::resolve()))
.build()
.unwrap()
);
二、注册规则
注册类型 = 解析类型
| 规则 | 正确示例 | 错误示例 | |------|---------|---------| | 注册为 trait,解析为 trait | `.singleton::<dyn ILogger>(...)` + `.get::<dyn ILogger>()` | `.singleton(|_| Arc::new(FileLogger))` + `.get::<dyn ILogger>()` |
// ✅ 正确
ServiceCollection::new()
.singleton::<dyn ILogger>(|_| Arc::new(FileLogger))
.build()
.unwrap();
let logger: Arc<dyn ILogger> = provider.get(); // OK
// ❌ 错误
ServiceCollection::new()
.singleton(|_| Arc::new(FileLogger)) // 注册为 FileLogger
.build()
.unwrap();
let logger: Arc<dyn ILogger> = provider.get()?; // Err: TypeId 不匹配
多实现用 keyed
| 场景 | 注册方式 | |------|---------| | 默认策略 | `.singleton::<dyn Trait>(...)` | | 替代策略 | `.keyed_singleton::<dyn Trait>("name", ...)` | | 批量获取 | `.get_all::<dyn Trait>()` |
// ✅ 多实现注册
ServiceCollection::new()
.singleton::<dyn IPayGateway>(|_| Arc::new(WechatPay)) // 默认
.keyed_singleton::<dyn IPayGateway>("wechat", |_| Arc::new(WechatPay))
.keyed_singleton::<dyn IPayGateway>("alipay", |_| Arc::new(Alipay))
.build()
.unwrap();
多接口注册
一个实现注册为多个 trait:
// ✅ 一个实现暴露多个接口:struct 用 #[derive(Inject)] 仅生成构造函数(不注册),两个 impl 块各注册一个 trait
#[derive(Inject)]
pub struct FileStream {
path: String,
}
#[inject]
impl IReader for FileStream { ... }
#[inject]
impl IWriter for FileStream { ... }
// 分别解析
let reader: Arc<dyn IReader> = provider.get();
let writer: Arc<dyn IWriter> = provider.get();
三、依赖规则
Handler 依赖接口
| ✅ 应该 | ❌ 避免 | |--------|--------| | `Arc<dyn IUserService>` | `Arc<UserService>` | | `Arc<dyn ILogger>` | `Arc<FileLogger>` | | `Option<Arc<dyn IService>>` | `Option<Arc<ServiceImpl>>` |
// ✅ 正确:Handler 推荐放 impl 上——struct 用 #[derive(Inject)] 仅生成构造函数,impl 用 #[inject] 注册为 trait
#[derive(Inject)]
pub struct CreateUserHandler {
#[inject]
user_svc: Arc<dyn IUserService>, // 接口
#[inject]
logger: Option<Arc<dyn ILogger>>, // 可选接口
}
#[inject]
impl IRequestHandler<CreateUserRequest, User> for CreateUserHandler { /* ... */ }
// ❌ 错误
pub struct CreateUserHandler {
user_svc: Arc<UserService>, // 具体类型
}
生命周期依赖方向
| 外层 | 可依赖 | 不可依赖 | |------|--------|---------| | Singleton | Singleton | Scoped / Transient | | Scoped | Singleton / Scoped | Transient | | Transient | 所有 | 无限制 |
// ❌ 错误:Singleton 依赖 Scoped
#[inject(singleton)]
pub struct GlobalService {
session: Arc<Session>, // Session 注册为 Scoped → build 阶段拒绝
}
// ✅ 正确:Scoped 依赖 Singleton
#[inject(scoped)]
pub struct RequestHandler {
config: Arc<AppConfig>, // Singleton → OK
}
四、容器使用规则
构建一次,全局持有
| ✅ 应该 | ❌ 避免 | |--------|--------| | main.rs 构建一次 | 每次请求重建 | | `Arc::new(provider)` 传递 | 传递 ServiceCollection | | 需要时解析服务 | 传递 Provider 到处使用 |
// ✅ 正确
fn main() {
let provider = Arc::new(ServiceCollection::from_injected().build().unwrap());
let handler: Arc<dyn IRequestHandler> = provider.get();
handler.handle(req);
}
// ❌ 错误
fn handle(req: Request, provider: &ServiceProvider) {
let svc = provider.get::<dyn IService>(); // Provider 传递过多
}
作用域正确使用
| ✅ 应该 | ❌ 避免 | |--------|--------| | 请求开始创建子 Scope | 期望根 Provider 每次 `get::<Scoped>()` 返回新实例 | | 子 Scope 内缓存 Scoped 服务 | 在根 Provider 上解析 Scoped 期望请求级隔离 |
// ✅ 正确:Web 请求处理
fn handle_request(provider: &Arc<ServiceProvider>, req: Request) {
let scope = provider.scope();
let uow: Arc<UnitOfWork> = scope.get().unwrap(); // Scope 内共享
let handler: Arc<dyn IRequestHandler> = scope.get().unwrap();
handler.handle(&uow, req);
}
// ⚠️ 注意:根 Provider 即 root scope,根级解析 Scoped 会缓存到 root_scoped_cache
let provider = ServiceCollection::new()
.scoped(|_| Arc::new(Session))
.build()
.unwrap();
let session: Arc<Session> = provider.get().unwrap(); // 缓存在 root_scoped_cache
let session2: Arc<Session> = provider.get().unwrap(); // session == session2(根 scope 单例语义)
// 根级 Scoped 不会随请求变化,需要请求级隔离必须用 scope()
五、测试规则
Mock 替换策略
| ✅ 应该 | ❌ 避免 | |--------|--------| | 注册为 trait 才可替换 | 注册为具体类型无法替换 | | 测试容器只改注册 | 修改业务代码 | | Mock 实现 trait | Mock 继承具体类型 |
// ✅ 正确:测试中替换
#[cfg(test)]
fn test_provider() -> ServiceProvider {
ServiceCollection::new()
.singleton::<dyn IUserService>(|_| Arc::new(MockUserService))
.singleton::<dyn IDbContext>(|_| Arc::new(MockDbContext))
.build()
.unwrap()
}
#[test]
fn test_create_user() {
let p = test_provider();
let handler: Arc<dyn IRequestHandler> = p.get();
// 使用 mock 验证
}
六、代码审查清单
审查代码时检查以下项:
| 类别 | 检查项 | |------|--------| | 分层 | trait 是否在 contracts / 实现是否在 handlers | | 依赖 | 字段是否都是 `Arc<dyn Trait>` | | 注册 | 是否在 `impl Trait for Type` 上加 `#[inject]` | | 放置位置 | struct 与 impl 是否同时用 `#[inject]`(应二选一;面向接口时 struct 用 `#[derive(Inject)]`) | | 多实现 | 是否用 keyed 注册替代实现 | | 生命周期 | Singleton 不依赖 Scoped(build 阶段自动检测) | | 容器 | 是否只构建一次 | | 测试 | 是否可替换 mock |
---
下一节:[常见陷阱与排查](common-pitfalls.md)