JavaRush /Blog Java /Random-FR /Cinq principes de base de la conception de classes (SOLID...
Ve4niY
Niveau 14

Cinq principes de base de la conception de classes (SOLID) en Java

Publié dans le groupe Random-FR
Les classes sont les blocs à partir desquels une application est construite. Tout comme les briques d'un immeuble. Des cours mal écrits peuvent causer des problèmes un jour. Cinq principes de base de la conception de classes (SOLID) en Java - 1Pour comprendre si un cours est rédigé correctement, vous pouvez consulter les « normes de qualité ». En Java, ce sont les principes dits SOLID. Parlons d'eux.

Principes SOLID en Java

SOLID est un acronyme formé des lettres majuscules des cinq premiers principes de la POO et du design. Les principes ont été inventés par Robert Martin au début des années 2000, et l'acronyme a ensuite été inventé par Michael Feathers. Voici ce que comprennent les principes SOLID :
  1. Principe de responsabilité unique.
  2. Principe Ouvert Fermé.
  3. Principe de substitution de Liskov.
  4. Principe de ségrégation 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 pour changer de classe. Chaque objet a une responsabilité, entièrement encapsulée dans une classe. Tous les services de classe visent à assurer cette responsabilité. De telles classes seront toujours faciles à modifier si nécessaire, car il est clair de quoi la classe est responsable et de ce qu'elle ne l'est pas. Autrement dit, il sera possible d'apporter des modifications sans avoir peur des conséquences - l'impact sur d'autres objets. Et un tel code est beaucoup plus facile à tester, car vous couvrez une fonctionnalité avec des tests isolés de toutes les autres. Imaginez un module qui traite les commandes. Si la commande est correctement formée, il l'enregistre dans la 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 to the database

        return true;
    }

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

        // Sending a letter to the client
    }
}
Un tel module peut évoluer pour trois raisons. Premièrement, la logique de traitement de la commande peut être différente, deuxièmement, la méthode de sauvegarde (type de base de données), troisièmement, la méthode d'envoi d'une lettre de confirmation (par exemple, au lieu d'un e-mail, vous devez envoyer un SMS). 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 appartenir à des classes ou des modules différents. La combinaison de plusieurs entités susceptibles de changer à différents moments et pour différentes raisons est considérée comme une mauvaise décision de conception. Il est préférable de diviser le 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 to the database

        return true;
    }
}

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

        // Sending a letter to the client
    }
}

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 succinctement décrit comme suit : les entités logicielles (classes, modules, fonctions, etc.) doivent être ouvertes à l'extension, mais fermées au changement . Cela signifie qu'il devrait être possible de modifier le comportement externe d'une classe sans apporter de modifications physiques à la classe elle-même. Suivant ce principe, les classes sont développées de telle sorte que pour ajuster la classe à des conditions d'application spécifiques, il suffit de l'étendre et de redéfinir certaines fonctions. Le système doit donc être flexible, capable de fonctionner dans des conditions variables sans modifier le code source. Poursuivant notre exemple de commande, disons que nous devons effectuer certaines actions avant que la commande ne soit traitée et après l'envoi de l'e-mail de confirmation. Au lieu de changer la classe elle-même OrderProcessor, nous allons l'étendre et parvenir à une solution au problème posé sans violer le principe OCP :
public class OrderProcessorWithPreAndPostProcessing extends OrderProcessor {

    @Override
    public void process(Order order) {
        beforeProcessing();
        super.process(order);
        afterProcessing();
    }

    private void beforeProcessing() {
        // Perform some actions before processing the order
    }

    private void afterProcessing() {
        // Perform some actions after order processing
    }
}

Principe de substitution de Barbara Liskov (LSP)

Il s’agit d’une variante du principe ouvert/fermé évoqué précédemment. Cela peut être décrit ainsi : les objets d'un programme peuvent être remplacés par leurs héritiers sans changer les propriétés du programme. Cela signifie qu'une classe développée en étendant une classe de base doit remplacer ses méthodes de manière à ne pas interrompre la fonctionnalité 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 de manière à ne pas interrompre la fonctionnalité du point de vue du client. Cela peut être examiné en détail à l’aide de l’exemple suivant. Supposons que nous ayons une classe responsable de la validation des commandes et vérifiant si tous les articles commandés sont en stock. Cette classe a une méthode isValidqui renvoie 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 : vérifiez si toutes les marchandises de la commande sont en stock et si toutes les marchandises sont emballées. Pour ce faire, nous avons étendu la classe OrderStockValidatoravec la classe OrderStockAndPackValidator:
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;
    }
}
Cependant, dans cette classe, nous avons violé le principe LSP, car au lieu de renvoyer false si la commande n'a pas réussi la validation, notre méthode lève une exception IllegalStateException. Les clients de ce code ne s'attendent pas à cela : ils s'attendent à ce que true ou false soit renvoyé . Cela peut entraîner des erreurs dans le programme.

Principe de partage d'interface (ISP)

Caractérisé par la déclaration suivante : Les clients ne doivent pas être obligés d’implémenter des méthodes qu’ils n’utiliseront pas . Le principe de séparation des interfaces suggère que les interfaces trop « épaisses » doivent être divisées en interfaces plus petites et plus spécifiques, afin que les clients des petites interfaces ne connaissent que les méthodes nécessaires à leur travail. Par conséquent, lors du changement d’une méthode d’interface, les clients qui n’utilisent pas cette méthode ne doivent pas changer. Regardons un exemple. Le développeur Alex a créé l'interface « rapport » et ajouté deux méthodes : generateExcel()et generatedPdf(). Le client A souhaite désormais utiliser cette interface, mais il a uniquement l'intention d'utiliser des rapports PDF et non Excel. Sera-t-il satisfait de cette fonctionnalité ? Non. Il devra mettre en œuvre deux méthodes, dont l'une est largement inutile et n'existe que grâce à Alex, le concepteur du logiciel. Le client utilisera une interface différente ou laissera le champ Excel vide. Alors quelle est la solution ? Elle consiste à diviser l’interface existante en deux plus petites. L'un est un rapport au format PDF, le second est un rapport au format Excel. Cela donnera à l'utilisateur la possibilité d'utiliser uniquement les fonctionnalités qui lui sont nécessaires.

Principe d'inversion de dépendance (DIP)

Ce principe SOLID en Java 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 sont indépendants des modules de niveau inférieur. Les abstractions ne devraient pas dépendre de 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 se connectent les uns aux autres par abstraction. Une application classique de ce principe est le framework Spring. Dans le framework Spring, 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 d'autres modules logiciels que le framework Spring. Ceci est réalisé grâce à la dépendance de principes fermés et ouverts. Tous les modules donnent accès uniquement à une abstraction pouvant être utilisée dans un autre module. Essayons de démontrer cela avec un exemple. Parlant du principe de responsabilité exclusive, nous en avons considéré quelques-uns OrderProcessor. Regardons à nouveau 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, le nôtre OrderProcessordépend de deux classes spécifiques MySQLOrderRepositoryet ConfirmationEmailSender. Nous présentons également le code de ces classes :
public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // save the order to the database

        return true;
    }
}

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

        // Sending a letter to the client
    }
}
Ces classes sont loin d’être appelées abstractions. Et du point de vue du principe DIP, il serait plus correct de commencer par créer des abstractions qui nous permettront de fonctionner avec elles à l'avenir, plutôt qu'avec des implémentations spécifiques. Créons deux interfaces MailSenderet OrderRepository, qui deviendront nos abstractions :
public interface MailSender {
    void sendConfirmationEmail(Order order);
}

public interface OrderRepository {
    boolean save(Order order);
}
Implémentons maintenant ces interfaces dans des classes déjà prêtes pour cela :
public class ConfirmationEmailSender implements MailSender {

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

        // Sending a letter to the client
    }

}

public class MySQLOrderRepository implements OrderRepository {

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

        return true;
    }
}
Nous avons fait le travail préparatoire pour que notre cours OrderProcessorne dépende pas de détails concrets, mais d'abstractions. Apportons-y des modifications en introduisant nos dépendances dans le 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);
        }
    }
}
Notre classe dépend désormais d'abstractions plutôt que d'implémentations concrètes. Vous pouvez facilement modifier son comportement en injectant la dépendance souhaitée au moment de la création de l'instance OrderProcessor. Nous avons examiné SOLID - principes de conception en Java. En savoir plus sur la POO en général, les bases de ce langage de programmation - pas ennuyeux et avec des centaines d'heures de pratique - dans le cours JavaRush. Il est temps de résoudre certains problèmes :) Cinq principes de base de la conception de classes (SOLID) en Java - 2
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION