Aller au contenu

Principe de responsabilité unique : les niveaux d'abstraction

Une fonction ne doit faire qu’une seule chose, mais comment savoir si c’est vraiment le cas ? À travers un exemple concret, découvrons comment appliquer le Single Responsibility Principle en respectant les niveaux d’abstraction pour un code plus clair et maintenable.

Les niveaux d'abstraction. Generated by RAISE

L’un des principes SOLID les plus importants est le Single Responsibility Principle (SRP), ou Principe de Responsabilité Unique en français.D’après Robert C. Martin (Uncle Bob) dans son livre Clean Code, une fonction ne doit avoir qu’une seule responsabilité.En lisant ce livre, je suis tombé sur un cas où l’auteur appliquait des techniques de refactorisation à un code existant, afin de rendre une fonction aussi simple et claire que possible.Pour lui, après refactorisation, cette fonction ne faisait plus qu’une seule chose. Voici la fonction :

public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) {
    if (isTestPage(pageData)){
        includeSetupAndTeardownPages(pageData, isSuite);
    }
    return pageData.getHtml();
}

Analysons la fonction

En examinant le code, on peut voir que cette fonction exécute en réalité trois actions distinctes :

  • Vérifie si la page est une page de test ;
  • Si c’est le cas, elle inclut les setups et teardowns ;
  • Retourne le HTML de la page.

Les niveaux d’abstraction

À partir de là, l’auteur introduit la notion de niveaux d’abstraction dans le Single Responsibility Principle (SRP). Il affirme que :

“Une fonction ne fait qu’une seule chose si toutes les instructions qu’elle contient sont au même niveau d’abstraction.”

Je dois avouer que, même après avoir relu cette phrase plusieurs fois, je n’arrivais pas à comprendre comment cette fonction ne faisait qu’une seule chose… Jusqu’à ce que je décide de mettre la main à la pâte. Et là, tout a pris sens !Prenons un exemple fictif pour mieux comprendre ce principe.

Exemple : traitement d’une approbation d’élève

Imaginons que dans un système de gestion d'une école, nous avons une fonction qui est responsable du traitement de l’approbation des élèves. Ce traitement consiste à mettre à jour les informations de l’élève et à envoyer un e-mail de notification.Voici à quoi ressemble la fonction initiale:

public void traiterApprobation(EtudiantDTO dto) {
    Etudiant etudiant = repository.findBy(dto.getMatricule());
    etudiant.setNote(dto.getNote());
    etudiant.setApprouve(true);
    repository.save(etudiant);

    String contenu = new StringBuilder()
            .append("Bonjour !\n\n Nous vous informons que l'étudiant ")
            .append(dto.getNomComplet())
            .append(" a réussi avec la note de ")
            .append(dto.getNote())
            .toString();

    Email email = new Email.EmailBuilder()
            .destinataires(singletonList(dto.getEmailDuResponsable()))
            .destinatairesEnCopie(singletonList(dto.getEmail()))
            .contenu(contenu)
            .build();

    mailSender.send(email);
}

Si on regarde bien, cette fonction exécute plusieurs actions différentes. Pour mieux les identifier, ajoutons des commentaires pour les séparer :

public void traiterApprobation(EtudiantDTO dto) {
    //Actualiser les données de l'étudiant
    Etudiant etudiant = repository.findBy(dto.getMatricule());
    etudiant.setNote(dto.getNote());
    etudiant.setApprouve(true);
    //Sauvegarder à la dataBase
    repository.save(etudiant);

    //Monter le corps du mail qui sera envoyé
    String contenu = new StringBuilder()
            .append("Bonjour !\n\n Nous vous informons que l'étudiant ")
            .append(dto.getNomComplet())
            .append(" a réussi avec la note de ")
            .append(dto.getNote())
            .toString();

    //Créer le mail avec les destinataires et le contenu
    Email email = new Email.EmailBuilder()
            .destinataires(singletonList(dto.getEmailDuResponsable()))
            .destinatairesEnCopie(singletonList(dto.getEmail()))
            .contenu(contenu)
            .build();

    //Envoyer le mail
    mailSender.send(email);
}

À ce stade, il est évident que notre fonction fait plusieurs choses en même temps. Mais la vraie question est : perçois-tu les différents niveaux d’abstraction qu’elle contient ? L’objectif de cette fonction est de traiter l’approbation.

"Le traitement de l’approbation consiste à mettre à jour les données de l’élève et à envoyer un e-mail pour informer qu’il a été approuvé."

Donc, l’envoi d’un e-mail fait bien partie des responsabilités de cette fonction. Mais, par contre, décider si l’e-mail doit commencer par "Bonjour" ou "Salut" est un détail d’implémentation dont cette fonction ne devrait pas se préoccuper. Ce genre de décision est à un niveau d’abstraction plus bas.

Séparation des niveaux d’abstraction

L’idée maintenant est de séparer les différents niveaux d’abstraction en créant des fonctions dédiées.En appliquant la technique de l’extraction de méthodes, nous pouvons commencer par déplacer la mise à jour des données de l’élève dans une fonction spécifique :

private void mettreAJourDonneesEtudiant(final EtudiantDTO dto) {
    Etudiant etudiant = repository.findBy(dto.getMatricule());
    etudiant.setNote(dto.getNote());
    etudiant.setApprouve(true);
    repository.save(etudiant);
}

Ensuite, nous appliquons la même technique pour l’envoi de l’e-mail, en le séparant en plusieurs niveaux d’abstraction :

private void envoyerMailApprobation(final EtudiantDTO dto) {
    mailSender.send(creerMailApprobation(dto));
}

private Email creerMailApprobation(final EtudiantDTO dto) {
    return new Email.EmailBuilder()
            .destinataires(singletonList(dto.getEmailDuResponsable()))
            .destinatairesEnCopie(singletonList(dto.getEmail()))
            .contenu(genererContenuMailApprobation(dto))
            .build();
}

private String genererContenuMailApprobation(final EtudiantDTO dto) {
    return new StringBuilder()
            .append("Bonjour !\n\n Nous vous informons que l'étudiant ")
            .append(dto.getNomComplet())
            .append(" a réussi avec la note de ")
            .append(dto.getNote())
            .toString();
}

Remarque : j’ai créé une fonction pour assembler l’e-mail (ajout des destinataires et du contenu) et une autre fonction uniquement pour générer le contenu de l’e-mail. Pourquoi ? Parce que ces deux tâches sont à des niveaux d’abstraction différents.

Résultat final

Après ces modifications, voici à quoi ressemble notre fonction principale :

public void traiterApprobation(final EtudiantDTO dto) {
    mettreAJourDonneesEtudiant(dto);
    envoyerMailApprobation(dto);
}

Si je te demande : "Combien de choses fait cette fonction ?", tu pourrais répondre :

"Deux choses : elle met à jour les données de l’élève et envoie un e-mail."

Si c’est ta réponse, tu n’as ni totalement tort, ni totalement raison hahaha. Reprenons la définition de notre fonction :

"Le traitement de l’approbation consiste à mettre à jour les données de l’élève et à envoyer un e-mail de notification."

Autrement dit, mettre à jour les données et envoyer un e-mail sont toutes, au même niveau d’abstraction. On peut donc conclure que notre fonction fait bien une seule chose : Elle traite l’approbation des élèves.

Dernier