Écrire des programmes informatiques est très amusant. Sauf si vous devez travailler avec le code d'autres personnes.
Si vous avez travaillé comme développeur professionnel pendant plus de trois jours, vous savez que notre travail est loin d'être créatif et passionnant. Cela s'explique d'une part par la direction de l'entreprise (lire : des gens qui ne comprennent jamais), et d'autre part par la complexité du code avec lequel nous devons travailler. Si nous ne pouvons absolument rien faire pour la première raison, nous pouvons en revanche faire beaucoup pour la seconde.
Alors, pourquoi les bases de code sont-elles si complexes que nous avons envie de nous étriper ? Tout simplement parce que les personnes qui ont écrit la première version étaient pressées, et que celles qui sont venues par la suite n'ont fait qu'ajouter au désordre. Le résultat final : une bouillie que peu de gens veulent toucher et que personne ne comprend.
Bienvenue au premier jour de travail !

Mais il n'est pas nécessaire qu'il en soit ainsi.
Écrire un bon code, un code modulaire et facile à maintenir, n'est pas si difficile. Cinq principes simples - établis de longue date et bien connus - s'ils sont suivis avec discipline, vous permettront de vous assurer que votre code est lisible, pour les autres et pour vous lorsque vous le regarderez six mois plus tard. 😂
Ces principes directeurs sont représentés par l'acronyme SOLIDE. Peut-être avez-vous déjà entendu parler des "principes SOLID", peut-être pas. Si c'est le cas, mais que vous avez remis cet apprentissage à "un jour", faisons en sorte que ce soit aujourd'hui !
Alors, sans plus attendre, voyons ce qu'est cette notion de SOLID et comment elle peut nous aider à écrire un code serré et vraiment soigné.
s" pour "Responsabilité unique" (Single Responsibility)
Si vous consultez différentes sources décrivant le principe de responsabilité unique, vous constaterez que sa définition varie. Cependant, en termes plus simples, il se résume à ceci : Chaque classe de votre base de code doit avoir un rôle très spécifique, c'est-à-dire qu'elle ne doit être responsable que d'un seul objectif. Et chaque fois qu'un changement est nécessaire dans cette classe, il s'ensuit que nous devrons la modifier uniquement parce que cette responsabilité spécifique a changé.
Lorsque j'ai découvert cela la première fois, la définition qui m'a été présentée était la suivante : "Il doit y avoir une, et une seule raison pour qu'une classe change". Je me suis dit : "Quoi ? Un changement ? Quel changement ? C'est pourquoi j'ai dit plus tôt que si vous lisez des articles sur le sujet à différents endroits, vous obtiendrez des définitions apparentées mais quelque peu différentes et potentiellement déroutantes.
Quoi qu'il en soit, cela suffit. Il est temps de passer aux choses sérieuses : si vous êtes comme moi, vous vous demandez probablement : "D'accord, tout va bien. Mais pourquoi diable devrais-je m'en soucier ? Je ne vais pas commencer à écrire du code dans un style totalement différent à partir de demain juste parce qu'un fou qui a écrit un livre (et qui est maintenant mort) le dit."
Excellent !
Et c'est l'esprit que nous devons conserver si nous voulons vraiment apprendre des choses. Alors, pourquoi toutes ces chansons et ces danses sur la "responsabilité unique" ont-elles de l'importance ? Les explications varient d'une personne à l'autre, mais pour moi, ce principe consiste à introduire de la discipline et de la concentration dans votre code.

Voyons un exemple avant d'expliquer mon interprétation. Contrairement à d'autres ressources trouvées sur le web qui fournissent des exemples que vous comprenez mais qui vous laissent demander comment ils peuvent vous aider dans le monde réel, plongeons dans quelque chose de spécifique, un style de codage que nous voyons encore et encore, et peut-être même que nous écrivons dans nos applications Laravel.
Lorsqu'une application Laravel reçoit une requête web, l'URL est comparée aux routes que vous avez définies dans web.php
et api.php
, et s'il y a une correspondance, les données de la requête atteignent le contrôleur. Voici à quoi ressemble une méthode de contrôleur typique dans les applications réelles, au niveau de la production :
class UserController extends Controller {
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'first_name' => 'required',
'last_name' => 'required',
'email' => 'required|email|unique :users',
'phone' => 'nullable'
]) ;
if ($validator->fails()) {
Session::flash('error', $validator->messages()->first()) ;
return redirect()->back()->withInput() ;
}
// créez un nouvel utilisateur
$user = User: :create([
'first_name' => $request->first_name,
'last_name' => $request->last_name,
'email' => $request->email,
'phone' => $request->phone,
]) ;
return redirect()->route('login') ;
}
}
Nous avons tous écrit un code de ce type. Et il est facile de voir ce qu'il fait : enregistrer de nouveaux utilisateurs. Il a l'air bien et fonctionne bien, mais il y a un problème : il n'est pas à l'épreuve du temps. Et par pérenne, je veux dire qu'il n'est pas prêt à gérer le changement sans créer de désordre.
Pourquoi ?
Vous pouvez constater que la fonction est destinée aux routes définies dans le fichier web.php
, c'est-à-dire aux pages traditionnelles, créées par le serveur. Quelques jours plus tard, votre client/employeur fait développer une application mobile, ce qui signifie que cette route ne sera d'aucune utilité pour les utilisateurs qui s'inscrivent à partir d'un appareil mobile. Que faites-vous ? Créez une route similaire dans le fichier api.php
et écrivez une fonction contrôleur basée sur JSON pour cette route ? Très bien, et ensuite ? Vous copiez tout le code de cette fonction, vous y apportez quelques modifications et vous vous arrêtez là ? C'est en effet ce que font de nombreux développeurs, mais ils s'exposent à l'échec.

Le problème est que HTML et JSON ne sont pas les seuls formats d'API au monde (considérons simplement les pages HTML comme une API pour les besoins de l'argumentation). Qu'en est-il d'un client qui dispose d'un ancien système fonctionnant au format XML ? Et il y en a un autre pour SOAP. Et gRPC. Et Dieu sait ce qui viendra le lendemain.
Vous pouvez toujours envisager de créer un fichier distinct pour chacun de ces types d'API et de copier le code existant, en le modifiant légèrement. Certes, il y a dix fichiers, dites-vous, mais tout fonctionne bien, alors pourquoi se plaindre ? Mais c'est alors que survient le coup de poing, l'ennemi juré du développement de logiciels : le changement. Supposez maintenant que les besoins de votre client/employeur aient changé. Il veut maintenant que, lors de l'enregistrement de l'utilisateur, nous enregistrions l'adresse IP et que nous ajoutions une option pour un champ indiquant que l'utilisateur a lu et compris les conditions générales d'utilisation.
Nous avons maintenant dix fichiers à modifier et nous devons nous assurer que la logique est traitée exactement de la même manière dans chacun d'entre eux. Une seule erreur peut entraîner des pertes importantes pour l'entreprise. Imaginez maintenant l'horreur dans les applications SaaS à grande échelle, car la complexité du code est déjà très élevée.

Comment en sommes-nous arrivés là ?
La réponse est que la méthode du contrôleur qui semble si inoffensive fait en réalité un certain nombre de choses différentes : elle valide la requête entrante, elle gère les redirections et elle crée de nouveaux utilisateurs.
Elle fait trop de choses ! Et oui, comme vous l'avez peut-être remarqué, savoir comment créer de nouveaux utilisateurs dans le système ne devrait pas être le travail d'une méthode de contrôleur. Si nous devions retirer cette logique de la fonction et la placer dans une classe séparée, nous aurions maintenant deux classes, chacune avec une seule responsabilité à gérer. Bien que ces classes puissent s'aider mutuellement en appelant leurs méthodes, elles ne sont pas autorisées à savoir ce qui se passe dans l'autre classe.
class UserController extends Controller {
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'first_name' => 'required',
'last_name' => 'required',
'email' => 'required|email|unique :users',
'phone' => 'nullable'
]) ;
if ($validator->fails()) {
Session::flash('error', $validator->messages()->first()) ;
return redirect()->back()->withInput() ;
}
UserService::createNewUser($request->all()) ;
return redirect()->route('login') ;
}
}
Regardez le code maintenant : beaucoup plus compact, facile à comprendre ... et surtout, adaptable au changement. Dans la continuité de notre discussion précédente où nous avions dix types d'API différents, chacun d'entre eux appelle maintenant une seule fonction UserService::createNewUser($request->all()) ;
et vous n'avez plus qu'à vous en occuper. Si des changements sont nécessaires dans la logique d'enregistrement des utilisateurs, la classe UserService
s'en chargera, tandis que les méthodes du contrôleur n'auront pas besoin d'être modifiées. Si la confirmation par SMS doit être activée après l'enregistrement de l'utilisateur, le UserService
s'en chargera (en appelant une autre classe qui sait comment envoyer des SMS), et les contrôleurs resteront inchangés.
C'est ce que j'entendais par concentration et discipline : concentration dans le code (une chose faisant une seule chose) et discipline de la part du développeur (ne pas tomber dans les solutions à court terme).
Voilà, c'était une sacrée visite ! Et nous n'avons abordé qu'un seul des cinq principes. Passons à la suite !
"O" pour Ouvert-Fermé
Je dois dire que la personne qui a défini ces principes ne pensait certainement pas aux développeurs les moins expérimentés. Il en va de même pour le principe Open-Closed, et ceux à venir ont une longueur d'avance en matière de bizarrerie. 😂😂
Quoi qu'il en soit, regardons la définition trouvée par tout le monde pour ce principe : Les classes doivent être ouvertes à l'extension mais fermées à la modification. Eh ? ? Oui, moi non plus je n'ai pas été amusé la première fois que je l'ai rencontrée, mais avec le temps j'ai fini par comprendre - et admirer - ce que cette règle essaie de dire : le code une fois écrit ne devrait pas avoir besoin d'être modifié.

D'un point de vue philosophique, cette règle est excellente : si le code ne change pas, il restera prévisible et de nouvelles bogues ne seront pas introduites. Mais comment est-il possible de rêver d'un code qui ne change pas alors que tout ce que nous faisons en tant que développeurs est de courir après le changement en permanence ?
Tout d'abord, ce principe ne signifie pas qu'aucune ligne du code existant ne doit être modifiée ; cela sortirait tout droit d'un conte de fées. Le monde change, l'entreprise change et, par conséquent, le code change - il n'y a pas d'échappatoire. Mais ce que ce principe signifie, c'est que nous limitons autant que possible la possibilité de modifier le code existant. Il vous indique également comment procéder : les classes doivent être ouvertes à l'extension et fermées à la modification.
"Extension" signifie ici réutilisation, que cette réutilisation prend la forme de classes enfants héritant des fonctionnalités d'une classe mère, ou que d'autres classes stockent des instances d'une classe et appellent ses méthodes.
Revenons donc à la question à un million de dollars : comment écrire un code qui survive au changement ? Et là, je crains que personne n'ait de réponse claire. Dans la programmation orientée objet, plusieurs techniques ont été découvertes et affinées pour atteindre cet objectif, depuis les principes SOLID que nous étudions jusqu'aux schémas de conception courants, aux schémas d'entreprise, aux schémas architecturaux, et ainsi de suite. Il n'y a pas de réponse parfaite, c'est pourquoi un développeur doit aller toujours plus haut, rassembler autant d'outils qu'il le peut et essayer de faire de son mieux.
En gardant cela à l'esprit, examinons l'une de ces techniques. Supposons que nous ayons besoin d'ajouter la fonctionnalité permettant de convertir un contenu HTML donné (peut-être une facture ?) en un fichier PDF et de forcer un téléchargement immédiat dans le navigateur. Supposons également que nous ayons souscrit un abonnement payant à un service hypothétique appelé MilkyWay, qui se chargera de la génération du fichier PDF. Nous pourrions finir par écrire une méthode de contrôleur comme celle-ci :
class InvoiceController extends Controller {
public function generatePDFDownload(Request $request) {
$pdfGenerator = new MilkyWay() ;
$pdfGenerator->apiKey = env('MILKY_WAY_API_KEY') ;
$pdfGenerator->setContent($request->content) ; // Format HTML
$pdfFile = $pdfGenerator->generateFile('invoice.pdf') ;
return response()->download($pdfFile, [
'Content-Type' => 'application/pdf',
]) ;
}
}
J'ai laissé de côté la validation des requêtes, etc., afin de me concentrer sur le problème principal. Vous remarquerez que cette méthode respecte bien le principe de la responsabilité unique : elle n'essaie pas de parcourir le contenu HTML qui lui est transmis et de créer un PDF (en fait, elle ne sait même pas qu'elle a reçu du HTML) ; au lieu de cela, elle transmet cette responsabilité à la classe spécialisée MilkyWay
et présente ce qu'elle reçoit sous forme de téléchargement.
Mais il y a un petit problème.

Notre méthode de contrôle est trop dépendante de la classe MilkyWay. Si la prochaine version de l'API MilkyWay modifie l'interface, notre méthode cessera de fonctionner. Et si nous souhaitons un jour utiliser un autre service, nous devrons littéralement effectuer une recherche globale dans notre éditeur de code et modifier tous les extraits de code qui mentionnent MilkyWay. Et pourquoi est-ce mauvais ? Parce que cela augmente considérablement le risque de commettre une erreur et que c'est une charge pour l'entreprise (le temps passé par les développeurs à régler le problème).
Tout ce gâchis est dû au fait que nous avons créé une méthode qui n'était pas ouverte au changement.
Peut-on faire mieux ?
Oui, c'est possible !
Dans ce cas, nous pouvons tirer parti d'une pratique qui va à peu près comme suit : programmer en fonction des interfaces, et non des implémentations.
Oui, je sais, c'est un autre de ces OOPSismes qui n'ont aucun sens la première fois. Mais ce que cela signifie, c'est que notre code doit dépendre de types de choses, et non de choses particulières elles-mêmes. Dans notre cas, nous devons nous affranchir de la classe MilkyWay
et dépendre plutôt d'une classe générique, un type de classe PDF (tout deviendra clair dans une seconde).
Maintenant, quels sont les outils dont nous disposons en PHP pour créer de nouveaux types ? En gros, nous avons l'héritage et les interfaces. Dans notre cas, créer une classe de base pour toutes les classes PDF ne sera pas une bonne idée car il est difficile d'imaginer que différents types de moteurs/services PDF partagent le même comportement. Ils peuvent peut-être partager la méthode setContent()
, mais même dans ce cas, le processus d'acquisition du contenu peut être différent pour chaque classe de service PDF, et le fait de tout inscrire dans une hiérarchie d'héritage ne fera qu'empirer les choses.
Ceci étant compris, créons une interface qui spécifie les méthodes que nous voulons que toutes nos classes de moteur PDF contiennent :
interface IPDFGenerator {
public function setup() ; // clés API, etc.
public function setContent($content) ;
public function generatePDF($fileName = null) ;
}
Alors, qu'avons-nous ici ?
Grâce à cette interface, nous disons que nous attendons de toutes nos classes PDF qu'elles aient au moins ces trois méthodes. Maintenant, si le service que nous voulons utiliser (MilkyWay, dans notre cas) ne suit pas cette interface, c'est à nous d'écrire une classe qui le fasse. Voici un aperçu de la manière dont nous pourrions écrire une classe enveloppante pour notre service MilkyWay
:
class MilkyWayPDFGenerator implements IPDFGenerator {
public function __construct() {
$this->setup() ;
}
public function setup() {
$this->generator = new MilkyWay() ;
$this->generator->api_key = env('MILKY_WAY_API_KEY') ;
}
public function setContent($content) {
$this->generator->setContent($content) ;
}
public function generatePDF($fileName) {
return $this->generator->generateFile($fileName) ;
}
}
Et de la même manière, chaque fois que nous aurons un nouveau service PDF, nous écrirons une classe enveloppante pour celui-ci. Par conséquent, toutes ces classes seront considérées comme étant de type IPDFGenerator
.
Alors, quel est le lien entre le principe d'ouverture et Laravel ?
Pour y parvenir, nous devons connaître deux autres concepts clés : Les liaisons de conteneurs de Laravel et une technique très courante appelée injection de dépendances. Encore une fois, de grands mots, mais l'injection de dépendances signifie simplement qu'au lieu de créer des objets de classes vous-même, vous les mentionnez dans les arguments des fonctions et quelque chose les créera automatiquement pour vous. Cela vous évite d'avoir à écrire du code tel que $account = new Account() ;
tout le temps et rend le code plus testable (un sujet pour un autre jour). Cette "quelque chose" que j'ai mentionné prend la forme du Conteneur de services dans le monde Laravel.
Pour l'instant, considérez-le simplement comme quelque chose qui peut créer de nouvelles instances de classe pour nous. Voyons comment cela nous aide.
Dans le conteneur de service de notre exemple, nous pouvons écrire quelque chose comme ceci :
$this->app->bind('App\Interfaces\IPDFGenerator', 'App\Services\PDF\MilkyWayPDFGenerator') ;
Cela revient à dire qu'à chaque fois que quelqu'un demande un IPDFGenerator
, donnez-lui la classe MilkyWayPDFGenerator
. Et après toutes ces chansons et ces danses, mesdames et messieurs, nous arrivons au moment où tout se met en place et où le principe d'ouverture et de fermeture se révèle à l'œuvre !
Armés de toutes ces connaissances, nous pouvons réécrire la méthode de notre contrôleur de téléchargement de PDF comme suit :
class InvoiceController extends Controller {
public function generatePDFDownload(Request $request, IPDFGenerator $generator) {
$generator->setContent($request->content) ;
$pdfFile = $generator->generatePDF('invoice.pdf') ;
return response()->download($pdfFile, [
'Content-Type' => 'application/pdf',
]) ;
}
}
Vous remarquez la différence ?
Tout d'abord, nous recevons notre instance de classe de générateur de PDF dans l'argument de la fonction. Celle-ci est créée et nous est transmise par le conteneur de services, comme nous l'avons vu précédemment. Le code est également plus propre et il n'y a aucune mention de clés API, etc. Mais surtout, il n'y a aucune trace de la classe MilkyWay
. Cela a également l'avantage de rendre le code plus facile à lire (quelqu'un qui le lit pour la première fois ne se dira pas "Whoa ! C'est quoi ce MilkyWay
?" et s'en préoccupera constamment).
Mais le plus grand avantage de tous ?
Cette méthode est désormais fermée à la modification et résistante au changement. Permettez-moi de m'expliquer. Supposons que nous estimions demain que le service MilkyWay
est trop cher (ou, comme c'est souvent le cas, que son service d'assistance à la clientèle est devenu minable) ; en conséquence, nous avons essayé un autre service appelé SilkyWay
et nous voulons passer à celui-ci. Tout ce que nous avons à faire est d'écrire une nouvelle classe IPDFGenerator
verser SilkyWay
et de changer le binding dans le code de notre Service Container :
$this->app->bind('App\Interfaces\IPDFGenerator', 'App\Services\PDF\SilkyWayPDFGenerator') ;
C'est tout !
Rien d'autre n'a besoin d'être changé, parce que notre application est écrite selon une interface (l'interface IPDFGenerator) au lieu d'une classe concrète. L'exigence commerciale a changé, un nouveau code a été ajouté (une classe enveloppante) et une seule ligne de code a été modifiée - tout le reste demeure intact et toute l'équipe peut rentrer chez elle en toute confiance et dormir tranquillement.
Vous voulez dormir sur vos deux oreilles ? Suivez le principe de l'ouverture et de la fermeture ! 🤭😆
"L comme Substitution de Liskov
Liskov quoi ?
On dirait que ce truc sort tout droit d'un manuel de chimie organique. Cela pourrait même vous faire regretter d'avoir choisi de faire carrière dans le développement de logiciels parce que vous pensiez que c'était uniquement pratique et pas du tout théorique.

Mais attendez un peu ! Croyez-moi, ce principe est aussi facile à comprendre que son nom est intimidant. En fait, il s'agit peut-être du principe le plus facile à comprendre parmi les cinq (enfin, euh... si ce n'est pas le plus facile, c'est au moins celui dont l'explication est la plus courte et la plus directe).
Cette règle dit simplement que le code qui fonctionne avec des classes mères (ou des interfaces) ne doit pas s'interrompre lorsque ces classes sont remplacées par des classes enfants (ou des classes implémentant l'interface). L'exemple que nous avons terminé juste avant cette section est une excellente illustration : si je remplace le type générique IPDFGenerator
dans l'argument de la méthode par l'instance spécifique MilkyWayPDFGenerator
, vous attendez-vous à ce que le code s'interrompe ou qu'il continue à fonctionner ?
Continuer à fonctionner, bien sûr ! C'est parce que la différence n'est que dans les noms, et que l'interface comme la classe ont les mêmes méthodes fonctionnant de la même manière, donc notre code fonctionnera comme avant.
Quel est donc l'intérêt de ce principe ? Eh bien, en termes plus simples, c'est tout ce que ce principe dit : assurez-vous que vos sous-classes implémentent toutes les méthodes exactement comme il se doit, avec le même nombre et le même type d'arguments, et le même type de retour. Si un seul paramètre différait, nous continuerions sans le savoir à construire davantage de code par-dessus, et un jour nous aurions le genre de désordre nauséabond dont la seule solution serait de le démanteler.
Voilà. Ce n'était pas si mal, n'est-ce pas ? 😇
Il y a encore beaucoup à dire sur la substitution de Liskov (consultez sa théorie et lisez sur les types covariants si vous vous sentez vraiment courageux), mais à mon avis, cela suffit pour le développeur moyen qui découvre pour la première fois ces terres mystérieuses de modèles et de principes.
i" est pour "Interface Segregation
La ségrégation d'interface ... hmm, cela ne sonne pas si mal, n'est-ce pas ? On dirait qu'il s'agit de ségréguer... humm, de séparer... les interfaces. Je me demande simplement où et comment.
Si vous pensiez de la sorte, croyez-moi, vous avez presque fini de comprendre et d'utiliser ce principe. Si les cinq principes SOLID étaient des véhicules d'investissement, c'est celui-ci qui offrirait la plus grande valeur à long terme pour apprendre à bien coder (d'accord, je réalise que je dis cela pour chaque principe, mais vous voyez, vous avez compris l'idée).
Dépouillé de son jargon et réduit à sa forme la plus élémentaire, le principe de séparation des interfaces est le suivant : Plus les interfaces sont nombreuses et spécialisées dans votre application, plus votre code sera modulaire et moins bizarre.
Prenons un exemple très courant et pratique. Chaque développeur Laravel rencontre le "Repository Pattern" au cours de sa carrière, après quoi il passe les semaines suivantes à traverser une phase cyclique de hauts et de bas, et finit par rejeter le pattern. Pourquoi ? Dans tous les tutoriels couvrant le Repository Pattern, il vous est conseillé de créer une interface commune (appelée Repository) qui définira les méthodes nécessaires pour accéder ou manipuler les données. Cette interface de base peut ressembler à ceci :
interface IRepository {
public function getOne($id) ;
public function getAll() ;
public function create(array $data) ;
public function update(array $data, $id) ;
public function delete($id) ;
}
Et maintenant, pour votre modèle User
, vous êtes censé créer un UserRepository
qui implémente cette interface ; ensuite, pour votre modèle Customer
, vous êtes censé créer un CustomerRepository
qui implémente cette interface ; vous voyez l'idée.
Il se trouve que dans un de mes projets, certains modèles n'étaient pas censés être accessibles en écriture par quelqu'un d'autre que le système. Avant que vous ne commenciez à lever les yeux au ciel, considérez que l'enregistrement ou le maintien d'une piste d'audit est un bon exemple concret de ces modèles "en lecture seule". Le problème auquel j'ai été confronté était que, puisque j'étais censé créer des référentiels qui implémentaient tous l'interface IRepository
, disons le LoggingRepository
, au moins deux des méthodes de l'interface, update()
et delete(
) ne m'étaient d'aucune utilité.
Oui, une solution rapide serait de les implémenter de toute façon et de les laisser vides ou de lever une exception, mais si dépendre de telles solutions était acceptable pour moi, je ne suivrais pas le Repository Pattern en premier lieu !

Cela signifie-t-il que tout est de la faute du modèle de référentiel ?
Non, pas du tout !
En fait, un Repository est un pattern bien connu et accepté qui apporte de la cohérence, de la flexibilité et de l'abstraction à vos modèles d'accès aux données. Le problème est que l'interface que nous avons créée - ou devrais-je dire l'interface qui a été popularisée dans pratiquement tous les tutoriels - est trop grand.
Parfois cette idée est exprimée en disant que l'interface est "grosse", mais cela signifie la même chose - l'interface fait trop d'hypothèses, et donc ajoute des méthodes qui sont inutiles pour certaines classes mais que ces classes sont forcées d'implémenter, ce qui résulte en un code fragile et confus. Notre exemple aurait pu être un peu plus simple, mais imaginez le désordre qui peut être créé lorsque plusieurs classes ont implémenté des méthodes qu'elles ne voulaient pas, ou celles qu'elles voulaient mais qui manquaient dans l'interface.
La solution est simple, et c'est aussi le nom du principe dont nous discutons : La ségrégation des interfaces.
Le fait est que nous ne devons pas créer nos interfaces à l'aveuglette. Nous ne devons pas non plus faire d'hypothèses, quelle que soit notre expérience ou notre intelligence. Au lieu de cela, nous devrions créer plusieurs interfaces spécialisées plus petites, en laissant les classes implémenter celles qui sont nécessaires et en laissant de côté celles qui ne le sont pas.
Dans l'exemple que nous avons discuté, j'aurais pu créer deux interfaces au lieu d'une : IReadOnlyRespository
(contenant les fonctions getOne()
et getAll()
), et IWriteModifyRepository
(contenant le reste des fonctions). Pour les dépôts ordinaires, je dirais alors que la classe UserRepository implémente IReadOnlyRepository, IWriteModifyRepository { ...
}
. (Remarque annexe : des cas particuliers peuvent encore se présenter, et c'est très bien ainsi car aucune conception n'est parfaite. Il se peut même que vous souhaitiez créer une interface distincte pour chaque méthode, et cela conviendra également, à condition que les besoins de votre projet soient aussi granulaires)
Oui, il y a plus d'interfaces maintenant, et certains pourraient dire qu'il y a trop de choses à retenir ou que la déclaration de classe est trop longue (ou a l'air moche), etc, mais regardez ce que nous avons gagné : des interfaces spécialisées, de petite taille, autonomes, qui peuvent être combinées selon les besoins et qui ne se gêneront pas les unes les autres. Tant que vous écrivez des logiciels pour gagner votre vie, n'oubliez pas que c'est l'idéal que tout le monde cherche à atteindre.
"D" comme Inversion de Dépendance
Si vous avez lu les parties précédentes de cet article, vous avez peut-être l'impression de comprendre ce que ce principe essaie de dire. Et vous avez raison, dans le sens où ce principe est plus ou moins une répétition de ce que nous avons discuté jusqu'à présent. Sa définition formelle n'est pas trop effrayante, alors regardons-la : Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau ; les deux doivent dépendre des abstractions.
Oui, c'est logique. Si j'ai une classe de haut niveau (de haut niveau dans le sens où elle utilise d'autres classes plus petites et plus spécialisées pour effectuer quelque chose et prendre des décisions), cette classe de haut niveau ne doit pas dépendre d'une classe de bas niveau particulière pour un certain type de travail. Elle doit plutôt être codée de manière à s'appuyer sur des abstractions (telles que les classes de base, les interfaces, etc.).
Pourquoi ?
Nous en avons déjà vu un excellent exemple dans la première partie de cet article. Si vous utilisiez un service de génération de PDF et que votre code était truffé de nouvelles
classes ABCService()
, le jour où l'entreprise déciderait d'utiliser un autre service serait un jour dont on se souviendrait à jamais - pour toutes les mauvaises raisons ! Nous devrions plutôt utiliser une forme générale de cette dépendance (créer une interface pour les services PDF), et laisser quelque chose d'autre gérer son instanciation et nous la transmettre (dans Laravel, nous avons vu comment le conteneur de service nous aidait à le faire).
En somme, notre classe de haut niveau, qui contrôlait auparavant la création d'instances de classes de niveau inférieur, doit maintenant se tourner vers quelque chose d'autre. Les rôles sont inversés, et c'est pourquoi nous appelons cela une inversion des dépendances.
Si vous cherchez un exemple pratique, revenez à la partie de cet article où nous expliquons comment éviter à notre code de dépendre exclusivement de la classe MilkyWay
PDF.
. . .
Devinez quoi, c'est ça ! Je sais, je sais, c'était une lecture assez longue et difficile, et je tiens à m'en excuser. Mais je suis de tout cœur avec le développeur moyen qui fait les choses intuitivement (ou de la manière dont on les lui a enseignées) et qui n'arrive pas à comprendre les principes de la norme SOLID. Et j'ai fait de mon mieux pour que les exemples soient aussi proches que possible du quotidien d'un développeur Laravel; après tout, à quoi nous servons les exemples qui contiennent des classes de véhicules et de voitures - ou même des génériques avancés, de la réflexion, etc. - alors qu'aucun d'entre nous ne va créer des bibliothèques.
Si vous avez trouvé cet article utile, n'hésitez pas à laisser un commentaire. Cela confirmera mon idée que les développeurs ont vraiment du mal à comprendre ces concepts "avancés", et je serai motivé pour écrire sur d'autres sujets de ce genre. A plus tard ! 🙂 .
-
J'écris sur, autour et pour l'écosystème des développeurs. Recommandations, tutoriels, discussions techniques - quoi que je publie, je fais de mon mieux pour dissiper la confusion et le flou, et fournir des réponses concrètes basées sur l'expérience personnelle... en savoir plus