Modéliser "le chat mange ou joue avec l'objet"

Le problème exposé dans ce sujet a été résolu.

Bonjour, le problème est simple:

Un animal domestique se trouve devant un objet (au sens large). Soit cet objet est comestible et l’animal le mange, soit l’objet ne l’est pas et l’animal joue avec. Quelle est la meilleure manière de modéliser cela sachant qu’il existe des dizaines d’objets possibles (ici, on simplifie avec deux cas possibles).

J’ai deux modèles en tête

public class Main {

    public static void main(String[] args) {
        Animal chat = new Animal();
        
        // Objet surprise : soit de la nourriture, soit un jouet
        Object surprise = Surprise.getSurprise();
        
        if (surprise instanceof Nourriture) {
            chat.manger(surprise);
        }
        else if (surprise instanceof Jouet) {
            chat.jouer(surprise);
        }
    }
}

Le souci de ce modèle c’est que il y a un if par objet possible, donc une dizaine de if pour une dizaine d’objects. Si on ajoute un nouveau type d’objet, on ajoute une nouveau if. L’avantage c’est que le code est logique: chat.manger(surprise); se lit Le chat mange la surprise.

public class Main {

    public static void main(String[] args) {
        Animal chat = new Animal();
        
        // Objet surprise : soit de la nourriture, soit un jouet
        Object surprise = Surprise.getSurprise();
        
        // L'objet est activé par le chat
        surprise.activation(chat);
    }
}

public class Nourriture implements Activation {
    
    public void activation(Animal animal) {
        animal.manger(this);
    }
}

public class Jouet implements Activation {
    
    public void activation(Animal animal) {
        animal.jouer(this);
    }
}

public interface Activation {
    public void activation(Animal animal);
}

L’avantage du modèle c’est qu’on évite les dizaines de if. De plus, si on ajoute un nouveau type d’objet, il suffit de le faire hériter de Activation. Le souci c’est qu’on perd de la logique dans le code : surprise.activation(chat); se lit la surprise active un chat, bref cette instruction ne veut rien dire.

Hello, j’aurais plutôt adopté la première approche pour conserver la logique car l’objet subissant l’action ne devrait pas définir l’action exécutée ni avoir de dépendance forte à l’acteur.

En soi le problème ici ne se pose que parce que tu ne connais pas le type de la surprise, mais c’est à l’animal de décider ce qu’il en fait lorsqu’il l’a reconnue AMHA.

public interface Gift {}

public interface Animal {
    void react(Gift gift);
    void play(Toy toy);
    void eat(Food food);
}

public interface Food extends Gift {}

public interface Toy extends Gift {}

public class Cat implements Animal {

    @Override
    public void react(Gift gift) {
        if (gift instanceof Toy) {
            play((Toy) gift);
        } else if (gift instanceof Food) {
            eat((Food) gift);
        }
    }

    @Override
    public void play(Toy toy) {
        System.out.println("I play with " + toy);
    }

    @Override
    public void eat(Food food) {
        System.out.println("I eat " + food);
    }
}
+2 -0

Je ne sais pas si c’est possible en Java mais tu pourrais peut-être combiner les deux avec une méthode privée de Interaction qui serait appelée par Chat::interagir (qui lui, aurait le droit de l’appeler). De cette manière, le code serait chat.interagir(surprise); qui se lirait Le chat interagit avec la surprise. mais ce serait bien la surprise qui sait comment le chat doit interagir avec.

Car si j’ajoute un nouveau type, il faut ajouter un else if dans le code…

marius007

C’est parce que tu pars du principe qu’il n’y a qu’une seule façon possible d’utiliser chaque objet. Ça peut a du sens si tu es certain qu’il n’y aura jamais qu’une seule façon d’interagir avec chaque type de surprise et donc que chaque action ne sera jamais conditionnée par la nature de l’acteur.

Tu peux aussi réutiliser des stratégies en reprenant l’idée de BorisD d’une certaine façon, mais ça sera peut-être plus compliqué :

public interface Interactive {}

public interface Food extends Interactive {}

public interface Toy extends Interactive {}

public interface Interaction {
    void interact(Interactive interactive);
}

public class EatInteraction implements Interaction {
    @Override
    void interact(Interactive interactive) {
        if (!(interactive instanceof Food)) {
            return;
        }
        // I eat ((Food) interactive)
    }
}

public class PlayInteraction implements Interaction {
    @Override
    void interact(Interactive interactive) {
        if (!(interactive instanceof Toy)) {
            return;
        }
        // I play with ((Toy) interactive)
    }
}

public interface Animal {
    void interact(Interactive gift);
}

public class Cat implements Animal {

    private final List<Interaction> interactions
            = Collections.unmodifiableList(Arrays.asList(new EatInteraction(), new PlayInteraction()));

    @Override
    public void interact(Interactive interactive) {
        interactions.forEach(i -> i.interact(interactive));
    }
}

La meilleure solution sera certainement la plus simple (et donc peut-être moins flexible) au vu de ce que tu souhaites faire car il ne sert à rien de compliquer le code pour des fonctionnalités que tu n’utiliseras jamais. Peut-être que dans ton cas il vaut mieux utiliser ta 2e approche si ton code n’aura pas à évoluer vers plus de complexité.

En java moderne on peut avoir des implémentations par défaut des méthodes dans les interfaces

public interface Animal {
    default void react(Gift gift) {
        if (gift instanceof Toy) {
            play((Toy) gift);
        } else if (gift instanceof Food) {
            eat((Food) gift);
        }
    }
    void play(Toy toy);
    void eat(Food food);
}

Ce qui évite d’avoir à redéfinir react() dans toutes les sous-classes, où d’intercaler une classe abstraite qui s’en occupe.

Ce ne sont pas des "astuces". Ce sont des mécanismes, des manières de faire, qui conviennent ou pas selon le contexte.

Et c’est difficile d’avoir un contexte qui tienne debout avec ces exemples à la con d’animaux et autres balivernes. La programmation orientée objets est là pour aider à réaliser des composants logiciels qui font quelque chose en interaction les uns avec les autres. Pas modéliser des relations imaginaires dans un monde qui n’existe pas (et dans lequel on ne peut pas jouer avec la nourriture !)

Une autre façon de faire :

interface Animal  {
    default void react(Gift g) {
        g.used_by(this);           // here is the trick
    }
    void play(Toy t);
    void eat(Food f);
}

interface Gift {
    void used_by(Animal a);
}

class Food implements Gift
    @Override
    void used_by(Animal a) {
          a.eat(this);
    }
}

La méthode react appelle la méthode used_by du Gift, ce qui permet de faire le "dispatch" naturellement, selon le type du Gift.

+1 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte