Dependency Injection контейнер для Rust: обзор библиотеки dicy
В мире системного программирования Rust славится своей производительностью и безопасностью памяти. Однако при построении крупных приложений разработчики часто сталкиваются с проблемой управления зависимостями. Ручное связывание компонентов ведёт к «жёсткому коду», усложняет тестирование и снижает гибкость. Решение — Dependency Injection (DI) контейнер, автоматизирующий создание и внедрение зависимостей. В этой статье мы рассмотрим библиотеку dicy — один из самых зрелых DI контейнеров для Rust.
Что такое Dependency Injection и зачем он нужен в Rust?
Dependency Injection — это паттерн, при котором объект получает свои зависимости извне, а не создаёт их сам. В Rust это особенно актуально из-за строгой системы типов и владения (ownership). DI контейнер берёт на себя:
- Создание экземпляров сервисов (singleton, transient, scoped).
- Разрешение графа зависимостей (рекурсивное внедрение).
- Управление временем жизни объектов.
- Возможность подмены реализаций при тестировании (mock-объекты).
Библиотека dicy: обзор возможностей
dicy — это легковесный, но мощный DI контейнер, написанный на чистом Rust без unsafe кода. Он поддерживает:
- Автоматическое разрешение зависимостей через конструкторы или фабрики.
- Несколько режимов времени жизни: Singleton, Transient, Scoped (для actix-web, axum и других фреймворков).
- Асинхронные фабрики — создание сервисов с использованием async/await.
- Внедрение через типажи — привязка типажа к конкретной реализации.
- Вложенные контейнеры (для изоляции модулей).
Установка и базовая настройка
Добавьте dicy в Cargo.toml:
[dependencies]
dicy = "0.6"
Пример 1: Простое внедрение через конструктор
Создадим два сервиса: Config (singleton) и Database (transient), который зависит от Config.
use dicy::{Container, Module, Provide, Singleton, Transient};
#[derive(Clone)]
struct Config {
url: String,
}
impl Config {
fn new() -> Self {
Config { url: "postgres://localhost".into() }
}
}
struct Database {
config: Config,
}
impl Database {
fn new(config: Config) -> Self {
Database { config }
}
}
// Определяем модуль с регистрацией
struct AppModule;
impl Module for AppModule {
fn configure(container: &mut Container) {
container
.register::<Config>(Singleton::new(Config::new))
.register::<Database>(Transient::new(Database::new));
}
}
fn main() {
let container = Container::new();
container.install(AppModule);
// Получаем Database с автоматически внедрённым Config
let db: Database = container.resolve().unwrap();
println!("Подключение к БД: {}", db.config.url);
}
Пример 2: Асинхронные фабрики и Scoped-зависимости
Для веб-приложений часто требуется создавать сервисы с областью видимости HTTP-запроса (scoped). dicy поддерживает это:
use dicy::{Container, Module, Scoped, AsyncFactory, Provide};
use async_trait::async_trait;
struct RequestContext {
user_id: u64,
}
#[async_trait]
trait UserService {
async fn get_name(&self) -> String;
}
struct UserServiceImpl {
ctx: RequestContext,
}
#[async_trait]
impl UserService for UserServiceImpl {
async fn get_name(&self) -> String {
format!("User #{}", self.ctx.user_id)
}
}
struct WebModule;
impl Module for WebModule {
fn configure(container: &mut Container) {
container
.register::<RequestContext>(Scoped::new(|| RequestContext { user_id: 42 }))
.register::<dyn UserService>(Scoped::new_async(|| async {
let ctx: RequestContext = container.resolve().await.unwrap();
Box::new(UserServiceImpl { ctx }) as Box<dyn UserService>
}));
}
}
#[tokio::main]
async fn main() {
let container = Container::new();
container.install(WebModule);
// В реальном приложении scope создаётся для каждого запроса
let mut scope = container.create_scope();
let service: Box<dyn UserService> = scope.resolve().await.unwrap();
println!("{}", service.get_name().await); // Вывод: User #42