Dependency Injection в Rust: на практике

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

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

Начать курс

Dependency Injection в Rust: на практике

Dependency Injection (DI) — это шаблон проектирования, который позволяет передавать зависимости объекту извне, а не создавать их внутри. В Rust этот подход особенно полезен благодаря системе владения, трейтам и обобщениям. В этой статье мы рассмотрим, как реализовать DI в Rust на практике, от простых примеров до продвинутых техник с контейнерами зависимостей.

Почему DI важен в Rust?

В Rust, где нет классического ООП с наследованием, DI помогает:

  • Уменьшить связанность — модули зависят от абстракций, а не от конкретных реализаций.
  • Упростить тестирование — вы можете подменять реальные реализации моками.
  • Повысить гибкость — легко менять поведение системы без изменения её кода.

Основы: передача зависимостей через параметры

Самый простой способ внедрения зависимости — передача её в качестве параметра функции или конструктора структуры.

// Определяем трейт для абстракции
trait Logger {
    fn log(&self, message: &str);
}

// Реализация для консоли
struct ConsoleLogger;

impl Logger for ConsoleLogger {
    fn log(&self, message: &str) {
        println!("LOG: {}", message);
    }
}

// Реализация для файла
struct FileLogger {
    filename: String,
}

impl Logger for FileLogger {
    fn log(&self, message: &str) {
        // В реальном коде здесь была бы запись в файл
        println!("FILE: {} -> {}", self.filename, message);
    }
}

// Сервис, использующий DI через конструктор
struct UserService<L: Logger> {
    logger: L,
}

impl<L: Logger> UserService<L> {
    fn new(logger: L) -> Self {
        Self { logger }
    }

    fn create_user(&self, name: &str) {
        self.logger.log(&format!("User created: {}", name));
    }
}

fn main() {
    let console_logger = ConsoleLogger;
    let user_service = UserService::new(console_logger);
    user_service.create_user("Alice");
}

В этом примере UserService не знает, какой именно логгер используется — он зависит только от трейта Logger. Это и есть суть DI.

Продвинутый подход: трейт-объекты и динамическая диспетчеризация

Когда вам нужно хранить разные реализации в одном контейнере, используйте трейт-объекты (Box<dyn Trait>).

trait Database {
    fn query(&self, sql: &str) -> Vec<String>;
}

struct PostgresDb;
impl Database for PostgresDb {
    fn query(&self, sql: &str) -> Vec<String> {
        vec!["result1".to_string(), "result2".to_string()]
    }
}

struct SqliteDb;
impl Database for SqliteDb {
    fn query(&self, sql: &str) -> Vec<String> {
        vec!["sqlite_result".to_string()]
    }
}

struct App {
    db: Box<dyn Database>,
    logger: Box<dyn Logger>,
}

impl App {
    fn new(db: Box<dyn Database>, logger: Box<dyn Logger>) -> Self {
        Self { db, logger }
    }

    fn run(&self) {
        let results = self.db.query("SELECT * FROM users");
        for r in results {
            self.logger.log(&r);
        }
    }
}

fn main() {
    let app = App::new(
        Box::new(PostgresDb),
        Box::new(ConsoleLogger),
    );
    app.run();
}

Преимущество трейт-объектов в том, что вы можете менять реализации во время выполнения. Недостаток — небольшой оверхед на динамическую диспетчеризацию.

Контейнер зависимостей: инверсия управления

Для крупных проектов удобно использовать контейнер зависимостей. Хотя в Rust нет стандартного DI-контейнера, его легко реализовать самостоятельно или использовать крейты, такие как shaku или dice.

Пример простого контейнера вручную:

use std::collections::HashMap;
use std::any::Any;

// Тип для хранения зависимостей
struct Container {
    services: HashMap<String, Box<dyn Any>>,
}

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

    fn register<T: 'static>(&mut self, name: &str, service: T) {
        self.services.insert(name.to_string(), Box::new(service));
    }

    fn resolve<T: 'static>(&self, name: &str) -> Option<&T> {
        self.services.get(name)
            .and_then(|s| s.downcast_ref::<T>())
    }
}

fn main() {
    let mut container = Container::new();
    container.register("logger", ConsoleLogger);
    container.register("db", PostgresDb);

    if let Some(logger) = container.resolve::<ConsoleLogger>("logger") {
        logger.log("Container works!");
    }
}

Более продвинутые крейты, например shaku, поддерживают автоматическое разрешение зависимостей, жизненные циклы и потокобезопасность.

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

Одно из главных преимуществ DI — упрощение тестирования. Вы можете легко подменить реальные зависимости моками.

struct MockLogger {
    messages: Vec<String>,
}

impl Logger for MockLogger {
    fn log(&self, message: &str) {
        // В моке просто сохраняем сообщение
        // В реальности вы бы использовали RefCell для изменения
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    struct TestLogger {
        pub messages: RefCell<Vec<String>>,
    }

    impl Logger for TestLogger {
        fn log(&self, message: &str) {
            self.messages.borrow_mut().push(message.to_string());
        }
    }

    #[test]
    fn test_create_user() {
        let logger = TestLogger {
            messages: RefCell::new(Vec::new()),
        };
        let service = UserService::new(logger);

        service.create_user("Bob");

        assert_eq!(logger.messages.borrow().len(), 1);
        assert!(logger.messages.borrow()[0].contains("Bob"));
    }
}

Обратите внимание на использование RefCell для внутренней мутабельности — это стандартный подход в Rust для тестовых моков.

Заключение

Dependency Injection в Rust — это не просто теория, а мощный инструмент для создания гибких, тестируемых и поддерживаемых приложений. Мы рассмотрели три уровня реализации:

  • Базовый — передача зависимостей через обобщения (статическая диспетчеризация).
  • Динамический — использование трейт-объектов для гибкости во время выполнения.
  • Кон

Блоги

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