CodeGym /Blog Java /Random-FR /SOLID : cinq principes de base de la conception de classe...
Auteur
Jesse Haniel
Lead Software Architect at Tribunal de Justiça da Paraíba

SOLID : cinq principes de base de la conception de classes en Java

Publié dans le groupe Random-FR
Les classes sont les blocs de construction des applications. Comme des briques dans un immeuble. Des cours mal écrits peuvent éventuellement causer des problèmes. SOLID : cinq principes de base de la conception de classes en Java - 1Pour comprendre si une classe est correctement écrite, vous pouvez vérifier comment elle se compare aux "normes de qualité". En Java, ce sont les principes dits SOLID, et nous allons en parler.

Principes SOLID en Java

SOLID est un acronyme formé à partir des majuscules des cinq premiers principes de la POO et de la conception de classe. Les principes ont été exprimés par Robert Martin au début des années 2000, puis l'abréviation a été introduite plus tard par Michael Feathers. Voici les principes SOLID :
  1. Principe de responsabilité unique
  2. Ouvert Fermé Principe
  3. Principe de substitution de Liskov
  4. Principe de séparation des interfaces
  5. Principe d'inversion de dépendance

Principe de responsabilité unique (SRP)

Ce principe stipule qu'il ne devrait jamais y avoir plus d'une raison de changer de classe. Chaque objet a une responsabilité, qui est entièrement encapsulée dans la classe. Tous les services d'une classe visent à soutenir cette responsabilité. De telles classes seront toujours faciles à modifier si nécessaire, car il est clair de quoi la classe est et n'est pas responsable. En d'autres termes, nous pourrons apporter des modifications et ne pas avoir peur des conséquences, c'est-à-dire de l'impact sur d'autres objets. De plus, un tel code est beaucoup plus facile à tester, car vos tests couvrent une fonctionnalité isolée de toutes les autres. Imaginez un module qui traite les commandes. Si une commande est correctement formée, ce module l'enregistre dans une base de données et envoie un email pour confirmer la commande :

public class OrderProcessor {

    public void process(Order order){
        if (order.isValid() && save(order)) {
            sendConfirmationEmail(order);
        }
    }

    private boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }

    private void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}
Ce module pourrait changer pour trois raisons. Premièrement, la logique de traitement des commandes peut changer. Deuxièmement, la façon dont les commandes sont enregistrées (type de base de données) peut changer. Troisièmement, la façon dont la confirmation est envoyée peut changer (par exemple, supposons que nous devions envoyer un SMS plutôt qu'un e-mail). Le principe de responsabilité unique implique que les trois aspects de ce problème sont en réalité trois responsabilités différentes. Cela signifie qu'ils doivent être dans des classes ou des modules différents. Combiner plusieurs entités qui peuvent changer à des moments différents et pour des raisons différentes est considéré comme une mauvaise décision de conception. Il est préférable de diviser un module en trois modules distincts, chacun remplissant une seule fonction:

public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}

public class ConfirmationEmailSender {
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}

public class OrderProcessor {
    public void process(Order order){

        MySQLOrderRepository repository = new MySQLOrderRepository();
        ConfirmationEmailSender mailSender = new ConfirmationEmailSender();

        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }

}

Principe Ouvert Fermé (OCP)

Ce principe est décrit comme suit : les entités logicielles (classes, modules, fonctions, etc.) doivent être ouvertes à l'extension, mais fermées à la modification. Cela signifie qu'il devrait être possible de modifier le comportement externe d'une classe sans apporter de modifications au code existant de la classe. Selon ce principe, les classes sont conçues de sorte que modifier une classe pour l'adapter à des conditions spécifiques nécessite simplement de l'étendre et de remplacer certaines fonctions. Cela signifie que le système doit être flexible, capable de fonctionner dans des conditions changeantes sans changer le code source. Poursuivant notre exemple impliquant le traitement des commandes, supposons que nous devions effectuer certaines actions avant le traitement d'une commande ainsi qu'après l'envoi de l'e-mail de confirmation. Au lieu de changer leOrderProcessorclasse elle-même, nous l'étendrons pour atteindre notre objectif sans violer le principe ouvert fermé:

public class OrderProcessorWithPreAndPostProcessing extends OrderProcessor {

    @Override
    public void process(Order order) {
        beforeProcessing();
        super.process(order);
        afterProcessing();
    }
    
    private void beforeProcessing() {
        // Take some action before processing the order
    }
    
    private void afterProcessing() {
        // Take some action after processing the order
    }
}

Principe de substitution de Liskov (LSP)

Il s'agit d'une variante du principe ouvert fermé que nous avons mentionné précédemment. Il peut être défini comme suit : les objets peuvent être remplacés par des objets de sous-classes sans modifier les propriétés d'un programme. Cela signifie qu'une classe créée en étendant une classe de base doit remplacer ses méthodes afin que la fonctionnalité ne soit pas compromise du point de vue du client. Autrement dit, si un développeur étend votre classe et l'utilise dans une application, il ne doit pas modifier le comportement attendu des méthodes remplacées. Les sous-classes doivent remplacer les méthodes de la classe de base afin que la fonctionnalité ne soit pas interrompue du point de vue du client. Nous pouvons explorer cela en détail dans l'exemple suivant. Supposons que nous ayons une classe chargée de valider une commande et de vérifier si toutes les marchandises de la commande sont en stock.isValid()méthode qui retourne true ou false :

public class OrderStockValidator {

    public boolean isValid(Order order) {
        for (Item item : order.getItems()) {
            if (!item.isInStock()) {
                return false;
            }
        }

        return true;
    }
}
Supposons également que certaines commandes doivent être validées différemment des autres, par exemple pour certaines commandes, nous devons vérifier si toutes les marchandises de la commande sont en stock et si toutes les marchandises sont emballées. Pour ce faire, nous étendons la OrderStockValidatorclasse en créant la OrderStockAndPackValidatorclasse :

public class OrderStockAndPackValidator extends OrderStockValidator {

    @Override
    public boolean isValid(Order order) {
        for (Item item : order.getItems()) {
            if ( !item.isInStock() || !item.isPacked() ){
                throw new IllegalStateException(
                     String.format("Order %d is not valid!", order.getId())
                );
            }
        }

        return true;
    }
}
Mais ici, nous avons violé le principe de substitution de Liskov, car au lieu de retourner false si la commande échoue à la validation, notre méthode lance un IllegalStateException. Les clients utilisant ce code ne s'attendent pas à cela : ils attendent une valeur de retour true ou false . Cela peut entraîner des erreurs d'exécution.

Principe de ségrégation d'interface (ISP)

Ce principe est caractérisé par la déclaration suivante : le client ne doit pas être contraint d'implémenter des méthodes qu'il n'utilisera pas . Le principe de ségrégation des interfaces signifie que les interfaces trop "épaisses" doivent être divisées en interfaces plus petites et plus spécifiques, afin que les clients utilisant de petites interfaces ne connaissent que les méthodes dont ils ont besoin pour leur travail. Par conséquent, lorsqu'une méthode d'interface change, les clients qui n'utilisent pas cette méthode ne doivent pas changer. Prenons cet exemple : Alex, un développeur, a créé une interface "rapport" et ajouté deux méthodes : generateExcel()etgeneratedPdf(). Maintenant, un client souhaite utiliser cette interface, mais n'a l'intention d'utiliser que des rapports au format PDF, et non dans Excel. Cette fonctionnalité satisfera-t-elle ce client ? Non. Le client devra implémenter deux méthodes, dont l'une est largement inutile et n'existe que grâce à Alex, celui qui a conçu le logiciel. Le client utilisera une interface différente ou ne fera rien avec la méthode pour les rapports Excel. Alors, quelle est la solution? Il s'agit de diviser l'interface existante en deux plus petites. L'un pour les rapports PDF, l'autre pour les rapports Excel. Cela permet aux clients d'utiliser uniquement les fonctionnalités dont ils ont besoin.

Principe d'inversion de dépendance (DIP)

En Java, ce principe SOLID est décrit comme suit : les dépendances au sein du système sont construites sur la base d'abstractions. Les modules de niveau supérieur ne dépendent pas des modules de niveau inférieur. Les abstractions ne doivent pas dépendre des détails. Les détails doivent dépendre des abstractions. Le logiciel doit être conçu de manière à ce que les différents modules soient autonomes et connectés les uns aux autres par abstraction. Une application classique de ce principe est le Spring Framework. Dans Spring Framework, tous les modules sont implémentés en tant que composants distincts pouvant fonctionner ensemble. Ils sont si autonomes qu'ils peuvent être utilisés tout aussi facilement dans des modules de programme autres que Spring Framework. Ceci est réalisé grâce à la dépendance des principes fermé et ouvert. Tous les modules donnent accès uniquement à l'abstraction, qui peut être utilisée dans un autre module. Essayons d'illustrer cela à l'aide d'un exemple. Parlant du principe de responsabilité unique, nous avons considéré leOrderProcessorclasse. Reprenons le code de cette classe:

public class OrderProcessor {
    public void process(Order order){

        MySQLOrderRepository repository = new MySQLOrderRepository();
        ConfirmationEmailSender mailSender = new ConfirmationEmailSender();

        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }

}
Dans cet exemple, notre OrderProcessorclasse dépend de deux classes spécifiques : MySQLOrderRepositoryet ConfirmationEmailSender. Nous allons également présenter le code de ces classes :

public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}

public class ConfirmationEmailSender {
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}
Ces classes sont loin de ce que nous appellerions des abstractions. Et du point de vue du principe d'inversion des dépendances, il serait préférable de commencer par créer des abstractions avec lesquelles nous pourrons travailler à l'avenir, plutôt que des implémentations spécifiques. Créons deux interfaces : MailSenderet OrderRepository). Ce seront nos abstractions :

public interface MailSender {
    void sendConfirmationEmail(Order order);
}

public interface OrderRepository {
    boolean save(Order order);
}
Maintenant, nous implémentons ces interfaces dans des classes qui ont déjà été préparées pour cela :

public class ConfirmationEmailSender implements MailSender {

    @Override
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }

}

public class MySQLOrderRepository implements OrderRepository {

    @Override
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}
Nous avons fait le travail préparatoire pour que notre OrderProcessorclasse repose, non sur des détails concrets, mais sur des abstractions. Nous allons le changer en ajoutant nos dépendances au constructeur de classe :

public class OrderProcessor {

    private MailSender mailSender;
    private OrderRepository repository;

    public OrderProcessor(MailSender mailSender, OrderRepository repository) {
        this.mailSender = mailSender;
        this.repository = repository;
    }

    public void process(Order order){
        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }
}
Maintenant, notre classe dépend d'abstractions, pas d'implémentations spécifiques. Nous pouvons facilement modifier son comportement en ajoutant la dépendance souhaitée au moment de OrderProcessorla création d'un objet. Nous avons examiné les principes de conception SOLID en Java. Vous en apprendrez plus sur la POO en général et les bases de la programmation Java - rien d'ennuyeux et des centaines d'heures de pratique - dans le cours CodeGym. Il est temps de résoudre quelques tâches :)
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION