Aller au contenu

En quête du principe de responsabilité unique

Le principe de responsabilité unique (Single Responsibility Principle) est l'un des principes SOLID. Il reste flou pour bon nombre de développeurs, et pour cause : il a évolué avec le temps ! Découvrons ensemble ce qu'il en est aujourd'hui.

Le principe de responsabilité unique a évolué au fil du temps. Coup de projecteur sur ce concept .

Parmi les définitions suivantes de ce principe, quelle est celle qui est correcte ?

A) Une classe ou une fonction doit avoir une et une seule raison de changer. Donc une classe doit avoir uniquement une responsabilité.

B) Chaque classe doit avoir une responsabilité unique : elle doit avoir un unique but dans le système et il ne doit y avoir qu'une seule raison de la changer.

C) Il faut regrouper les éléments qui changent pour les mêmes raisons. Séparer ceux qui changent pour des raisons différentes.

D) Séparer le code qui supporte différents acteurs.

Une idée ? Prenons l'avis du public.

A) Une classe ou une fonction doit avoir une seule et unique raison de changer, ce qui signifie qu'une classe doit avoir uniquement une responsabilité.

Voici quelques personnalités et ressources qui soutiennent cette définition :

Cette définition est l'une des premières (si ce n'est LA première ? 🤨) qui a été énoncée concernant ce principe, dans les années 1990 par Robert C Martin. On énonce qu'une classe ou une fonction ne doit avoir qu'une seule responsabilité. Il faut donc que le développeur se pose la question "Quelle est la responsabilité de ma classe ?".

Cette question peut donner lieu à débat. Pourquoi ? Tout simplement parce qu'il est difficile de déterminer jusqu'où il est nécessaire de découper le code. La responsabilité est une notion très vague : "effectuer une transaction", ou bien "effectuer des débits", ou "effectuer des crédits", ou "calculer le découvert autorisé en amont d'une transaction". Comment découper ?

Quant à la raison de changer, on comprend intuitivement l'esprit qu'il y a derrière cette assertion : Si une entité n'a qu'une seule raison de changer, alors il n'existera qu'une seule cause à la modification de celle-ci et il deviendra facile de maintenir une entité, mais la question se pose encore : comment déterminer la raison de changer ? Cette notion peut paraître floue, ou subjective.

B) Chaque classe doit avoir une responsabilité unique : elle doit avoir un unique but dans le système et il ne doit y avoir qu'une seule raison de la changer.

Voici les membres du public qui optent pour cette définition.

  • Mickael Feathers ("Working Effectively with Legacy Code" - 2004)

Dans cette définition, on ne parle que de "classe", et on ne parle plus de "fonction". On peut constater que la notion de "responsabilité" se définit par le fait d'avoir un unique but dans le système.

Cependant, là encore la notion de "but" peut paraître floue toujours pour la même raison : comment savoir si la classe est trop ou pas assez découpée ? Le but d'une classe peut être de "jouer de la musique", "jouer un morceau en particulier", ou "jouer d'un instrument spécifique".

Concernant la raison de changer qui demeure une notion centrale de ce principe, elle reste également subjective et sujette à interprétation.

C) Il faut regrouper les éléments qui changent pour les mêmes raisons. Séparer ceux qui changent pour des raisons différentes

Robert C. Martin y su legado: Los principios SOLID
Que vois-je ? Un membre du public vient de changer son vote !
  • Robert C Martin ("The Single Responsibility Principle" - 2014)

Ici, on ne parle plus de responsabilité, mais uniquement de raison de changer. On ne parle plus de notions concrètes (classes, fonctions, ...) mais "d'éléments". Robert C Martin tente de clarifier le principe. On voit désormais qu'un élément peut avoir plusieurs raisons de changer à partir du moment où ces raisons sont exactement les mêmes que d'autres éléments du système. Si tel est le cas, ces éléments doivent être regroupés, on crée ainsi des catégories d'éléments (des classes d'éléments au sens "logique").

Cela va également dans le sens "Séparer ceux qui changent pour des raisons différentes".

En d'autres termes : tous les éléments qui ont exactement les mêmes raisons de changer doivent être regroupés.

Un flou demeure cependant : qu'est-ce qu'une raison de changer ?

D) Séparer le code qui supporte différents acteurs

Libros de Desarrollo #1: Clean Code de Robert C Martin
Décidemment, l'oncle Bob change son vote une nouvelle fois !
  • Robert C Martin ("Clean Architecture" - 2017)

Le principe devient désormais bien plus concret, moins flou. On ne parle plus de classes, ou de fonctions, mais de code. La notion est donc générique. Robert C Martin suggère désormais de séparer le code par "acteurs" du système, par responsabilité du système.

Un acteur d'un système n'est pas une personne, mais bien un rôle, ou une responsabilité.

Par exemple, dans une boulangerie vous avez plusieurs acteurs différents . Pour n'en citer que quelques-uns : le client, le boulanger, le caissier.

Robert C Martin nous dit que le code qui concerne le client (passer une commande, payer, déposer une réclamation, ...) ne doit pas être mélangé avec le code qui concerne par exemple le boulanger (cuire la baguette, allumer le four, etc...)

Pourquoi ? Tout simplement pour que l'évolution du besoin d'un acteur du système n'impacte pas les autres acteurs du système, et que ce besoin soit techniquement indépendant des autres besoins. Cela cloisonne les besoins, et permet donc également de cloisonner les risques. Retenons donc qu'il est important de bien définir les acteurs du système en amont du développement !

Il existe des design patterns favorisant ce principe (exemple : Stratégie )

Un petit exercice pour illustrer tout cela

Dans une application, nous souhaitons enregistrer des entraînements sportifs. Voici le code proposé :

    public class Training {

        private int minutes;
        private int calories;
        
        private static final List<Training> HISTORY = new ArrayList<>();;

        private static final int COEFF_EFFORT = 10;
        Training() {
            this.minutes = 0;
            this.calories = 0;
        }

        public void go(int minutes){
            this.minutes += minutes;
            this.calories += minutes*COEFF_EFFORT;
        }

        public void displaySummary(){
            System.out.println(STR."\{minutes} minutes -> \{calories} calories");
        }
        
        public static void displayTrainings(){
            HISTORY.forEach(Training::displaySummary);
        }
        
        public void registerTraining(){
            HISTORY.add(this);
        }
    }

Est-ce que ce code viole le principe SRP ?

Selon A), oui car notre classe a plusieurs raisons de changer : Evolution de la notion d'entrainement, modification de la manière d'enregistrer (par exemple, on n'enregistrerait pas les entrainements trop courts), mise à jour de la manière d'afficher un entrainement...

Selon B), oui car notre classe a plusieurs "buts" : lancer un entrainement, afficher un entrainement, maintenir l'historique des entrainements

Selon C), oui et non, mais oui : "regrouper les choses qui changent pour les mêmes raisons" -> Notre classe le fait bien car tout est dans la même classe, "Séparer ceux qui changent pour des raisons différentes" -> En revanche, ce n'est pas le cas, et on rejoint A)

Selon D), woops ! On a oublié de définir les acteurs du système !

On pourrait par exemple identifier les acteurs suivants (rappel : ce ne sont pas forcément des humains !) :

  • Le sportif : C'est celui qui court, et qui transpire 😄
  • Le scribe : C'est celui qui note toutes les sessions d'entrainement
  • La montre connectée : c'est celui qui va afficher le résumé de l'entrainement

En partant de ces acteurs, on comprend alors qu'il faut séparer la classe en 3 classe distinctes. Chacune portant un acteur. Ainsi, si le besoin d'un acteur change (par exemple, le scribe ne conserve que les 10 derniers entrainements), les autres acteurs ne seront pas impactés !

Et au final ?

Le principe a évolué et continuera probablement d'évoluer avec le temps. Bien que sa formulation a longtemps été un casse-tête, son objectif est principalement de regrouper et confiner tout ce qui "va ensemble" pour limiter le nombre de bugs et faciliter la maintenance potentielle.

Pour avoir une petite introduction aux principes SOLID, c'est par ici

Pour aller plus loin

Dernier