[DUMD 16/24] La responsabilité unique (Single Responsibility Principle)

Auteur du billet de blog : Nicolas Hilaire - Neotech Solutions

Nicolas Hilaire

Consultant .NET
  Publié le lundi 23 janvier 2017

Artisan logiciel particulièrement intéressé par les technologies .NET. Polyvalent et curieux, je suis néanmoins à l'écoute des autres technologies du marché. MVP (Microsoft Most Valuable Professional) de 2007 à 2014, je suis également auteur d'un ouvrage pour apprendre le C#, à destination des débutants et de plusieurs MOOCs sur le C#, Windows Phone ou ASP.NET MVC...

Seizième billet sur comment devenir un meilleur développeur. Pour retrouver le sommaire ainsi que tous les liens, rendez-vous sur le premier billet.

 

Nous allons maintenant parler du principe de responsabilité unique (le S de SOLID). C'est l'un des principes les plus simples à comprendre, mais c'est aussi un des plus dur à appliquer.

Le mot "responsabilité" est un peu ambigu ; on peut également exprimer ce principe ainsi :

une classe doit avoir une seule et unique raison de changer.

 

Pour les classes

Voyons ceci avec un exemple, imaginez la classe Client suivante :

 

 

Je n'implémente pas le corps des méthodes, mais vous aurez compris que la classe Client est utilisée pour calculer le montant moyen des paniers du client. On peut s'en servir également pour lui envoyer un email. Enfin, nous avons une méthode qui permet de persister le client dans notre système (en base de données par exemple).

Ça vous parait naturel ? C'est certain, et c'est aussi ce que nous apprenons à l'école. Nous avons tendance à grouper les responsabilités au sein de la même classe.

Ici, c'est très simple à constater. Il y a trois méthodes, elles font trois choses vraiment différentes et ces trois choses découlent sans doute fonctionnellement de trois besoins différents. On peut supposer que la première méthode va servir au service achat ou au service marketing. Ces services ont besoin d'informations sur les clients afin de pouvoir les cibler plus correctement et d'augmenter le montant de leur panier moyen. La seconde méthode sera peut-être utilisée par le service de relation clientèle ou par le SAV afin de prendre contact avec le client. Enfin, la dernière est plutôt technique.

Que se passera-t'il si le DBA veut changer le schéma de la base de données ? La méthode Enregistrer est impactée. Et si le service client veut ajouter des fichiers à inclure dans le corps de l'email ? La méthode EnvoyerMail est impactée. Bref, vous avez compris l'idée. Comme le changement va forcément arriver, et que les besoins vont venir de différentes "personnes", il y aura plusieurs raisons différentes de faire évoluer la classe Client. Et si le marketing vous demande une évolution et que vous cassez la méthode du service client, cela va poser un problème. On en revient à un code fragile.

Bien sûr, vous allez me dire que ça n'arrivera pas. Mais vu que tout est dans la même classe, les méthodes utiliseront sans doute des variables membres communes, ou des méthodes privées communes. Bref, des choses qui en évoluant risquent de casser les autres. Alors que jamais nous ne voudrions ça !

Et c'est là que ça devient compliqué : trouver et séparer les responsabilités d'une classe ! Mais c'est tout là le travail de conception.

Cela ne veut pas dire que ce n'est pas à la classe Client de centraliser ça et de déléguer les responsabilités dans des autres classes, par exemple une classe CalculateurDePanierMoyen et une classe qui implémente le design pattern Repository... Avec des abstractions bien sûr ! Il y a plusieurs façons de faire (interface, classe abstraite, composition, etc.) mais ce qui est important c'est d'isoler les parties qui n'ont pas les mêmes raisons de changer.

On pourrait donc proposer un remaniement du code pour avoir :

 

 

Je ne vous fait pas tout, mais vous avez compris le principe. On sépare les responsabilités et on délègue aux classes adéquates.

Ici la classe Client n'a peut-être plus de sens. C'est à vous de voir, peut-être que le calcul du panier moyen peut être utilisé directement depuis la classe de ServiceClient, sans passer par une classe Client.


Donc forcément, quand vous modifierez la classe CalculateurDePanier, vous êtes certains de ne pas impacter l'envoi de mail ! Et ça, c'est quand même super chouette ! :p

Le respect de ce principe a aussi un autre avantage. En isolant les responsabilités, nous allons multiplier les classes et nous allons devoir nous triturer le cerveau pour trouver un nom qui va bien. Et c'est une bonne chose ; cela clarifie l'intention de la responsabilité et cela évite les noms fourre-tout, comme les classes suffixées par Manager ou autre mot du même genre :). (oui je sais, vous l'avez déjà fait, et moi aussi !)

On évite aussi le syndrome des god objects qui ont la fâcheuse tendance de toujours attirer de plus en plus de code.

 

Mais aussi pour les méthodes

Bien sûr, la responsabilité unique doit aussi s'appliquer aux méthodes. Votre méthode doit faire une seule chose et doit la faire bien. Regardez la méthode suivante :

 

 

La méthode fait trois choses ! Vérification de l'email, envoi du mail, log de l'action ...

Vous pourriez au moins dans un premier temps, pour un moindre coût, découper en sous-méthodes :

 

 

A part les expressions régulières qui sont toujours aussi illisibles ^^, sinon le reste est plutôt bien non ?

Et puis vous pouvez aller plus loin en rajoutant des abstractions et en exportant les comportements dans des classes :

 

 

Bon, je sais, je rabâche, mais vous avez compris maintenant, n'est-ce pas ? :)

 

Responsabilité unique et DRY

Je tiens à rajouter une petite mise en garde sur la compatibilité entre le principe DRY et la responsabilité unique. Vous avez compris qu'en bon développeur, vous devez éviter le code dupliqué. On vous a dit que le code dupliqué, c'était mal et qu'il fallait l'éviter à tous prix. C'est souvent vrai sauf quand le code dupliqué appartient à des responsabilités différentes. En effet, si un changement intervient sur une partie du code et que ce code est utilisé à plusieurs endroits différents alors forcément les deux utilisateurs auront les répercutions des changements et ce n'est pas forcément ce qui est souhaité.

Rappelez-vous, c'est un des syndromes d'un code fragile. On fait une modification à un endroit et ça casse complètement un autre endroit, non prévu bien sûr.


Imaginons un client dont on calcule le panier moyen ; on se sert de ce calcul pour faire des rapports sur les clients et des statistiques. Et ce calcul est aussi partagé avec le client dans son espace client. Si pour X raisons, on voulait changer la manière dont on calcule le panier moyen dans nos statistiques pour ajouter une pondération par rapport à la période de l'année, alors l'affichage du client serait également modifié alors que ce n'était pas forcément ce qui était voulu.

Dans des cas comme celui-ci, il est judicieux de dupliquer le code car il a des chances d'évoluer de manières différentes car il n'aura pas les mêmes raisons de changer. Alors oui, le code est peut-être strictement identique, mais ce n'est qu'un hasard par rapport à la responsabilité du code. Si on se rend compte que la mutualisation du code risque d'être une mauvaise idée, alors allez-y. Dupliquez ! Expliquez pourquoi en commentaire, afin de vous en rappeler - et aussi pour ne pas passer pour un mauvais développeur auprès de vos collègues - mais osez le code dupliqué.

 

Conclusion

En suivant le principe de responsabilité unique, nous nous retrouvons avec un code plus robuste, plus facile à lire et à comprendre. Il est également plus facile à tester et à maintenir. Enfin, il sera aussi plus facile à étendre avec de nouvelles fonctionnalités.

Les classes sont plus courtes, les méthodes aussi, il est donc plus difficile de faire des erreurs.

Bien souvent, séparer la classe pour n'avoir que des responsabilités uniques introduit de la complexité et beaucoup de classes supplémentaires. Il n'est pas obligatoire de le faire trop tôt. Un bon indicateur pour savoir quand le faire est dès que la classe change. A ce moment là, vous devez refactorer votre code et appliquer ce principe.

L'injection de dépendances dans le constructeur est également un bon indicateur pour nous aider à identifier les classes ayant trop de responsabilités. Si vous commencez à avoir trop de paramètres dans le constructeur, c'est sans doute que la classe fait trop de choses :).

 

Prochain billet sur le principe ouvert/fermé

Commentaires