Dependency Injection контейнеры в Rust: обзор и реализация на примере «dicy»

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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                    

Рекомендаций