GLO-4002 - Site du cours 2023

Temps estimé: 60 minutes

Exercices sur le DIP

Pratiquez le DIP en complétant les exercices suivants:

1. Cognage de clous

Créez un projet en respectant les critères suivants:

  • Un marteau peut cogner un clou.
  • Un clou ne peut se faire cogner qu'une seule fois.
  • Lorsqu'un clou se fait cogner, il envoi un message à l'utilisateur disant qu'il a été cogné.

⚠️ Voici une solution qui enfreint le DIP: ⚠️

Hammer.java:
public class Hammer {

    public void hit(Nail nail) {
        nail.hit();
    }
}
Nail.java:
public class Nail {

    private boolean isHit = false;

    public void hit() {
        if (!isHit) {
            isHit = true;
            System.out.println("Ouch!");
        }
    }
}

Remarquez que l'écriture d'un message à l'utilisateur est un niveau plus bas que notre domaine: Marteau et Clou.

Si vous ne l'avez pas déjà fait, assurez-vous maintenant de respecter le DIP.

Voici une solution qui respecte le DIP:

Hammer.java:
public class Hammer {

    public void hit(Nail nail) {
        nail.hit();
    }
}
Nail.java:
public class Nail {

    private final Notifier notifier;
    private boolean isHit = false;

    public Nail(Notifier notifier) {
        this.notifier = notifier;
    }

    public void hit() {
        if (!isHit) {
            isHit = true;
            notifier.doNotify("Ouch!");
        }
    }
}
Notifier.java:
public interface Notifier {

    void doNotify(String message);
}
ConsoleNotifier.java:
public class ConsoleNotifier implements Notifier {

    public void doNotify(String message) {
        System.out.println(message);
    }
}

Afin d'éviter que Nail (haut niveau) connaisse le mode de communication avec l'utilisateur (bas niveau), nous utilisons une abstraction Notifier.

2. Par Minou

Dans un jeu où les lions prétendent être des chats afin de mieux les manger, créez un projet tout en respectant les critères suivants:

  • Le jeu commence avec une quantité de 100 souris.
  • Le jeu détient une liste de 8 chats et 2 lions. Chaque espèce a accès à une action différente:
    • Chat: Manger une souris. Ceci décrémente de 1 le nombre de souris du jeu.
    • Lion: Manger un chat. Ceci retire le chat en question de la liste des chats du jeu.
  • Respectez le DIP.

Pour vous aider, nous vous fournissons deux classes que vous pouvez modifier comme vous voulez:

Application.java:
public class Application {

    private static final int INITIAL_NUMBER_OF_MICE = 100;
    private static final int INITIAL_NUMBER_OF_CATS = 8;
    private static final int INITIAL_NUMBER_OF_LIONS = 2;

    public Application() {
        Game game = new Game(INITIAL_NUMBER_OF_MICE, INITIAL_NUMBER_OF_LIONS, INITIAL_NUMBER_OF_CATS);
    }
}
Game.java
public class Game {

    private int numberOfMice;
    private final List<Cat> cats;
    private final List<Lion> lions;

    public Game(int numberOfMice, int numberOfLions, int numberOfCats) {
        this.numberOfMice = numberOfMice;
        this.cats = new ArrayList<>();
        this.lions = new ArrayList<>();

        for (int i = 0; i < numberOfLions; i++) {
            lions.add(new Lion());
        }

        for (int i = 0; i < numberOfCats; i++) {
            cats.add(new Cat());
        }
    }
}

Pour respecter le DIP dans cet exercice, il faut permettre aux chats et aux lions (haut niveau) de modifier les éléments du jeu (bas niveau), sans leurs donner un accès direct.

Voici les deux interfaces utilisées:

MouseEater.java:
public interface MouseEater {

    void eatAMouse();
}
Cat.java:
public class Cat {

    private final MouseEater mouseEater;

    public Cat(MouseEater mouseEater) {
        this.mouseEater = mouseEater;
    }

    public void eatAMouse() {
        mouseEater.eatAMouse();
    }
}
CatEater.java:
public interface CatEater {

    void eatACat(Cat cat);
}
Lion.java:
public class Lion {

    private final CatEater catEater;

    public Lion(CatEater catEater) {
        this.catEater = catEater;
    }

    public void eatACat(Cat cat) {
        catEater.eatACat(cat);
    }
}

Voici un exemple d'implémentation, mais ça pourrait être n'importe quoi:

CatList.java:
public class CatList extends ArrayList<Cat> implements CatEater {

    public void eatACat(Cat cat) {
        this.remove(cat);
    }
}
MousePopulation.java:
public class MousePopulation implements MouseEater {

    private int numberOfMice;

    public MousePopulation(int numberOfMice) {
        this.numberOfMice = numberOfMice;
    }

    public void eatAMouse() {
        numberOfMice--;
    }
}

Ces implémentations sont ensuite ajoutées au jeu:

Game.java:
public class Game {

    private final MousePopulation mousePopulation;
    private final List<Lion> lions;
    private final CatList cats;

    public Game(int numberOfMice, int numberOfLions, int numberOfCats) {
        this.mousePopulation = new MousePopulation(numberOfMice);
        this.lions = new ArrayList<>();
        this.cats = new CatList();

        for (int i = 0; i < numberOfLions; i++) {
            lions.add(new Lion(cats));
        }

        for (int i = 0; i < numberOfCats; i++) {
            cats.add(new Cat(mousePopulation));
        }
    }
}

Dans le cadre de cet exercice, cette solution est suffisante. Cependant, il y a toujours place à amélioration.

Par exemple, pour éviter que Game dépende d'implémentations, nous pourrions déplacer la logique de création de ces objets dans des Factories qui retournent des Interfaces.

En faisant ça, nous obtiendrions une classe Game avec aucun comportement dont nous pourrions supprimer, en déplacant ce qui reste dans Application.

Conclusion

Le respect du DIP implique souvent la création d'une abstraction pour séparer les modules de haut niveau des modules de bas niveau.

Certains patrons de conception peuvent également être très utiles pour faciliter ces changements (ex. Visitor, Bridge).