Aller au contenu

Injection de dépendances

🤖 Rédaction assistée par IA.

Problème

Une classe a besoin d'autres objets (ses dépendances) pour fonctionner. Si elle les crée elle-même (new) ou les récupère par un accès statique (getInstance()), alors :

  • Ces dépendances sont cachées : elles n'apparaissent pas dans la signature de la classe
  • Elles sont difficiles à remplacer (ex : par un objet factice dans les tests, par une autre implémentation selon la configuration)
  • La classe est fortement couplée à une implémentation concrète

Cas d'école

Reprenons la Sequence présentée dans la fiche Singleton. Un service de création d'utilisateurs s'en sert pour attribuer des identifiants uniques :

class UserService {
    createUser(name: string): User {
        const id = Sequence.getInstance().next();
        return new User(id, name);
    }
}

Il est impossible de tester UserService avec un identifiant prévisible : l'état de la Sequence est partagé par toute l'application (et donc par tous les tests).

Solution

Le principe consiste à fournir à la classe ses dépendances au lieu de la laisser les obtenir elle-même :

  • Les dépendances sont passées par le constructeur (elles deviennent explicites)
  • La classe dépend d'une abstraction (interface) plutôt que d'une implémentation concrète (voir le principe d'inversion des dépendances, le "D" de SOLID)
  • Les objets sont assemblés à un seul endroit, au démarrage de l'application

On parle d'inversion de contrôle (IoC) : ce n'est plus la classe qui contrôle la création de ses dépendances.

Exemple d'implémentation

Nous définissons une abstraction pour la génération d'identifiants et la Sequence l'implémente (avec un constructeur public, ce n'est plus un singleton) :

interface IdGenerator {
    next(): number;
}

class Sequence implements IdGenerator {
    private _current = 0;

    next(): number {
        return this._current++;
    }
}

UserService reçoit son générateur par le constructeur :

class UserService {
    constructor(private idGenerator: IdGenerator) {}

    createUser(name: string): User {
        return new User(this.idGenerator.next(), name);
    }
}

Au démarrage de l'application, une seule instance de Sequence est créée puis transmise aux classes qui en ont besoin :

// main.ts - assemblage des objets (composition root)
const sequence = new Sequence();
const userService = new UserService(sequence);
const groupService = new GroupService(sequence);

Dans les tests, il devient trivial d'injecter un générateur factice :

class FakeIdGenerator implements IdGenerator {
    next(): number {
        return 42;
    }
}

const userService = new UserService(new FakeIdGenerator());
const user = userService.createUser("john");
// user.id vaut 42

Conteneurs IoC et portée singleton

Lorsque le nombre de classes augmente, l'assemblage manuel devient fastidieux. Les frameworks proposent un conteneur IoC qui construit les objets et injecte automatiquement leurs dépendances.

Par défaut, ces conteneurs créent une seule instance par service (portée singleton). Nous obtenons ainsi l'unicité de l'instance sans accès statique :

@Injectable({ providedIn: 'root' })
export class Sequence {
    // ...
}

@Injectable({ providedIn: 'root' })
export class UserService {
    private sequence = inject(Sequence);
    // ...
}
  • Avec Symfony, les services sont partagés par défaut et l'autowiring injecte les dépendances en s'appuyant sur le type des paramètres du constructeur :
class UserService
{
    public function __construct(private IdGenerator $idGenerator) {}
}
  • Avec Spring, les composants (@Component, @Service...) ont la portée singleton par défaut :
@Service
public class UserService {
    private final IdGenerator idGenerator;

    public UserService(IdGenerator idGenerator) {
        this.idGenerator = idGenerator;
    }
}

Mises en garde

  • Il convient de privilégier l'injection par le constructeur (plutôt que par mutateur ou attribut) : l'objet est complet dès sa création et ses dépendances sont visibles
  • Un constructeur avec un grand nombre de dépendances est le signe d'une classe qui a trop de responsabilités
  • Le conteneur ne doit pas être utilisé comme un service locator : récupérer le conteneur dans le code métier pour y chercher ses dépendances revient à réintroduire des dépendances cachées
  • L'injection de dépendances n'impose pas l'utilisation d'un framework : un assemblage manuel au démarrage suffit pour une petite application

Liens utiles