Version sans JavaScript, en lecture seule : les ateliers, les quiz et l’exécution du code n’y sont pas. Version complète · Catalogue · Se connecter

Support de cours · chapitre 5

Dépendre d'une interface

Pour réduire le couplage, il ne suffit pas de déclarer une interface : la classe doit recevoir l'objet dont elle dépend au lieu de le créer elle-même.

Ce chapitre prolonge la séquence 3. Il est court, mais l'idée qu'il présente est souvent mal appliquée.

Couplage fort et couplage faible

Voici un dresseur qui ne sait entraîner qu'un colvert :

class Dresseur {
    private CanardColvert canard;

    Dresseur(CanardColvert c) { this.canard = c; }
    void entrainer() { canard.voler(); }
}

Pour ajouter un CanardEnPlastique, il faut modifier Dresseur, et de même pour un CanardRobot. Chaque nouvelle sorte de canard oblige à modifier la classe Dresseur, qui n'a pourtant pas à connaître les différentes sortes de canards.

Voici la version faiblement couplée :

interface ICanard { void voler(); }

class CanardColvert implements ICanard {
    public void voler() { System.out.println("Je vole !"); }
}

class Dresseur {
    private ICanard canard;

    Dresseur(ICanard c) { this.canard = c; }
    void entrainer() { canard.voler(); }
}

Le dresseur peut maintenant entraîner n'importe quel canard, y compris ceux qui seront ajoutés plus tard, sans qu'on ait à modifier sa classe.

Déclarer une interface ne suffit pas

Le code suivant déclare bien une interface, mais le couplage reste le même.

class Dresseur {
    private ICanard canard;

    Dresseur() {
        this.canard = new CanardColvert();   // ← il le fabrique lui-même
    }
}

Le type déclaré est ICanard, mais la classe reste liée à CanardColvert : elle en connaît le nom, elle l'instancie, et on ne peut pas lui fournir un autre canard. Pour tester le dresseur, il faut un vrai colvert, et pour l'utiliser avec un robot, il faut modifier son code.

La correction ne demande qu'une ligne : le new est déplacé chez l'appelant.

class Dresseur {
    private final ICanard canard;

    Dresseur(ICanard canard) { this.canard = canard; }   // injection
}

C'est ce qu'on appelle l'injection de dépendances : dans sa forme la plus simple, il s'agit seulement d'un paramètre de constructeur.

Ce que l'injection apporte

Le remplacement. Un CanardRobot, un faux canard pour les tests ou une implémentation de secours peuvent être utilisés sans modifier Dresseur.

Le test. Un test unitaire peut fournir un faux canard qui enregistre qu'on l'a fait voler, sans base de données, sans réseau et sans vrai bassin.

class CanardEspion implements ICanard {
    boolean aVole = false;
    public void voler() { aVole = true; }
}

CanardEspion espion = new CanardEspion();
new Dresseur(espion).entrainer();
// espion.aVole vaut true : le dresseur a bien fait son travail

La direction des dépendances. Le dresseur dépend d'une abstraction (savoir voler) et non d'une implémentation. Les classes concrètes dépendent de la même abstraction, et aucune classe concrète ne dépend d'une autre.

classDiagram
    direction TB
    class ICanard {
        <<interface>>
        +voler()
    }
    Dresseur --> ICanard : dépend de
    ICanard <|.. CanardColvert
    ICanard <|.. CanardRobot
    ICanard <|.. CanardEspion

Sur le diagramme, toutes les flèches pointent vers l'interface et aucune vers une classe concrète.

Où placer l'interface

Une interface est justifiée quand elle correspond à un rôle que plusieurs objets peuvent tenir, ou à un objet qu'on veut pouvoir remplacer.

La composition comme choix de comportement

Le même mécanisme, appliqué à l'intérieur d'une classe, correspond au patron Stratégie, présenté au chapitre 10.

interface VolStrategy { void voler(); }

class VolerAvecAiles implements VolStrategy {
    public void voler() { System.out.println("Je vole avec mes ailes !"); }
}

class NePasVoler implements VolStrategy {
    public void voler() { System.out.println("Je ne vole pas."); }
}

class Canard {
    private VolStrategy vol;
    Canard(VolStrategy vol) { this.vol = vol; }
    void effectuerVol() { vol.voler(); }
}

Un CanardRobot reçoit simplement NePasVoler, sans qu'il faille de sous-classe, d'instanceof ou de if.

Question d'épreuve

Que faut-il faire, en plus de déclarer un type interface, pour qu'une classe soit réellement remplaçable ?

Il faut lui passer l'implémentation depuis l'extérieur (par le constructeur) au lieu de la laisser l'instancier elle-même. Tant que la classe contient new CanardColvert(), le type déclaré ne change rien.