Aller au contenu

Inversion des dépendances (DIP)

Inversion des dépendances (DIP)

Introduction

Dans une architecture applicative, les composants de haut niveau ne doivent pas dépendre directement des composants de bas niveau. Les deux doivent dépendre d'abstractions, telles que des interfaces. De même, les abstractions ne doivent pas dépendre des détails d'implémentation. A l'inverse, les implémentations doivent dépendre des abstractions.

En appliquant ce principe :

  • Les composants sont faiblement couplés.

  • Une implémentation peut être remplacée sans modifier le code qui l'exécute.

  • Les tests unitaires sont plus faciles à écrire grâce aux implémentations simulées (mocks).

  • L'application est plus évolutive.

Ainsi, le code devient alors plus flexible, plus maintenable et plus simple à faire évoluer.

Exemple ne respectant pas le principe d'inversion des dépendances

Soit une classe responsable de l'envoi des factures par e-mail :

public class EmailSender
{
    public void Send(Invoice invoice)
    {
        // Envoi de l'e-mail
    }
}

Cette classe est utilisée dans la classe InvoiceService :

public class InvoiceService
{
    private readonly EmailSender _emailSender;

    public InvoiceService()
    {
        _emailSender = new EmailSender();
    }

    public void SendInvoice(Invoice invoice)
    {
        _emailSender.Send(invoice);
    }
}

Cette implémentation présente deux inconvénients :

  • Ce choix de conception augmente le couplage entre les deux classes. Toute évolution de la classe EmailSender est susceptible d'avoir un impact direct sur la classe InvoiceService.

  • Cette dépendance rend également les tests unitaires plus difficiles à écrire. La classe EmailSender ne pouvant pas être remplacée par une implémentation simulée (mock), les tests risquent d'exécuter un véritable envoi d'e-mail ou de nécessiter des techniques de contournement.

Application du principe d'inversion des dépendances

L'application du principe d'inversion des dépendances consiste à faire dépendre les composants d'une abstraction, plutôt que d'une implémentation concrète. Implémentons pour cela l'interface IEmailSender :

public interface IEmailSender
{
    void Send(Invoice invoice);
}

Puis son implémentation :

public class EmailSender : IEmailSender
{
    public void Send(Invoice invoice)
    {
        // Envoi de l'e-mail
    }
}

La classe InvoiceService dépend désormais de cette interface :

public class InvoiceService
{
    private readonly IEmailSender _emailSender;

    public InvoiceService(IEmailSender emailSender)
    {
        _emailSender = emailSender;
    }

    public void SendInvoice(Invoice invoice)
    {
        _emailSender.Send(invoice);
    }
}

La dépendance est désormais inversée. La classe InvoiceService ne dépend plus directement de l'implémentation EmailSender, mais uniquement de l'abstraction IEmailSender. Les implémentations concrètes peuvent ainsi être remplacées sans modifier InvoiceService.

Concernant les injections de dépendances

Dans une application .NET, il suffit d'enregistrer l'implémentation dans le conteneur d'injection de dépendances :

services.AddScoped<IEmailSender, EmailSender>();

Lors de l'instanciation de la classe InvoiceService, le conteneur injecte automatiquement l'implémentation EmailSender via l'interface IEmailSender.

L'injection de dépendances n'est pas le principe DIP lui-même. Il s'agit d'un mécanisme qui permet de mettre en œuvre ce principe en fournissant les implémentations des abstractions, sans que les classes aient à les créer.