Перейти к основному содержимому
ИнженерияMar 28, 2026

Deep EVM #22: Dependency Injection в Rust — ServiceLocator, Arc и трейт-объекты

OS
Open Soft Team

Engineering Team

DI в Rust: другой подход

Dependency Injection (DI) в Rust выглядит иначе, чем в Java или C#. Здесь нет рефлексии, нет аннотаций @Inject, нет DI-контейнеров в привычном смысле. Вместо этого Rust предлагает мощную систему трейтов, дженериков и lifetime’ов, которая позволяет реализовать DI на уровне типов — с нулевыми runtime-затратами.

В этой статье мы рассмотрим четыре подхода к DI в Rust: трейт-объекты, дженерики, паттерн ServiceLocator и compile-time DI.

Подход 1: Трейт-объекты и Arc

Самый распространённый подход — определить трейт для каждой зависимости и передавать Arc<dyn Trait>:

use std::sync::Arc;

#[async_trait::async_trait]
trait UserRepository: Send + Sync {
    async fn find_by_id(&self, id: Uuid) -> Result<User>;
    async fn save(&self, user: &User) -> Result<()>;
}

#[async_trait::async_trait]
trait EmailService: Send + Sync {
    async fn send(&self, to: &str, subject: &str, body: &str) -> Result<()>;
}

struct UserService {
    repo: Arc<dyn UserRepository>,
    email: Arc<dyn EmailService>,
}

impl UserService {
    fn new(
        repo: Arc<dyn UserRepository>,
        email: Arc<dyn EmailService>,
    ) -> Self {
        Self { repo, email }
    }

    async fn register(&self, name: String, email_addr: String) -> Result<User> {
        let user = User::new(name, email_addr.clone());
        self.repo.save(&user).await?;
        self.email.send(&email_addr, "Welcome", "Hello!").await?;
        Ok(user)
    }
}

Плюсы: простота, привычно для разработчиков из других языков. Минусы: динамическая диспетчеризация (vtable), аллокация через Arc.

Подход 2: Дженерики (compile-time DI)

Для максимальной производительности используйте дженерики — компилятор мономорфизирует код:

struct UserService<R, E>
where
    R: UserRepository,
    E: EmailService,
{
    repo: R,
    email: E,
}

impl<R: UserRepository, E: EmailService> UserService<R, E> {
    fn new(repo: R, email: E) -> Self {
        Self { repo, email }
    }
}

Плюсы: нулевые runtime-затраты, инлайнинг. Минусы: сложные типы, дублирование кода при мономорфизации.

Подход 3: Паттерн ServiceLocator

ServiceLocator — это контейнер зависимостей, из которого компоненты извлекают нужные сервисы:

use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::sync::Arc;

struct ServiceLocator {
    services: HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
}

impl ServiceLocator {
    fn new() -> Self {
        Self { services: HashMap::new() }
    }

    fn register<T: Send + Sync + 'static>(&mut self, service: T) {
        self.services.insert(
            TypeId::of::<T>(),
            Arc::new(service),
        );
    }

    fn resolve<T: Send + Sync + 'static>(&self) -> Option<Arc<T>> {
        self.services
            .get(&TypeId::of::<T>())
            .and_then(|s| s.clone().downcast::<T>().ok())
    }
}

Использование:

let mut locator = ServiceLocator::new();
locator.register(PgUserRepository::new(pool.clone()));
locator.register(SmtpEmailService::new(config));

let repo: Arc<PgUserRepository> = locator.resolve().unwrap();

Плюсы: гибкость, динамическая регистрация. Минусы: нет compile-time проверки, downcast может упасть.

Подход 4: AppState (идиома Axum)

В Axum идиоматический подход — создать структуру AppState со всеми зависимостями:

#[derive(Clone)]
struct AppState {
    db: PgPool,
    redis: RedisPool,
    jwt_secret: String,
    email: Arc<dyn EmailService>,
}

async fn create_user(
    State(state): State<AppState>,
    Json(payload): Json<CreateUserRequest>,
) -> Result<Json<User>, AppError> {
    let user = User::new(payload.name);
    sqlx::query!("INSERT INTO users ...")
        .execute(&state.db)
        .await?;
    state.email.send(&user.email, "Welcome", "Hello!").await?;
    Ok(Json(user))
}

Это самый прагматичный подход для веб-приложений на Axum.

Тестирование с DI

Главное преимущество DI — тестируемость. Создавайте mock-реализации трейтов:

#[cfg(test)]
mod tests {
    struct MockUserRepo {
        users: Mutex<Vec<User>>,
    }

    #[async_trait]
    impl UserRepository for MockUserRepo {
        async fn find_by_id(&self, id: Uuid) -> Result<User> {
            self.users.lock().unwrap()
                .iter()
                .find(|u| u.id == id)
                .cloned()
                .ok_or(Error::NotFound)
        }

        async fn save(&self, user: &User) -> Result<()> {
            self.users.lock().unwrap().push(user.clone());
            Ok(())
        }
    }

    #[tokio::test]
    async fn test_register_user() {
        let repo = Arc::new(MockUserRepo::default());
        let email = Arc::new(MockEmailService::default());
        let service = UserService::new(repo.clone(), email);

        let user = service.register("Alice".into(), "a@b.com".into())
            .await.unwrap();

        assert_eq!(user.name, "Alice");
    }
}

Выбор подхода

ПодходRuntime-затратыТестируемостьСложность
Трейт-объектыНизкие (vtable)ВысокаяСредняя
ДженерикиНулевыеВысокаяВысокая
ServiceLocatorСредниеСредняяНизкая
AppStateНизкиеВысокаяНизкая

Для большинства веб-приложений на Axum рекомендуем AppState + трейт-объекты для сервисов, которые нужно подменять в тестах.

Заключение

DI в Rust — это не фреймворк, а набор паттернов. Система типов Rust обеспечивает безопасность на этапе компиляции, что делает многие DI-фреймворки просто ненужными. Выбирайте подход исходя из требований проекта: для максимальной производительности — дженерики, для гибкости — трейт-объекты, для простоты — AppState.