Responsabilité unique (SRP), ouverture / fermeture, substitution de Liskov, ségrégation d'interface et inversion de dépendance. Cinq principes agiles qui devraient vous guider chaque fois que vous écrivez du code.
Une classe ne devrait avoir qu'une seule raison de changer.
Défini par Robert C. Martin dans son livre intitulé Développement de logiciels agiles, Principes, modèles et pratiques, puis republié dans la version C # du livre Principes, modèles et pratiques agiles en C #, il fait partie des cinq principes agiles SOLID. Ce qu’il énonce est très simple, mais réaliser cette simplicité peut être très délicat. Une classe ne devrait avoir qu'une seule raison de changer.
Mais pourquoi? Pourquoi est-il si important de n'avoir qu'une seule raison de changer??
Dans les langages statiquement typés et compilés, plusieurs raisons peuvent entraîner plusieurs redéploiements indésirables. S'il y a deux raisons différentes de changer, il est concevable que deux équipes différentes puissent travailler sur le même code pour deux raisons différentes. Chacun devra déployer sa solution, ce qui dans le cas d'un langage compilé (comme C ++, C # ou Java) peut conduire à des modules incompatibles avec d'autres équipes ou d'autres parties de l'application..
Même si vous ne pouvez pas utiliser un langage compilé, vous devrez peut-être refaire le test de la même classe ou du même module pour différentes raisons. Cela signifie plus de travail, de temps et d'effort en matière d'assurance qualité.
Déterminer la responsabilité unique qu'une classe ou un module devrait avoir est beaucoup plus complexe que de simplement regarder une liste de contrôle. Par exemple, un indice pour trouver les raisons de notre changement est d'analyser l'audience de notre classe. Les utilisateurs de l'application ou du système que nous développons et qui sont desservis par un module particulier seront ceux qui demanderont des modifications. Ceux qui seront servis demanderont du changement. Voici quelques modules et leurs publics possibles.
Associer des personnes concrètes à tous ces rôles peut être difficile. Dans une petite entreprise, une seule personne peut avoir besoin de remplir plusieurs rôles, alors que dans une grande entreprise, plusieurs personnes peuvent être affectées à un seul rôle. Il semble donc beaucoup plus raisonnable de penser aux rôles. Mais les rôles en eux-mêmes sont assez difficiles à définir. Qu'est-ce qu'un rôle? Comment le trouvons-nous? Il est beaucoup plus facile d’imaginer des acteurs jouer ces rôles et associer notre public à ces acteurs..
Donc, si notre public définit les raisons du changement, les acteurs définissent le public. Cela nous aide grandement à réduire le concept de personnes concrètes comme "Jean l'architecte" à l'architecture ou "Marie la référence" aux opérations..
Une responsabilité est donc une famille de fonctions qui sert un acteur particulier. (Robert C. Martin)
Au sens de ce raisonnement, les acteurs deviennent une source de changement pour la famille de fonctions qui les sert. À mesure que leurs besoins évoluent, cette famille de fonctions spécifique doit également évoluer pour répondre à leurs besoins..
Un acteur pour une responsabilité est la source unique de changement pour cette responsabilité. (Robert C. Martin)
Disons que nous avons un Livre
classe encapsulant le concept de livre et ses fonctionnalités.
class Book function getTitle () return "Un grand livre"; function getAuthor () return "John Doe"; function turnPage () // pointeur sur la page suivante function printCurrentPage () echo "contenu de la page en cours";
Cela peut ressembler à une classe raisonnable. Nous avons un livre, il peut fournir son titre, son auteur et il peut tourner la page. Enfin, il est également possible d’imprimer la page en cours à l’écran. Mais il y a un petit problème. Si nous pensons aux acteurs impliqués dans le fonctionnement du Livre
objet, qui pourraient-ils être? Nous pouvons facilement imaginer deux acteurs différents ici: la gestion du livre (comme le bibliothécaire) et le mécanisme de présentation des données (comme la façon dont nous voulons transmettre le contenu à l'utilisateur - interface utilisateur graphique, écran, interface texte uniquement, éventuellement impression). . Ce sont deux acteurs très différents.
Mélanger la logique métier avec la présentation est une mauvaise chose car cela va à l'encontre du principe de responsabilité unique (SRP). Regardez le code suivant:
class Book function getTitle () return "Un grand livre"; function getAuthor () return "John Doe"; function turnPage () // pointeur sur la page suivante function getCurrentPage () return "contenu de la page actuelle"; interface Printer function printPage ($ page); La classe PlainTextPrinter implémente Printer function printPage ($ page) echo $ page; La classe HtmlPrinter implémente Printer function printPage ($ page) echo ''. $ page. '';
Même cet exemple très basique montre à quel point la séparation de la présentation de la logique métier et le respect du PRS confèrent de grands avantages à la flexibilité de notre conception..
Un exemple similaire à celui ci-dessus est lorsqu'un objet peut enregistrer et se récupérer à partir de la présentation.
class Book function getTitle () return "Un grand livre"; function getAuthor () return "John Doe"; function turnPage () // pointeur sur la page suivante function getCurrentPage () return "contenu de la page actuelle"; function save () $ filename = '/ documents /'. $ this-> getTitle (). '-' $ this-> getAuthor (); fichier_put_contents ($ filename, serialize ($ this));
Nous pouvons à nouveau identifier plusieurs acteurs tels que Book Management System et Persistence. Chaque fois que nous voulons changer de persistance, nous devons changer cette classe. Chaque fois que nous voulons changer le passage d'une page à l'autre, nous devons modifier cette classe. Il y a plusieurs axes de changement ici.
class Book function getTitle () return "Un grand livre"; function getAuthor () return "John Doe"; function turnPage () // pointeur sur la page suivante function getCurrentPage () return "contenu de la page actuelle"; class SimpleFilePersistence function save (Livre $ livre) $ nomfichier = '/ documents /'. $ book-> getTitle (). '-' $ book-> getAuthor (); fichier_put_contents ($ filename, serialize ($ book));
Déplacer l'opération de persistance dans une autre classe séparera clairement les responsabilités et nous serons libres d'échanger des méthodes de persistance sans affecter notre Livre
classe. Par exemple, implémenter un DatabasePersistence
la classe serait triviale et notre logique d'entreprise construite autour d'opérations avec des livres ne changera pas.
Dans mes articles précédents, j'ai souvent mentionné et présenté le schéma architectural de haut niveau que l'on peut voir ci-dessous..
Si nous analysons ce schéma, vous pouvez voir comment le principe de responsabilité unique est respecté. La création d’objets est séparée à droite dans Usines et le point d’entrée principal de notre application, un acteur, une responsabilité. La persistance est également pris en charge au bas. Un module séparé pour la responsabilité séparée. Enfin, à gauche, nous avons la présentation ou le mécanisme de diffusion si vous le souhaitez, sous la forme d’un MVC ou de tout autre type d’UI. SRP respecté à nouveau. Reste à savoir quoi faire dans notre logique métier..
Lorsque nous pensons au logiciel que nous devons écrire, nous pouvons analyser de nombreux aspects différents. Par exemple, plusieurs exigences affectant la même classe peuvent représenter un axe de changement. Ces axes de changement peuvent être un indice pour une responsabilité unique. Il y a une forte probabilité que des groupes d'exigences affectant le même groupe de fonctions aient des raisons de changer ou d'être spécifiés en premier lieu.
La principale valeur du logiciel est la facilité de changement. Le secondaire est la fonctionnalité, dans le sens de satisfaire le plus possible les exigences, de satisfaire les besoins de l'utilisateur. Cependant, pour atteindre une valeur secondaire élevée, une valeur primaire est obligatoire. Pour que notre valeur principale reste élevée, nous devons avoir une conception qui soit facile à modifier, à étendre, à intégrer de nouvelles fonctionnalités et à garantir le respect du PRS..
On peut raisonner pas à pas:
Ainsi, lorsque nous concevons notre logiciel, nous devrions:
class Book function getTitle () return "Un grand livre"; function getAuthor () return "John Doe"; function turnPage () // pointeur sur la page suivante function getCurrentPage () return "contenu de la page actuelle"; function getLocation () // renvoie la position dans la bibliothèque // ie. numéro d'étagère & numéro de chambre
Maintenant, cela peut sembler parfaitement raisonnable. Nous n'avons aucune méthode traitant de la persistance ou de la présentation. Nous avons notre turnPage ()
fonctionnalité et quelques méthodes pour fournir différentes informations sur le livre. Cependant, nous pouvons avoir un problème. Pour le savoir, nous voudrons peut-être analyser notre application. La fonction getLocation ()
peut être le problème.
Toutes les méthodes du Livre
la classe concerne la logique métier. Notre perspective doit donc être celle de l'entreprise. Si notre application est écrite pour être utilisée par de vrais bibliothécaires qui recherchent des livres et nous donnent un livre physique, il est possible que le SRP soit violé..
On peut penser que les opérations d'acteur sont celles qui s'intéressent aux méthodes getTitle ()
, getAuthor ()
et getLocation ()
. Les clients peuvent également avoir accès à l’application pour sélectionner un livre et lire les premières pages pour se faire une idée du livre et décider s’ils le souhaitent ou non. Ainsi, les lecteurs d'acteur peuvent être intéressés par toutes les méthodes sauf getLocations ()
. Un client ordinaire ne se soucie pas de savoir où le livre est conservé à la bibliothèque. Le livre sera remis au client par le bibliothécaire. Donc, nous avons effectivement une violation du PÉR.
class Book function getTitle () return "Un grand livre"; function getAuthor () return "John Doe"; function turnPage () // pointeur sur la page suivante function getCurrentPage () return "contenu de la page actuelle"; class BookLocator function local (Book $ book) // renvoie la position dans la bibliothèque // ie. numéro d'étagère et numéro de chambre $ libraryMap-> findBookBy ($ book-> getTitle (), $ book-> getAuthor ());
Présentation de la BookLocator
, le bibliothécaire sera intéressé par le BookLocator
. Le client sera intéressé par le Livre
seulement. Bien sûr, il existe plusieurs façons de mettre en œuvre un BookLocator
. Il peut utiliser l'auteur et le titre ou un objet de livre et obtenir les informations requises du Livre
. Cela dépend toujours de nos affaires. Ce qui est important, c’est que si la bibliothèque est changée et que le bibliothécaire devra trouver des livres dans une bibliothèque organisée différemment, le Livre
l'objet ne sera pas affecté. De la même manière, si nous décidons de fournir aux lecteurs un résumé pré-compilé au lieu de les laisser parcourir les pages, cela n’affectera pas le bibliothécaire ni le processus de recherche de la tablette sur laquelle se trouvent les livres..
Toutefois, si notre activité consiste à éliminer le bibliothécaire et à créer un mécanisme de libre-service dans notre bibliothèque, nous pouvons considérer que le PRS est respecté dans notre premier exemple. Les lecteurs sont également nos bibliothécaires, ils doivent aller chercher le livre eux-mêmes, puis le vérifier au moyen du système automatisé. C'est aussi une possibilité. Ce qui est important à retenir ici est que vous devez toujours bien considérer votre entreprise..
Le principe de responsabilité unique doit toujours être pris en compte lorsque nous écrivons du code. La conception des classes et des modules en est fortement affectée, ce qui conduit à une conception à faible couplage avec des dépendances moins nombreuses et plus légères. Mais comme toute pièce de monnaie, elle a deux faces. Il est tentant de concevoir dès le début de notre application en tenant compte de SRP. Il est également tentant d'identifier autant d'acteurs que nous voulons ou dont nous avons besoin. Mais c’est vraiment dangereux - du point de vue de la conception - d’essayer de penser à toutes les parties dès le début. Une prise en compte excessive des PRS peut facilement conduire à une optimisation prématurée et, au lieu d’une meilleure conception, à une solution dispersée dans laquelle les responsabilités claires des classes ou des modules peuvent être difficiles à comprendre..
Donc, chaque fois que vous constatez qu'une classe ou un module commence à changer pour différentes raisons, n'hésitez pas, prenez les mesures nécessaires pour respecter la SRP, mais ne vous y attardez pas car une optimisation précoce peut facilement vous tromper..