comment appliquer les principes SOLID?

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

Bonjour j’essaye de comprendre les principes SOLID mais ils m’échappent complètement, en commençant par le premier.

Single Responsibility Principle

Si une classe a plus d’une responsabilité, alors ces responsabilités deviennent couplées. Des modifications apportées à l’une des responsabilités peuvent porter atteinte ou inhiber la capacité de la classe de remplir les autres. Ce genre de couplage amène à des architectures fragiles qui dysfonctionnent de façon inattendues lorsqu’elles sont modifiées. — Robert C. Martin

J’essaye de trouver un exemple très très simple de code qui ne respecte pas ce principe et que je puisse essayer de corriger. Le tuto où j’apprend donne un exemple très difficile avec des BDD en C#…. et sur d’autres sites, je ne comprends pas leur exemple :

http://image.noelshack.com/fichiers/2019/18/5/1556875169-capture.png

Je vais essayer de vous expliquer ce que j’ai compris de ce principe avec mes mots :

Chaque classe devrait gérer une seule tâche spécifique dans le code global. Une classe représente le modèle d’un objet, donc un objet devrait être dédié à une tâche et s’occuper d’une action spécifique.

Oui, c’est nul comme explication…. :(

Salut,

Ta compréhension est plutôt bonne. Comme beaucoup de principes généraux, il est assez dur de les comprendre profondément sans en avoir eu l’expérience pratique.

Je peux expliquer un peu plus en détail l’exemple du mouton. Ce que dit le diagramme de gauche, c’est qu’un mouton est un type de producteur de lait et de laine (relation d’héritage). Il n’y a pas de séparation entre les deux. Dans le diagramme de classe, un producteur ne peut être qu’un producteur de lait et de laine, pas un seul des deux.

Là où ça commence à poser problème, c’est que quand on réfléchit plus en détail, on voit bien que si un mouton peut bien produire de la laine et du lait, ce sont des choses tout à fait indépendantes ! Par exemple, seule la brebis produit du lait, les agneaux n’ont pas vraiment de laine. Il vaut mieux donc dire qu’un mouton implémente les interfaces produire de la laine et produire du lait (relation d’implémentation d’interface), parce que l’indépendance offre plus de future flexibilité. Au début, la fusion ne gêne peut-être pas, mais peut-être que plus tard dans le développement, on voudra différents types de moutons (bélier, agneau, brebis), peut-être qu’on voudra aussi considérer d’autres animaux qui font du lait mais pas de laine (vache) ou des animaux qui font de la laine mais pas du lait (des bisons ?).

Si on fusionne les deux interfaces a priori indépendantes, on risque d’introduire des erreurs en modifiant l’une ou l’autre, car rien ne dit que l’implémentation du producteur de lait et laine n’a pas des interdépendances cachées entre la production de laine et de lait. Par exemple, on pourrait imaginer un champ « dernière opération » sur un producteur de lait et de laine et qui correspondrait à la dernière fois qu’un mouton a été trait ou tondu. On voit bien que si l’implémentation partage cette variable, on aura du mal à savoir si la dernière opération était une traite ou bien une tonte. Pourtant, c’est tout ce qui compte, parce qu’on ne tond et ne trait pas à la même fréquence.

+3 -0

En fait pas besoin de code je pense avoir compris, j’explique sur un nouvel exemple.

Une classe modélise des robots. Cette classe Robot pourrait contenir deux méthodes: rotation() et déplacement() qui gère respectivement la rotation d’un robot sur lui même et son déplacement dans la pièce.

Mais comme il existe des robots qui peuvent se déplacer sans être capable de faire une rotation sur eux-même,il est donc préférable d’avoir une classe absraite Robot et deux interfaces, l’une pour gérer les rotation, l’autre pour gérer le déplacement.

Ainsi chaque interface et chaque classe a une seule responsabilité. C’est plus ou moins ça ?

C’est exactement ça. L’idée est d’isoler les "responsabilités" qui peuvent être indépendantes les unes des autres, et de ne pas les coupler pour rien.

De façon très vulgarisée: c’est pas parce qu’une personne dans un foyer est responsable de faire la cuisine qu’elle devrait être automatiquement responsable de faire la vaisselle. Ça n’a pas de sens de coupler ces deux responsabilités "en dur".

+0 -0

Bonjour, j’ai ceci :

class Personnage {
    private Voiture voiture;

    public setEssence(int q) {
        voiture.setEssence(q);
    }
}

Et dans la méthode main :

public static void main(String[] args) {
    monPersonnage.setEssence(30);
}

On est bien d’accord que ce code ne respecte pas le premier principe de SOLID ? (le single responsability)

Ça me paraît être une modélisation correcte : une voiture a un réservoir d’essence, un personnage peut posséder une voiture, c’est le personnage qui met l’essence dans la voiture. Après le nommage de la méthode de l’utilisateur est assez mauvais, quelque-chose comme faireLePlein() ou alimenterVoiture(int quantiteEssence) serait sûrement plus judicieux…

merci… je vais préciser ma question avec du vrai code parce que j’ai peur d’avoir choisi un mauvais exemple.

public class Game {
    private Map map;
    private Window window;
    private Keyboard keyboard;
    
    public void start() {
        map = new Map();
        window = new Window(map);
        keyboard = new Keyboard();
        
        window.setKeyboard(keyboard);
    }
}

public class Map {
    private Keyboard keyboard;
    
    public void setKeyboard(Keyboard k) {
        keyboard = k;
    }
}

public class Window {
    private Map map; // Le constructeur initialise map
    
    public void setKeyboard(Keyboard k) {
        map.setKeyboard(k);
    }
}

Dans ce code, je pense que le S de SOLID n’est pas respecté. Pour corriger l’erreur, je pense qu’il faut remplacer la ligne 11 par celle-ci :

map.setKeyboard(keyboard);

Je fais ça parce que ça n’a aucun sens que window s’occupe de donner un Keyboard à Map. Par le single responsability principle, je dirais que il faut directement donner à map le keyboard dont il a besoin.

+0 -0

Ça fait quoi un Keyboard dans ce code ? En effet, je vois pas bien pourquoi une Map (renommer en WorldMap, CityMap ou autre peut-être ?) devrait avoir un Keyboard.

J’ai l’impression que ton idée derrière est de réagir à l’utilisation du clavier, auquel cas tu devrais creuser le design pattern Observer, voire même des libs comme Reactor ou ReactiveX si tu as besoin d’un système publish/subscribe efficace pour gérer les interactions entre tes composants et que tu as du temps.

+0 -0

Je pense avoir repéré du code qui ne respecte pas le premier principe de SOLID:

class Game {
    public void movePlayer(int x, int y) {
        if (player.getEnergy() > 0) {
            player.decreaseEnergy();
            player.moveTo(x, y);
        }
        else {
            System.out.println("Pas assez d'énergie pour se déplacer.");
        }
    }
}

En effet je pense que ce n’est pas à la classe Game de s’assurer que le player a assez d’énergie pour avancer. La vérification de l’énergie devrait se faire dans la classe Player :

class Game {
    public void movePlayer(int x, int y) {
        player.moveTo(x, y);
    }
}

class Player {
    public void moveTo(int x, int y) {
        if (this.energy > 0) {
            this.decreaseEnergy();
            this.posX = x;
            this.posY = y;
        }
        else {
            System.out.println("Pas assez d'énergie pour se déplacer.");
        }
    }
}

Est ce bien comme ça?

Pourquoi ce choix ?
Ça ne respecte pas un autre principe. Si tu veux changer le comportement de player (genre sa vitesse dépend de son énergie, ou il peut courir…), ça t’oblige à modifier la classe game.

Looping

En fait mon implémentation est plutôt étrange, l’un et l’autre sont implémentés j’ai l’impression.

Si l’énergie du perso était une donnée interne invisible de l’extérieur sans accesseurs, je me posais alors la question de comment le joueur peut-il notifier le Game qu’il doit aller se reposer ? Je n’ai pas encore compris la bonne solution à ce problème.

https://zestedesavoir.com/forums/sujet/12487/implementation-dun-jeu-de-plateau/

class Game {
    private Player activePlayer; // Défini dans le constructeur de Game
    
    public void movePlayer(int x, int y) {
        int energy = activePlayer.getEnergy();
        
        if (energy <= 0) {
            goToSleep();
            return;
        }
        
        int frontX = activePlayer.getFrontX();
        int frontY = activePlayer.getFrontY();
        
        if (frontX >= 0 && frontX < Level.SIZE_X && frontY >= 0 && frontY < Level.SIZE_Y) {
            for (GameObject o: actualLevel.getObjects()) {
                if (o.isAtPosition(frontX, frontY) && o.isObstacle()) {
                    // Obstacle trouvé devant le joueur
                    return;
                }
            }
            // Déplace le joueur et met à jour la position affichée
            activePlayer.move(x, y);
            window.update();
        }
    }
}

class Player {
    public void move(int x, int y) {
        if (energy > 0) {
            energy--;
        }
        // Pour des raisons d'une autre fonctionnalité,
        // Le joueur "peut" se déplacer sans énergie
        setPos(posX + x, posY + y);
    }
}
+0 -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