SOLID Partie 4 - Le principe d'inversion de dépendance

La responsabilité unique, ouverte / fermée (OCP), la substitution de Liskov, la séparation des interfaces et Inversion de dépendance. Cinq principes agiles qui devraient vous guider chaque fois que vous écrivez du code.

Il serait injuste de vous dire que l’un quelconque des principes SOLID est plus important qu’un autre. Cependant, aucun des autres n'a probablement un effet aussi immédiat et profond sur votre code que le principe d'inversion de dépendance, ou DIP en bref. Si vous avez du mal à comprendre ou à appliquer les autres principes, commencez par celui-ci et appliquez le reste sur du code qui respecte déjà le DIP.

Définition

A. Les modules de haut niveau ne doivent pas dépendre de modules de bas niveau. Les deux devraient dépendre d'abstractions.
B. Les abstractions ne doivent pas dépendre des détails. Les détails devraient dépendre des abstractions.

Ce principe a été défini par Robert C. Martin dans son livre Développement de logiciels agiles - Principes, modèles et pratiques, puis republié dans la version C # du livre «Principes, modèles et pratiques Agile en C #». Il s'agit du dernier des cinq Principes agiles solides.

DIP dans le monde réel

Avant de commencer à coder, j'aimerais vous raconter une histoire. Chez Syneto, nous n’avons pas toujours été aussi prudents avec notre code. Il y a quelques années, nous en savions moins et même si nous avons essayé de faire de notre mieux, tous nos projets n'étaient pas aussi agréables. Nous sommes passés par l'enfer et retour encore et nous avons appris beaucoup de choses par essais et erreurs.

Les principes SOLID et les principes d’architecture propre d’Oncle Bob (Robert C. Martin) ont changé la donne pour nous et ont transformé notre façon de coder de manière difficile à décrire. Je vais essayer d’illustrer, en quelques mots, quelques décisions architecturales clés imposées par DIP qui ont eu un impact considérable sur nos projets..

La plupart des projets Web contiennent trois technologies principales: HTML, PHP et SQL. La version particulière de ces applications dont nous parlons ou le type d'implémentation SQL que vous utilisez n'est pas pertinent. Le fait est que les informations d'un formulaire HTML doivent se retrouver, d'une manière ou d'une autre, dans la base de données. La colle entre les deux peut être fournie avec PHP.

Ce qui est essentiel à retenir, c’est que les trois technologies représentent bien trois couches architecturales différentes: l’interface utilisateur, la logique commerciale et la persistance. Nous parlerons des implications de ces couches dans une minute. Pour l'instant, concentrons-nous sur des solutions étranges mais fréquemment rencontrées pour que les technologies fonctionnent ensemble..

J'ai souvent vu des projets qui utilisaient du code SQL dans une balise PHP dans un fichier HTML, ou du code PHP faisant écho à des pages et à des pages HTML et interprétant directement le code. $ _GET ou $ _POST variables globales. Mais pourquoi est-ce mauvais?


Les images ci-dessus représentent une version brute de ce que nous avons décrit dans le paragraphe précédent. Les flèches représentent différentes dépendances et, comme nous pouvons le conclure, fondamentalement, tout dépend de tout. Si nous devons modifier une table de base de données, nous pouvons éventuellement éditer un fichier HTML. Ou si nous modifions un champ en HTML, nous pourrions éventuellement changer le nom d'une colonne dans une instruction SQL. Ou si nous regardons le second schéma, nous aurons peut-être besoin de modifier notre PHP si le HTML change, ou dans de très mauvais cas, lorsque nous générons tout le contenu HTML à partir d'un fichier PHP, nous devrons sûrement changer un fichier PHP modifier le contenu HTML. Donc, il ne fait aucun doute que les dépendances zigzaguent entre les classes et les modules. Mais cela ne s'arrête pas là. Vous pouvez stocker des procédures; Code PHP dans les tables SQL.


Dans le schéma ci-dessus, les requêtes sur la base de données SQL renvoient le code PHP généré avec les données des tables. Ces fonctions ou classes PHP effectuent d'autres requêtes SQL qui renvoient un code PHP différent, et le cycle continue jusqu'à ce que toutes les informations soient finalement obtenues et renvoyées… probablement à l'interface utilisateur..

Je sais que cela peut paraître scandaleux pour beaucoup d’entre vous, mais si vous n’avez pas encore travaillé sur un projet inventé et mis en œuvre de cette manière, vous le ferez sûrement dans votre future carrière. La plupart des projets existants, quels que soient les langages de programmation utilisés, ont été écrits en tenant compte des anciens principes, par des programmeurs peu soucieux ou peu informés pour faire mieux. Si vous lisez ces tutoriels, vous avez probablement un niveau supérieur à celui-là. Vous êtes prêt ou prêt à respecter votre profession, à embrasser votre art et à faire mieux.

L'autre option consiste à répéter les erreurs commises par vos prédécesseurs et à en supporter les conséquences. Chez Syneto, lorsqu'un de nos projets a atteint un état presque intenable à cause de son architecture ancienne et interdépendante et que nous avons dû l'abandonner à jamais, nous avons décidé de ne plus jamais revenir sur cette voie. Depuis lors, nous nous sommes efforcés d’avoir une architecture propre qui respecte correctement les principes SOLID et, plus important encore, le principe de dépendance par inversion..


Ce qui est étonnant dans cette architecture, c’est la façon dont les dépendances pointent:

  • L’interface utilisateur (dans la plupart des cas, une infrastructure Web MVC) ou tout autre mécanisme de diffusion disponible pour votre projet dépend de la logique d’entreprise. La logique commerciale est assez abstraite. Une interface utilisateur est très concrète. L'interface utilisateur n'est qu'un détail pour le projet, et il est également très volatile. Rien ne devrait dépendre de l'interface utilisateur, rien ne devrait dépendre de votre framework MVC.
  • L'autre observation intéressante que nous pouvons faire est que la persistance, la base de données, votre MySQL ou PostgreSQL, dépend de la logique métier. Votre logique métier est indépendante de la base de données. Cela permet d’échanger la persistance à votre guise. Si demain vous voulez changer MySQL avec PostgreSQL ou simplement des fichiers texte, vous pouvez le faire. Vous devrez bien entendu implémenter une couche de persistance spécifique pour la nouvelle méthode de persistance, mais vous ne devrez pas modifier une seule ligne de code dans votre logique métier. Il existe une explication plus détaillée sur le sujet de persistance dans le didacticiel Évolution vers une couche de persistance.
  • Enfin, à droite de la logique métier, en dehors de celle-ci, nous avons toutes les classes qui créent des classes de logique métier. Ce sont des usines et des classes créées par le point d’entrée de notre application. Beaucoup de gens ont tendance à penser que ceux-ci appartiennent à la logique métier, mais s'ils créent des objets métier, leur seule raison est de le faire. Ce sont des classes juste pour nous aider à créer d'autres classes. Les objets métier et la logique qu'ils fournissent sont indépendants de ces usines. Nous pourrions utiliser différents modèles, tels que Simple Factory, Abstract Factory, Builder ou la création d'objets simples pour fournir la logique métier. Ça n'a pas d'importance. Une fois les objets métier créés, ils peuvent faire leur travail..

Montre moi le code

L'application du principe d'inversion de dépendance (DIP) au niveau architectural est assez facile si vous respectez les modèles de conception agiles classiques. L'exercer et l'illustrer à l'intérieur de la logique métier est également très facile et peut même être amusant. Nous imaginerons une application de lecture de livre électronique.

La classe Test étend PHPUnit_Framework_TestCase function testItCanReadAPDFBook () $ b = new PDFBook (); $ r = new PDFReader ($ b); $ this-> assertRegExp ('/ livre pdf /', $ r-> read ());  class PDFReader private $ book; function __construct (PDFBook $ book) $ this-> book = $ book;  fonction read () return $ this-> book-> read ();  class PDFBook function read () return "lecture d'un livre pdf."; 

Nous commençons à développer notre lecteur électronique en tant que lecteur PDF. Jusqu'ici tout va bien. Nous avons un Lecteur PDF classe utilisant un PDFBook. le lis() fonctionner sur les lecteurs lecteurs au livre lis() méthode. Nous vérifions simplement cela en faisant une vérification de l'expression rationnelle après une partie clé de la chaîne retournée par PDFBookde lecteur() méthode.

S'il vous plaît gardez à l'esprit que ce n'est qu'un exemple. Nous n'implémenterons pas la logique de lecture de fichiers PDF ou d'autres formats de fichiers. C'est pourquoi nos tests vont simplement vérifier quelques chaînes de base. Si nous devions écrire la vraie application, la seule différence serait la façon dont nous testons les différents formats de fichiers. La structure de dépendance serait très similaire à notre exemple.


Avoir un lecteur PDF utilisant un livre PDF peut être une solution judicieuse pour une application limitée. Si notre objectif était d'écrire un lecteur PDF et rien de plus, ce serait en fait une solution acceptable. Mais nous voulons écrire un lecteur de livre électronique générique, prenant en charge plusieurs formats, parmi lesquels notre première version PDF mise en œuvre. Renommons notre classe de lecteurs.

La classe Test étend PHPUnit_Framework_TestCase function testItCanReadAPDFBook () $ b = new PDFBook (); $ r = new EBookReader ($ b); $ this-> assertRegExp ('/ livre pdf /', $ r-> read ());  class EBookReader private $ book; function __construct (PDFBook $ book) $ this-> book = $ book;  fonction read () return $ this-> book-> read ();  class PDFBook function read () return "lecture d'un livre pdf."; 

Renommer n'avait aucun effet de compteur fonctionnel. Les tests passent toujours.

Les tests ont commencé à 13h04…
PHPUnit 3.7.28 par Sebastian Bergmann.
Temps: 13 ms, mémoire: 2,50 Mo
OK (1 test, 1 assertion)
Processus terminé avec le code de sortie 0

Mais cela a un effet sérieux sur le design.


Notre lecteur est devenu beaucoup plus abstrait. Beaucoup plus général. Nous avons un générique Lecteur ebook qui utilise un type de livre très spécifique, PDFBook. Une abstraction dépend d'un détail. Le fait que notre livre soit de type PDF ne devrait être qu'un détail et personne ne devrait en dépendre.

La classe Test étend PHPUnit_Framework_TestCase function testItCanReadAPDFBook () $ b = new PDFBook (); $ r = new EBookReader ($ b); $ this-> assertRegExp ('/ livre pdf /', $ r-> read ());  interface EBook function read ();  class EBookReader private $ book; function __construct (EBook $ book) $ this-> book = $ book;  fonction read () return $ this-> book-> read ();  La classe PDFBook implémente EBook function read () return "en lisant un livre pdf."; 

La solution la plus courante et la plus utilisée pour inverser la dépendance consiste à introduire un module plus abstrait dans notre conception. "L'élément le plus abstrait de la programmation orientée objet est une interface. Ainsi, toute autre classe peut dépendre d'une interface tout en respectant le DIP".

Nous avons créé une interface pour notre lecteur. L'interface s'appelle EBook et représente les besoins du Lecteur ebook. Cela découle directement du respect du principe de séparation des interfaces (ISP), qui préconise l’idée que les interfaces doivent répondre aux besoins des clients. Les interfaces appartiennent aux clients. Elles ont donc été nommées pour refléter les types et les objets dont les clients ont besoin. Elles contiendront les méthodes que les clients souhaitent utiliser. C’est naturel pour un Lecteur ebook utiliser Livres électroniques et avoir un lis() méthode.


Au lieu d'une seule dépendance, nous avons maintenant deux dépendances.

  • Les premiers points de dépendance de Lecteur ebook vers la EBook interface et il est de type usage. Lecteur ebook les usages Livres électroniques.
  • La deuxième dépendance est différente. Il pointe de PDFBook vers le même EBook interface mais il est de type implémentation. UNE PDFBook est juste une forme particulière de EBook, et implémente donc cette interface pour satisfaire les besoins du client.

Sans surprise, cette solution nous permet également de brancher différents types de livres électroniques dans notre lecteur. La seule condition pour tous ces livres est de satisfaire la EBook interface et le mettre en œuvre.

La classe Test étend PHPUnit_Framework_TestCase function testItCanReadAPDFBook () $ b = new PDFBook (); $ r = new EBookReader ($ b); $ this-> assertRegExp ('/ livre pdf /', $ r-> read ());  function testItCanReadAMobiBook () $ b = new MobiBook (); $ r = new EBookReader ($ b); $ this-> assertRegExp ('/ mobi book /', $ r-> read ());  interface EBook function read ();  class EBookReader private $ book; function __construct (EBook $ book) $ this-> book = $ book;  fonction read () return $ this-> book-> read ();  La classe PDFBook implémente EBook function read () return "en lisant un livre pdf.";  La classe MobiBook implémente EBook function read () return "lire un livre mobi."; 

Ce qui à son tour nous conduit au principe ouvert / fermé, et le cercle est fermé.

Le principe d'inversion de dépendance en est un qui nous guide ou nous aide à respecter tous les autres principes. Respecter DIP:

  • Vous obliger presque à respecter OCP.
  • Vous permettent de séparer les responsabilités.
  • Vous faire utiliser correctement le sous-typage.
  • Vous offre la possibilité de séparer vos interfaces.

Dernières pensées

C'est tout. Nous avons fini. Tous les tutoriels sur les principes SOLID sont complets. Pour moi personnellement, découvrir ces principes et mettre en œuvre des projets en tenant compte de ces changements a été un énorme changement. J'ai complètement changé ma conception du design et de l'architecture et je peux dire depuis que tous les projets sur lesquels je travaille sont exponentiellement plus faciles à gérer et à comprendre..

Je considère les principes SOLID comme l’un des concepts les plus essentiels de la conception orientée objet. Ces concepts doivent nous guider pour améliorer notre code et rendre notre vie de programmeur beaucoup plus facile. Un code bien conçu est plus facile à comprendre pour les programmeurs. Les ordinateurs sont intelligents, ils peuvent comprendre le code indépendamment de sa complexité. Les êtres humains, par contre, ont un nombre limité de choses qu’ils peuvent garder dans leur esprit actif et concentré. Plus précisément, le nombre de ces choses est le nombre magique sept, plus ou moins deux.

Nous devrions nous efforcer de structurer notre code autour de ces chiffres et plusieurs techniques nous aident à le faire. Fonctions avec une longueur maximale de quatre lignes (cinq avec la ligne de définition incluse) afin qu'elles puissent toutes s'intégrer en même temps dans notre esprit. Les indentations ne dépassent pas cinq niveaux de profondeur. Classes avec pas plus de neuf méthodes. Modèles de conception qui utilisent généralement un nombre de cinq à neuf classes. Notre conception de haut niveau dans les schémas ci-dessus utilise quatre à cinq concepts. Il existe cinq principes SOLID, chacun nécessitant d’illustrer cinq à neuf sous-concepts / modules / classes. La taille idéale d'une équipe de programmation est comprise entre cinq et neuf. Le nombre idéal d'équipes dans une entreprise se situe entre cinq et neuf..

Comme vous pouvez le constater, le nombre magique sept, plus ou moins deux est tout autour de nous, alors pourquoi votre code devrait-il être différent??