Solid Code

Un article pour présenter les principes du code SOLID.

SOLID est un acronyme représentant cinq principes de bases pour la programmation orientée objet, introduits par Michael Feathers et Robert C. Martin au début des années 2000. Ils sont censés apporter une ligne directrice permettant le développement de logiciel plus fiable et plus robuste.

Wikipedia

S : SINGLE RESPONSABILITY PRINCIPLE (Ou Principe de responsabilité unique).

Une classe doit avoir une et seule responsabilité.

Le meilleur moyen de savoir si une classe est une fonction ou autre respecte le principe de responsabilité unique, il suffit de se répondre à la question qu’est ce que ça fait : Si vous répondez quelque chose comme :

  • « Ma classe fait ceci ET cela », ou
  • « Ma classe fait ceci OU cela », ou encore,
  • « Ma classe fait ceci, sauf dans tel cas »

alors c’est que votre classe n’a pas qu’une seule responsabilité.

O : OPEN / CLOSED PRINCIPLE

Les objets doivent être Ouverts aux extensions mais fermés aux modifications.

Un objet, doit avoir des propriétés et méthodes privées, et permettre de rajouter des fonctionnalités (class ClientGold extends Client) via des méthodes publiques (voir protégées).

Ce principe implique que l’on doit pouvoir changer le comportement d’un objet sans en changer le code source.

L : LISKOV SUBSTITUTION PRINCIPLE

Wikipedia

Une classe doit pouvoir être remplacée par une instance d’un de ses sous-types, sans modifier la cohérence du programme.

Ce n’est pas un principe simple à comprendre, en effet, une bonne façon de comprendre ce principe est de passer par un exemple (fréquent) qui montre le NON RESPECT du principe :

Prenons une classe `Rectangle :

  • Rectangle.java
package fr.mimiz.tries;

public class Rectangle {

    private int height;
    private int width;


    public int surface(){
        return height*width;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }
}

Cette classe est utilisée dans un programme de dessin :

  • Dessin.java
package fr.mimiz.tries;

public class Dessin {

    public static void main(String[] args) {

        Rectangle r = Drawing.getRectangle();
        r.setHeight(10);
        r.setWidth(2);

        System.out.println(r.surface());

    }

    private static Rectangle getRectangle() {
        return new Rectangle();
    }
}

Maintenant, nous le savons, un Carré est une “sorte” de Rectangle, nous pouvons créer la classe Carre.

  • Carre.java
package fr.mimiz.tries;

public class Carre extends Rectangle {

    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width);
    }

    @Override
    public void setHeight(int height) {
        super.setHeight(height);
        super.setWidth(height);
    }
}

Et du coup le programme pourrait être modifié comme ceci …

  • Dessin.java
package fr.mimiz.tries;

public class Dessin {

    public static void main(String[] args) {

        Rectangle r = Drawing.getRectangle();
        r.setHeight(10);
        r.setWidth(2);

        System.out.println(r.surface());

    }

    private static Rectangle getRectangle() {
        return new Carre();
    }
}

Vous voyez le problème ?

Et oui une dépendance (ici représentée par la méthode : getRectangle()) peut très bien changer d’implémentation, comme dans notre exemple, et du coup causer des problèmes dans le code…

Note : Quelle serait une implémentation valable de mon Rectangle et Carré en conservant l’héritage en respectant le principe de substitution de Liskov ? Envoyez moi vos propositions…

I : INTERFACE SEGREGATION PRINCIPLE

L’idée derrière ce principe est de créer une interface par client plutôt qu’une interface pour tous les clients.

Les clients (ici client signifie « classes qui implementent l’interface ») n’ont pas à implementer des méthodes dont ils n’ont pas à se servir.

Reprenons l’exemple des Carre et des Rectangle … Et créons une interface Forme

  • Forme.java
package fr.mimiz.tries;

public interface Forme {

    public int surface();

    public void draw();
}

Et du coup, si on demande à nos classes d’implémenter l’interface nous devons créer les deux méthodes … Et comme Carre hérite de Rectangle … l’implémentation peut seulement être dans la classe `Rectangle.

  • Rectangle.java
package fr.mimiz.tries;

public class Rectangle implements Forme{

    private int height;
    private int width;

    @Override
        public int surface(){
        return height*width;
    }

    @Override
    public void draw() {
        // Draw content
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }
}

Imaginons une classe Ecran, et disons que l’on souhaite connaitre la surface de l’écran … on pourrait très bien dire que écran implemente Forme :

  • Ecran.java
package fr.mimiz.tries;

public class Ecran implements Forme{


    @Override
        public int surface(){
        return height*width;
    }

    @Override
    public void draw() throws Exception{
                throw new Exception("Screen is not Drawable ...");
    }

}

Or, je pense que l’écran n’a pas besoin de la méthode draw().

Du coup le Principe de ségrégation des interfaces, propose de créer une interface pour chaque méthode (pour chaque utilisation) comme ceci :

  • Surfacable.java
package fr.mimiz.tries;

interface Surfacable {
        public int surface();
}

Et

  • Drawable.java
package fr.mimiz.tries;

interface Drawable {
        public int draw();
}

Et donc nos classes Rectangle et Ecran

  • Rectangle.java
package fr.mimiz.tries;

public class Rectangle implements Surfacable, Drawable{

    private int height;
    private int width;

    @Override
          public int surface(){
        return height*width;
    }

    @Override
    public void draw() {
        // Draw content
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }
}
  • Ecran.java
package fr.mimiz.tries;
public class Ecran implements Surfacable{

    @Override
        public int surface(){
        return height*width;
    }

}

Ca ne change pas grand chose dans cet exemple, mais si on imagine un système plus complexe, on peut facilement imaginer que la création d’interfaces simples permettra une meilleure lisibilité, une plus grande évolutivité et maintenabilité du code.

D : DEPENDENCY INVERSION PRINCIPLE

wikipedia

Il ne faut pas confondre Inversion de dépendance et Injection de dépendance.

Le principe d’inversion de dépendance précise deux points :

  • A. Les modules de haut niveau ne doivent pas dépendre des modules de bas niveaux. Les deux doivent dépendre d’abstractions.
  • B. Les abstractions ne doivent pas dépendre des détails. Les détails doivent dépendre des abstractions.

Ces deux points signifient que si un objet A dépend d’un objet B, pour découpler A et B l’idée est de créer une Interface entre les deux objets. Et faire en sorte que l’interface soit définie par rapport aux besoins de A, plus que par rapport au comportement de B.

Vous pouvez trouver une très bon article ici : http://lostechies.com/derickbailey/2011/09/22/dependency-injection-is-not-the-same-as-the-dependency-inversion-principle/

Conclusion

N’hésitez pas à me laisser vos commentaires et autres remarques.