Aller au contenu principal
IngénierieMar 28, 2026

Deep EVM #22 : Injection de dépendances en Rust — ServiceLocator, Arc et objets trait

OS
Open Soft Team

Engineering Team

Le principe d’injection de dépendances

L’injection de dépendances est un principe de conception fondamental : un composant doit recevoir ses dépendances de l’extérieur plutôt que de les créer en interne. Rust n’a pas de framework DI standard, et son modèle de propriété rend les patterns DI naïfs maladroits.

Traits comme interfaces

En Rust, les traits sont l’équivalent des interfaces :

#[async_trait]
trait Database: Send + Sync {
    async fn get_user(&self, id: UserId) -> Result<User>;
    async fn save_user(&self, user: &User) -> Result<()>;
}

#[async_trait]
trait Cache: Send + Sync {
    async fn get(&self, key: &str) -> Option<Vec<u8>>;
    async fn set(&self, key: &str, value: &[u8], ttl: Duration) -> Result<()>;
}

Arc vs génériques

Dispatch dynamique avec Arc

struct UserService {
    db: Arc<dyn Database>,
    cache: Arc<dyn Cache>,
}

impl UserService {
    fn new(db: Arc<dyn Database>, cache: Arc<dyn Cache>) -> Self {
        Self { db, cache }
    }
}

Avantages : flexibilité, échange facile des implémentations, pas de monomorphisation. Inconvénients : indirection de pointeur, pas d’inlining.

Dispatch statique avec génériques

struct UserService<D: Database, C: Cache> {
    db: D,
    cache: C,
}

Avantages : inlining, pas d’allocation heap, performances optimales. Inconvénients : code plus verbeux, monomorphisation (plus gros binaire).

Règle pratique : utilisez Arc<dyn Trait> pour les frontières de service et les génériques pour le code critique en performance.

Le pattern Composition Root

Construisez le graphe de dépendances au point d’entrée de l’application :

#[tokio::main]
async fn main() -> Result<()> {
    // Composition root — toutes les dépendances sont créées ici
    let pool = PgPool::connect(&database_url).await?;
    let redis = RedisClient::connect(&redis_url).await?;

    let db: Arc<dyn Database> = Arc::new(PostgresDb::new(pool));
    let cache: Arc<dyn Cache> = Arc::new(RedisCache::new(redis));

    let user_service = Arc::new(UserService::new(db.clone(), cache.clone()));
    let auth_service = Arc::new(AuthService::new(db.clone()));

    let app = Router::new()
        .route("/users", get(list_users))
        .with_state(AppState { user_service, auth_service });

    axum::serve(listener, app).await?;
    Ok(())
}

Mocks pour les tests

struct MockDatabase {
    users: Mutex<HashMap<UserId, User>>,
}

#[async_trait]
impl Database for MockDatabase {
    async fn get_user(&self, id: UserId) -> Result<User> {
        self.users.lock().await
            .get(&id)
            .cloned()
            .ok_or(Error::NotFound)
    }

    async fn save_user(&self, user: &User) -> Result<()> {
        self.users.lock().await.insert(user.id, user.clone());
        Ok(())
    }
}

#[tokio::test]
async fn test_user_creation() {
    let db: Arc<dyn Database> = Arc::new(MockDatabase::default());
    let cache: Arc<dyn Cache> = Arc::new(MockCache::default());
    let service = UserService::new(db, cache);

    let user = service.create_user("Alice").await.unwrap();
    assert_eq!(user.name, "Alice");
}

Le pattern ServiceLocator

Pour les systèmes complexes avec de nombreuses dépendances :

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

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

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

Conclusion

L’injection de dépendances en Rust repose sur les traits, Arc et le pattern composition root. Pas besoin de framework — le système de types de Rust et son modèle de propriété guident naturellement vers une bonne architecture.