Comment écrire un code qui embrasse le changement

L'écriture de code, qui est facile à changer, est le Saint Graal de la programmation. Bienvenue à la programmation du nirvana! Mais les choses sont bien plus difficiles en réalité: le code source est difficile à comprendre, les dépendances pointent dans d'innombrables directions, le couplage est agaçant et vous ressentez rapidement la chaleur de la programmation. Dans ce tutoriel, nous aborderons quelques principes, techniques et idées qui vous aideront à écrire du code facile à modifier..


Quelques concepts orientés objet

La programmation orientée objet (OOP) est devenue populaire en raison de sa promesse d’organisation et de réutilisation du code; il a complètement échoué dans cette entreprise. Nous utilisons les concepts de la programmation orientée objet depuis de nombreuses années maintenant, mais nous continuons à mettre en œuvre à plusieurs reprises la même logique dans nos projets. OOP a introduit un ensemble de principes de base qui, s’ils sont correctement utilisés, peuvent permettre d’obtenir un code meilleur, plus propre..

Cohésion

Les choses qui vont ensemble devraient être gardées ensemble; sinon, ils devraient être déplacés ailleurs. C’est ce à quoi se réfère le terme cohésion. Le meilleur exemple de cohésion peut être démontré avec une classe:

classe ANOTCohesiveClass private $ firstNumber; private $ secondNumber; longueur privée $; private $ width; function __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  function setLength ($ length) $ this-> length = $ length;  function setHeight ($ height) $ this-> width = $ height;  function add () return $ this-> firstNumber + $ this-> secondNumber;  function soustract () return $ this-> firstNumber - $ this-> secondNumber;  zone de fonction () return $ this-> length * $ this-> width; 

Cet exemple définit une classe avec des champs représentant des nombres et des tailles. Ces propriétés, jugées uniquement par leurs noms, n'appartiennent pas ensemble. Nous avons alors deux méthodes, ajouter() et soustraire (), qui fonctionnent uniquement sur les deux variables numériques. Nous avons en outre un surface() méthode, qui opère sur le longueur et largeur des champs.

Il est évident que cette classe est responsable de groupes d'informations distincts. Il a une très faible cohésion. Faisons le refactoriser.

classe ACohesiveClass private $ firstNumber; private $ secondNumber; function __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  function add () return $ this-> firstNumber + $ this-> secondNumber;  function soustract () return $ this-> firstNumber - $ this-> secondNumber; 

C'est un classe très cohésive. Pourquoi? Parce que chaque section de cette classe appartient les uns aux autres. Vous devez rechercher la cohésion, mais attention, il peut être difficile à atteindre.

Orthogonalité

En termes simples, l'orthogonalité fait référence à l'isolement ou à l'élimination des effets secondaires. Une méthode, une classe ou un module qui modifie l'état d'autres classes ou modules non liés n'est pas orthogonal. Par exemple, la boîte noire d'un avion est orthogonale. Il a ses fonctionnalités internes, sa source d'alimentation interne, ses microphones et ses capteurs. Cela n'a aucun effet sur l'avion dans lequel il réside ni sur le monde extérieur. Il fournit seulement un mécanisme pour enregistrer et récupérer les données de vol.

L'électronique de votre voiture est un exemple d'un tel système non orthogonal. L'augmentation de la vitesse de votre véhicule a plusieurs effets secondaires, tels que l'augmentation du volume de la radio (entre autres). La vitesse n'est pas orthogonale à la voiture.

calculatrice de classe private $ firstNumber; private $ secondNumber; function __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  function add () $ sum = $ this-> firstNumber + $ this-> secondNumber; if ($ sum> 100) (new AlertMechanism ()) -> tooBigNumber ($ sum);  return $ sum;  function soustract () return $ this-> firstNumber - $ this-> secondNumber;  class AlertMechanism fonction tooBigNumber ($ number) echo $ number. 'C est trop gros!'; 

Dans cet exemple, le Calculatrice la classe ajouter() méthode présente un comportement inattendu: elle crée un AlertMechanism objet et appelle l'une de ses méthodes. C'est un comportement inattendu et indésirable; Les usagers des bibliothèques n'attendront jamais un message imprimé à l'écran. Au lieu de cela, ils attendent seulement la somme des nombres fournis.

calculatrice de classe private $ firstNumber; private $ secondNumber; function __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  function add () return $ this-> firstNumber + $ this-> secondNumber;  function soustract () return $ this-> firstNumber - $ this-> secondNumber;  class AlertMechanism function checkLimits ($ firstNumber, $ secondNumber) $ sum = (nouvelle calculatrice ($ firstNumber, $ secondNumber)) -> add (); if ($ sum> 100) $ this-> tooBigNumber ($ sum);  function tooBigNumber ($ number) echo $ number. 'C est trop gros!'; 

C'est mieux. AlertMechanism n'a aucun effet sur Calculatrice. Au lieu, AlertMechanism utilise tout ce dont il a besoin pour déterminer si une alerte doit être émise.

Dépendance et couplage

Dans la plupart des cas, ces deux mots sont interchangeables. mais, dans certains cas, un terme est préféré à un autre.

Alors qu'est-ce qu'un dépendance? Quand objet UNE doit utiliser l'objet B, afin d'accomplir son comportement prescrit, on dit que UNE dépend de B. En POO, les dépendances sont extrêmement courantes. Les objets travaillent souvent avec et dépendent les uns des autres. Ainsi, si éliminer la dépendance est une noble tâche, il est presque impossible de le faire. Contrôler les dépendances et les réduire est cependant préférable.

Les termes, couplage lourd et couplage lâche, fait généralement référence à combien un objet dépend d'autres objets.

Dans un système faiblement couplé, les modifications apportées à un objet ont un effet réduit sur les autres objets qui en dépendent. Dans de tels systèmes, les classes dépendent d'interfaces plutôt que d'implémentations concrètes (nous en reparlerons plus tard). C'est pourquoi les systèmes à couplage lâche sont plus ouverts aux modifications.

Couplage dans un champ

Considérons un exemple:

classe Display calculatrice privée $; function __construct () $ this-> calculator = new Calculator (1,2); 

Il est courant de voir ce type de code. Une classe, Afficher dans ce cas, dépend de la Calculatrice classe en référençant directement cette classe. Dans le code ci-dessus, Afficherde $ calculatrice le champ est de type Calculatrice. L'objet que ce champ contient est le résultat d'un appel direct Calculatricele constructeur.

Couplage en accédant aux autres méthodes de classe

Consultez le code suivant pour une démonstration de ce type de couplage:

classe Display calculatrice privée $; function __construct () $ this-> calculator = new Calculator (1, 2);  function printSum () echo $ this-> calculator-> add (); 

le Afficher la classe appelle le Calculatrice objets ajouter() méthode. C'est une autre forme de couplage, car une classe accède à la méthode de l'autre.

Couplage par référence de méthode

Vous pouvez également coupler des classes avec des références de méthodes. Par exemple:

 classe Display calculatrice privée $; function __construct () $ this-> calculator = $ this-> makeCalculator ();  function printSum () echo $ this-> calculator-> add ();  function makeCalculator () renvoie la nouvelle calculatrice (1, 2); 

Il est important de noter que le makeCalculator () méthode retourne un Calculatrice objet. C'est une dépendance.

Couplage par polymorphisme

L'héritage est probablement la forme de dépendance la plus forte:

class AdvancedCalculator étend la calculatrice function sinus ($ value) return sin ($ value); 

Non seulement peut Calculatrice avancée ne pas faire son travail sans Calculatrice, mais il ne pourrait même pas exister sans elle.

Réduction du couplage par injection de dépendance

On peut réduire le couplage en injectant une dépendance. Voici un exemple:

classe Display calculatrice privée $; function __construct (Calculator $ calculator = null) $ this-> calculator = $ calculator? : $ this-> makeCalculator ();  //… //

En injectant le Calculatrice objet à travers Afficherconstructeur, nous avons réduit Afficherla dépendance de la Calculatrice classe. Mais ce n'est que la moitié de la solution.

Réduction du couplage avec les interfaces

Nous pouvons réduire davantage le couplage en utilisant des interfaces. Par exemple:

interface CanCompute function add (); fonction soustraction ();  class Calculator implémente CanCompute private $ firstNumber; private $ secondNumber; function __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  function add () return $ this-> firstNumber + $ this-> secondNumber;  function soustract () return $ this-> firstNumber - $ this-> secondNumber;  class Display private $ calculator; function __construct (CanCompute $ calculator = null) $ this-> calculator = $ calculator? : $ this-> makeCalculator ();  function printSum () echo $ this-> calculator-> add ();  function makeCalculator () renvoie la nouvelle calculatrice (1, 2); 

Vous pouvez considérer le FAI comme un principe de cohésion de niveau supérieur.

Ce code introduit le CanCompute interface. Une interface est aussi abstraite que vous pouvez obtenir en POO; il définit les membres qu'une classe doit implémenter. Dans le cas de l'exemple ci-dessus, Calculatrice met en œuvre le CanCompute interface.

AfficherLe constructeur attend un objet qui implémente CanCompute. À ce point, Afficherla dépendance de Calculatrice est effectivement cassé. A tout moment, nous pouvons créer une autre classe qui implémente CanCompute et passer un objet de cette classe à Afficherle constructeur. Afficher dépend maintenant seulement de la CanCompute interface, mais même cette dépendance est facultative. Si nous ne transmettons aucun argument à Afficherconstructeur, il va simplement créer un classique Calculatrice objet en appelant makeCalculator (). Cette technique est fréquemment utilisée et est extrêmement utile pour le développement piloté par les tests (TDD)..


Les principes solides

SOLID est un ensemble de principes pour l'écriture de code propre, qui facilite ensuite les modifications, la maintenance et l'extension dans le futur. Ce sont des recommandations qui, appliquées au code source, ont un effet positif sur la maintenabilité.

Un peu d'histoire

Les principes SOLID, également appelés principes Agiles, ont été définis à l'origine par Robert C. Martin. Même s'il n'a pas inventé tous ces principes, c'est lui qui les a mis en place. Vous pouvez en apprendre plus à leur sujet dans son livre: Développement de logiciels agiles, principes, modèles et pratiques. Les principes de SOLID couvrent un large éventail de sujets, mais je les exposerai aussi simplement que possible. N'hésitez pas à demander des détails supplémentaires dans les commentaires, si nécessaire.

Principe de responsabilité unique (SRP)

Une classe a une seule responsabilité. Cela peut sembler simple, mais il peut parfois être difficile à comprendre et à mettre en pratique.

classe Reporter function generateIncomeReports (); function generatePaymentsReports (); fonction computeBalance (); fonction printReport (); 

À votre avis, qui profite du comportement de cette classe? Un service de comptabilité est une option (pour le solde), le service financier peut en être une autre (pour les rapports de revenus / paiements) et même le service d'archivage peut imprimer et archiver les rapports..

Il y a quatre raisons pour lesquelles vous pourriez avoir à changer de classe. chaque département peut vouloir que leurs méthodes respectives soient adaptées à leurs besoins.

Le PRS recommande de diviser ces classes en classes plus petites et spécifiques à chaque utilisateur, chacune n'ayant qu'une raison de changer. De telles classes ont tendance à être très cohésives et à être couplées de manière lâche. En un sens, SRP est une cohésion définie du point de vue des utilisateurs.

Principe ouvert-fermé (OCP)

Les classes (et les modules) doivent accepter l'extension de leurs fonctionnalités, ainsi que résister aux modifications de leurs fonctionnalités actuelles. Jouons avec l'exemple classique d'un ventilateur électrique. Vous avez un commutateur et vous voulez contrôler le ventilateur. Donc, vous pourriez écrire quelque chose comme:

classe Switch_ private $ fan; function __construct () $ this-> fan = new Fan ();  function turnOn () $ this-> fan-> on ();  function turnOff () $ this-> fan-> off (); 

L'héritage est probablement la forme de dépendance la plus forte.

Ce code définit un Commutateur_ classe qui crée et contrôle un Ventilateur objet. Veuillez noter le trait de soulignement après "Switch_". PHP ne vous permet pas de définir une classe avec le nom "Switch".

Votre patron décide qu'il veut contrôler la lumière avec le même interrupteur. Ceci est un problème, parce que vous devoir changer Commutateur_.

Toute modification du code existant est un risque; d'autres parties du système peuvent être affectées et nécessiter encore plus de modifications. Il est toujours préférable de laisser les fonctionnalités existantes seules lors de l'ajout de nouvelles fonctionnalités..

En terminologie POO, vous pouvez voir que Commutateur_ a une forte dépendance sur Ventilateur. C’est là que réside notre problème et où nous devrions apporter nos changements..

interface Switchable function on (); fonction désactivée ();  classe Fan implémente Switchable fonction publique on () // code pour démarrer le ventilateur public fonction off () // code pour arrêter le ventilateur classe Switch_ private $ switchable; function __construct (Switchable $ switchable) $ this-> switchable = $ switchable;  function turnOn () $ this-> commutable-> on ();  function turnOff () $ this-> commutable-> off (); 

Cette solution introduit le Commutable interface. Il définit les méthodes que tous les objets activés par commutateur doivent implémenter. le Ventilateur met en oeuvre Commutable, et Commutateur_ accepte une référence à un Commutable objet dans son constructeur.

Comment cela nous aide-t-il??

Premièrement, cette solution rompt la dépendance entre Commutateur_ et Ventilateur. Commutateur_ n'a aucune idée qu'il commence un fan, ni il se soucie de. Deuxièmement, introduire un Lumière la classe n'affectera pas Commutateur_ ou Commutable. Voulez-vous contrôler un Lumière objet avec votre Commutateur_ classe? Créez simplement un Lumière objet et le passer à Commutateur_, comme ça:

class Light implémente Switchable public function on () // code pour allumer ligh public function off () // code pour éteindre la lumière class SomeWhereInYourCode function controlLight () $ light = new Light (); $ switch = new Switch _ ($ light); $ switch-> turnOn (); $ switch-> turnOff (); 

Principe de substitution de Liskov (LSP)

Le LSP indique qu'une classe enfant ne doit jamais interrompre la fonctionnalité de la classe parent. Ceci est extrêmement important car les consommateurs d'une classe parente s'attendent à ce que la classe se comporte d'une certaine manière. Le passage d'une classe enfant à un consommateur doit simplement fonctionner et ne pas affecter la fonctionnalité d'origine.

Ceci est déroutant au premier abord, regardons donc un autre exemple classique:

class Rectangle private $ width; hauteur privée $; fonction setWidth ($ width) $ this-> width = $ width;  function setHeigth ($ hauteur) $ ceci-> hauteur = $ hauteur;  zone de fonction () return $ this-> width * $ this-> height; 

Cet exemple définit un simple Rectangle classe. Nous pouvons définir sa hauteur et sa largeur, et sa surface() méthode fournit la zone du rectangle. En utilisant le Rectangle La classe pourrait ressembler à ceci:

classe Geometry fonction rectArea (Rectangle $ rectangle) $ rectangle-> setWidth (10); $ rectangle-> setHeigth (5); return $ rectangle-> area (); 

le rectArea () méthode accepte un Rectangle objet en tant qu'argument, définit sa hauteur et sa largeur et renvoie l'aire de la forme.

À l'école, on nous apprend que les carrés sont des rectangles. Cela suggère que si nous modélisons notre programme à notre objet géométrique, un Carré la classe devrait prolonger un Rectangle classe. A quoi ressemblerait une telle classe?

classe Square s étend Rectangle // Quel code écrire ici? 

J'ai du mal à comprendre quoi écrire dans le Carré classe. Nous avons plusieurs options. Nous pourrions remplacer le surface() méthode et retourne le carré de $ largeur:

class Rectangle protected $ width; hauteur protégée; //… // classe Square s étend Rectangle function area () return $ this-> width ^ 2; 

Notez que j'ai changé RectangleLes champs de protégé, donnant Carré accès à ces champs. Cela semble raisonnable d'un point de vue géométrique. Un carré a des côtés égaux; retourner le carré de largeur est raisonnable.

Cependant, nous avons un problème du point de vue de la programmation. Si Carré est une Rectangle, nous ne devrions avoir aucun problème à le nourrir dans le Géométrie classe. Mais, ce faisant, vous pouvez voir que Géométriele code n'a pas beaucoup de sens; il définit deux valeurs différentes pour la hauteur et la largeur. C'est pourquoi un carré n'est pas un rectangle en programmation. LSP violé.

Principe de séparation des interfaces (ISP)

Les tests unitaires doivent être rapides - très rapides.

Ce principe se concentre sur la transformation de grandes interfaces en petites interfaces spécialisées. L'idée de base est que différents consommateurs d'une même classe ne doivent pas connaître les différentes interfaces, mais uniquement les interfaces que le consommateur doit utiliser. Même si un consommateur n'utilise pas directement toutes les méthodes publiques sur un objet, cela dépend toujours de toutes les méthodes. Alors, pourquoi ne pas fournir des interfaces qui déclarent uniquement les méthodes dont chaque utilisateur a besoin?

Cela concorde étroitement avec le fait que les interfaces doivent appartenir aux clients et non à la mise en œuvre. Si vous adaptez vos interfaces aux classes consommatrices, elles respecteront les FAI. L'implémentation elle-même peut être unique, comme une classe peut implémenter plusieurs interfaces.

Imaginons que nous implémentions une application boursière. Nous avons un courtier qui achète et vend des actions, et il peut déclarer ses gains et pertes quotidiens. Une implémentation très simple inclurait quelque chose comme un Courtier interface, un NYSEBroker classe qui implémente Courtier et quelques classes d’interface utilisateur: une pour la création de transactions (TransactionsUI) et un pour les rapports (DailyReporter). Le code pour un tel système pourrait être similaire à celui-ci:

interface Broker function buy (symbole $, $ volume); fonction sell (symbole $, $ volume); function dailyLoss ($ date); function dailyAarnings ($ date);  La classe NYSEBroker implémente Broker public function buy (symbole, $ volume) // implémentation va ici public fonction currentBalance () // implémentation va ici public fonction dailyEarnings ($ date) // implémentation va ici public function dailyLoss ($ date) // implémentation va ici fonction publique vendre (symbole $, $ volume) // implémentation va ici classe TransactionsUI private $ broker; function __construct (courtier $ courtier) $ this-> courtier = $ courtier;  function buyStocks () // Logique d'interface utilisateur ici pour obtenir des informations d'un formulaire dans $ data $ this-> courtier>> buy ($ data ['sybmol'], $ data ['volume']);  function sellStocks () // Logique d'interface utilisateur ici pour obtenir des informations d'un formulaire dans $ data $ this-> broker-> sell ($ data ['sybmol'], $ data ['volume']);  class DailyReporter private $ broker; function __construct (courtier $ courtier) $ this-> courtier = $ courtier;  function currentBalance () echo 'Balace actuel pour aujourd'hui'. date (heure ()). "\ n"; echo 'Gains:'. $ this-> broker-> dailyEarnings (time ()). "\ n"; echo 'Pertes:'. $ this-> broker-> dailyLoss (time ()). "\ n"; 

Bien que ce code puisse fonctionner, il enfreint le fournisseur de services Internet. Tous les deux DailyReporter et TransactionUI dépend de la Courtier interface. Cependant, ils n'utilisent chacun qu'une fraction de l'interface. TransactionUI utilise le acheter() et vendre() méthodes, tout en DailyReporter utilise le tous les jours () et dailyLoss () les méthodes.

Vous pouvez argumenter que Courtier n'est pas cohésif car il a des méthodes qui ne sont pas liées et qui ne vont donc pas ensemble.

Cela peut être vrai, mais la réponse dépend des implémentations de Courtier; les ventes et les achats peuvent être fortement liés aux pertes et aux bénéfices actuels. Par exemple, vous pouvez ne pas être autorisé à acheter des actions si vous perdez de l'argent.

Vous pouvez aussi dire que Courtier viole également SRP. Parce que nous avons deux classes qui l'utilisent de différentes manières, il peut y avoir deux utilisateurs différents. Eh bien, je dis non. Le seul utilisateur est probablement le courtier lui-même. Il / elle veut acheter, vendre et voir leurs fonds actuels. Mais encore une fois, la réponse dépend du système et des activités dans leur ensemble..

ISP est sûrement violé. Les deux classes d'interface utilisateur dépendent de l'ensemble Courtier. C'est un problème courant si vous pensez que les interfaces appartiennent à leurs implémentations. Toutefois, le fait de déplacer votre point de vue peut suggérer le design suivant:

interface BrokerTransactions function buy (symbole $, $ volume); fonction sell (symbole $, $ volume);  interface BrokerStatistics fonction dailyLoss ($ date); function dailyAarnings ($ date);  La classe NYSEBroker implémente BrokerTransactions, BrokerStatistics fonction publique buy (symbole, $ volume) // implémentation va ici fonction publique currentBalance () // implémentation va ici  fonction publique dailyLoss ($ date) // implémentation va ici fonction publique sell (symbole $, $ volume) // implémentation va ici classe TransactionsUI private $ broker; function __construct (BrokerTransactions $ broker) $ this-> broker = $ broker;  function buyStocks () // Logique d'interface utilisateur ici pour obtenir des informations d'un formulaire dans $ data $ this-> courtier>> buy ($ data ['sybmol'], $ data ['volume']);  function sellStocks () // Logique d'interface utilisateur ici pour obtenir des informations d'un formulaire dans $ data $ this-> broker-> sell ($ data ['sybmol'], $ data ['volume']);  class DailyReporter private $ broker; function __construct (BrokerStatistics $ broker) $ this-> broker = $ broker;  function currentBalance () echo 'Balace actuel pour aujourd'hui'. date (heure ()). "\ n"; echo 'Gains:'. $ this-> broker-> dailyEarnings (time ()). "\ n"; echo 'Pertes:'. $ this-> broker-> dailyLoss (time ()). "\ n"; 

Cela a du sens et respecte le FAI. DailyReporter ne dépend que de BrokerStatistics; il s'en moque et ne doit pas connaître les opérations de vente et d'achat. TransactionsUI, d'autre part, sait seulement sur l'achat et la vente. le NYSEBroker est identique à notre classe précédente, sauf qu'il implémente maintenant le CourtierTransactions et BrokerStatistics interfaces.

Vous pouvez considérer le FAI comme un principe de cohésion de niveau supérieur.

Lorsque les deux classes d’UI dépendaient de la Courtier d’interface, elles ressemblaient à deux classes, chacune ayant quatre champs, dont deux étaient utilisés dans une méthode et les deux autres dans une autre. La classe n'aurait pas été très cohésive.

Un exemple plus complexe de ce principe peut être trouvé dans l'un des premiers articles de Robert C. Martin sur le sujet: The Principe Segregation Principle.

Principe d'inversion de dépendance (DIP)

Ce principe stipule que les modules de haut niveau ne doivent pas dépendre de modules de bas niveau; les deux devraient dépendre des abstractions. Les abstractions ne doivent pas dépendre des détails; les détails devraient dépendre des abstractions. En termes simples, vous devriez dépendre autant que possible d'abstractions et jamais de mises en œuvre concrètes.

Le truc avec DIP est que vous voulez inverser la dépendance, mais que vous voulez toujours garder le flux de contrôle. Passons en revue notre exemple de l'OCP (le Commutateur et Lumière Des classes). Dans la mise en œuvre initiale, nous avions un commutateur contrôlant directement une lumière.

Comme vous pouvez le constater, la dépendance et le contrôle découlent de Commutateur vers Lumière. Bien que ce soit ce que nous voulons, nous ne voulons pas dépendre directement de Lumière. Nous avons donc introduit une interface.

Il est étonnant de voir à quel point la simple introduction d’une interface permet à notre code de respecter à la fois DIP et OCP. Comme vous pouvez le constater, la classe dépend de la mise en œuvre concrète de Lumière, et les deux Lumière et Commutateur dépend de la Commutable interface. Nous avons inversé la dépendance et le flux de contrôle était inchangé.


Conception de haut niveau

Un autre aspect important de votre code est votre conception de haut niveau et votre architecture générale. Une architecture enchevêtrée produit un code difficile à modifier. Garder une architecture propre est essentiel, et la première étape consiste à comprendre comment séparer les différentes préoccupations de votre code..

Dans cette image, j'ai tenté de résumer les principales préoccupations. Notre logique métier est au centre du schéma. Il devrait être bien isolé du reste du monde, et pouvoir travailler et se comporter comme prévu sans l'existence d'aucun des autres éléments. Voyez-le comme orthogonalité à un niveau supérieur.

En partant de la droite, vous avez votre "principal" - le point d'entrée de l'application - et les usines qui créent des objets. Une solution idéale obtiendrait ses objets d'usines spécialisées, mais c'est en grande partie impossible ou peu pratique. Néanmoins, vous devez utiliser les usines lorsque vous en avez la possibilité et les garder en dehors de votre logique métier..

Ensuite, en bas (en orange), nous avons la persistance (bases de données, accès aux fichiers, communications réseau) pour la persistance des informations. Aucun objet dans notre logique métier ne devrait savoir comment fonctionne la persistance.

À gauche, le mécanisme de livraison.

Un MVC, comme Laravel ou CakePHP, ne devrait être que le mécanisme de livraison, rien de plus.

Cela vous permet d'échanger un mécanisme avec un autre sans toucher à votre logique métier. Cela peut paraître scandaleux pour certains d'entre vous. On nous dit que notre logique commerciale devrait être placée dans nos modèles. Eh bien, je ne suis pas d'accord. Nos modèles doivent être des "modèles de requête", c’est-à-dire des objets de données idiots utilisés pour transmettre des informations de MVC à la logique applicative. En option, je ne vois pas de problème à inclure la validation des entrées dans les modèles, mais rien de plus. La logique métier ne devrait pas être dans les modèles.

Lorsque vous examinez l'architecture de votre application ou la structure de répertoires de votre application, vous devriez voir une structure suggérant ce que fait le programme par opposition à la structure ou à la base de données que vous avez utilisée.

Enfin, assurez-vous que toutes les dépendances pointent vers notre logique métier. Les interfaces utilisateur, les usines, les bases de données sont des implémentations très concrètes, et vous ne devriez jamais en dépendre. Inverser les dépendances pour qu'elles pointent vers notre logique métier modularise notre système, ce qui nous permet de changer de dépendance sans modifier la logique métier.


Quelques réflexions sur les modèles de conception

Les modèles de conception jouent un rôle important en facilitant la modification du code en offrant une solution de conception commune que chaque programmeur peut comprendre. D'un point de vue structurel, les modèles de conception sont évidemment avantageux. Ce sont des solutions bien testées et réfléchies.

Si vous souhaitez en savoir plus sur les modèles de conception, j'ai créé un cours Tuts + Premium sur eux.!


La force du test

Le développement piloté par les tests encourage la rédaction d'un code facile à tester. TDD vous oblige à respecter la plupart des principes ci-dessus afin de rendre votre code facile à tester. L'injection de dépendances et l'écriture de classes orthogonales sont essentielles; sinon, vous vous retrouvez avec des méthodes de test énormes. Les tests unitaires doivent être rapides - très rapides, en fait, et tout ce qui n'a pas été testé doit être ridiculisé. Se moquer de nombreuses classes complexes pour un test simple peut être accablant. Donc, lorsque vous vous retrouvez en train de vous moquer de dix objets pour tester une seule méthode sur une classe, vous pouvez avoir un problème avec votre code… pas votre test.


Dernières pensées

En fin de compte, tout dépend de votre souci du code source. Avoir des connaissances techniques ne suffit pas; vous devez appliquer cette connaissance encore et encore, jamais satisfait à 100% de votre code. Vous devez vouloir rendre vo