Ancien code. Code laid. Code compliqué. Code spaghetti. Gibberish absurdité. En deux mots, Code hérité. C’est une série qui vous aidera à travailler et à vous en occuper.
Il est maintenant temps de parler d'architecture et de la manière dont nous organisons nos nouvelles couches de code. Il est temps de prendre notre application et d'essayer de la mapper à la conception architecturale théorique.
C'est quelque chose que nous avons vu tout au long de nos articles et tutoriels. Architecture propre.
À un niveau élevé, il ressemble au schéma ci-dessus et je suis sûr que vous le connaissez déjà. C’est une solution architecturale proposée par Robert C. Martin.
Notre logique d’affaires est au centre de notre architecture. Ce sont les classes représentant les processus métier que notre application tente de résoudre. Ce sont les entités et les interactions représentant le domaine de notre problème.
Ensuite, il existe plusieurs autres types de modules ou de classes autour de notre logique métier. Ceux-ci peuvent être vus comme de simples modules auxiliaires d'aide. Ils ont différents objectifs et la plupart d'entre eux sont indispensables. Ils assurent la connexion entre l'utilisateur et notre application via un mécanisme de diffusion. Dans notre cas, il s’agit d’une interface en ligne de commande. Il existe un autre ensemble de classes auxiliaires qui connectent notre logique métier à notre couche de persistance et à toutes les données de cette couche, mais nous n'avons pas une telle couche dans notre application. Ensuite, il y a les classes d'aide telles que les usines et les constructeurs qui construisent et fournissent de nouveaux objets à notre logique métier. Enfin, il y a les classes représentant le point d'entrée de notre système. Dans notre cas, GameRunner
peut être considéré comme une telle classe, ou tous nos tests sont aussi des points d’entrée à leur manière.
Le point le plus important à noter sur le diagramme est le sens de la dépendance. Toutes les classes auxiliaires dépendent de la logique métier. La logique métier ne dépend de rien d'autre. Si tous les objets de notre logique métier pouvaient apparaître comme par magie, avec toutes les données qu'ils contiennent, et que nous pouvions voir tout ce qui se passait directement dans notre ordinateur, ils devraient pouvoir fonctionner. Notre logique métier doit pouvoir fonctionner sans interface utilisateur ni couche de persistance. Notre logique métier doit exister isolée, dans une bulle d'un univers logique.
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.
Ça y est, le dernier principe SOLID et probablement celui qui a le plus grand impact sur votre code. C'est à la fois simple à comprendre et simple à mettre en œuvre.
En termes simples, il est dit que les choses concrètes doivent toujours dépendre de choses abstraites. Votre base de données est très concrète, elle devrait donc dépendre de quelque chose de plus abstrait. Votre interface utilisateur est très concrète, elle devrait donc dépendre de quelque chose de plus abstrait. Vos usines sont à nouveau très concrètes. Mais qu'en est-il de votre logique d'entreprise. Dans votre logique métier, vous devez continuer à appliquer ces idées afin que les classes les plus proches des limites dépendent de classes plus abstraites, plus au cœur de votre logique métier..
Une logique métier pure représente de manière abstraite les processus et les comportements d'un domaine ou d'un modèle d'entreprise défini. Une telle logique métier ne contient pas de détails (éléments concrets) tels que des valeurs, de l'argent, des noms de compte, des mots de passe, la taille d'un bouton ou le nombre de champs d'un formulaire. La logique métier ne devrait pas se soucier de choses concrètes. Il ne devrait s'occuper que de vos processus métier.
Ainsi, le principe d'inversion de dépendance (DIP) dit que nous devrions inverser nos dépendances chaque fois qu'il y a du code qui dépend de quelque chose de concret. En ce moment, notre structure de dépendance ressemble à ceci.
GameRunner
, en utilisant les fonctions de RunnerFunctions.php
crée un Jeu
classe et ensuite l'utilise. D'autre part, notre Jeu
classe, représentant notre logique métier, crée et utilise un Afficher
objet.
Le coureur dépend donc de notre logique métier. C'est correct. D'autre part, notre Jeu
dépend de Afficher
, ce qui n'est pas bon. Notre logique métier ne devrait jamais dépendre de notre présentation.
L'astuce technique la plus simple que nous puissions faire consiste à utiliser les constructions abstraites dans notre langage de programmation. Une classe traditionnelle est plus concrète qu'une classe abstraite, mais plus concrète qu'une interface.
Un Classe abstraite est un type spécial qui ne peut pas être initialisé. Il ne contient que des définitions et des implémentations partielles. Une classe de base abstraite a généralement plusieurs classes enfants. Ces classes enfant héritent de la fonctionnalité partielle commune du parent abstrait, elles ajoutent leur propre comportement étendu et doivent implémenter toutes les méthodes définies dans le parent abstrait mais non implémentées..
Un Interface est un type spécial qui permet uniquement la définition de méthodes et de variables. C'est la construction la plus abstraite de la programmation orientée objet. Toute implémentation doit toujours implémenter toutes les méthodes de son interface parent. Une classe concrète peut implémenter plusieurs interfaces.
À l'exception des langages orientés objet de la famille C, les autres, comme Java ou PHP, n'autorisent pas l'héritage multiple. Ainsi, une classe concrète peut étendre une seule classe abstraite, mais elle peut implémenter plusieurs interfaces, même en même temps si nécessaire. Ou, d'un autre point de vue, une classe abstraite unique peut avoir de nombreuses implémentations, alors que de nombreuses interfaces peuvent avoir de nombreuses implémentations..
Pour une explication plus complète du DIP, veuillez lire le tutoriel consacré à ce principe SOLID..
PHP supporte pleinement les interfaces. À partir du Afficher
En tant que modèle, nous pourrions définir une interface avec les méthodes publiques que toutes les classes responsables de l'affichage des données devront implémenter..
Regarder Afficher
Dans la liste des méthodes de, il y a 12 méthodes publiques, y compris le constructeur. Il s’agit d’une interface assez volumineuse; vous devez garder ce nombre le plus bas possible, en exposant les interfaces selon les besoins des clients. Le principe de séparation des interfaces a de bonnes idées à ce sujet. Peut-être que nous allons essayer de résoudre ce problème dans un prochain tutoriel.
Ce que nous voulons réaliser maintenant est une architecture comme celle ci-dessous..
De cette façon, au lieu de Jeu
en fonction du plus concret Afficher
, ils dépendent tous les deux de l'interface très abstraite. Jeu
utilise l'interface, tandis que Afficher
le met en œuvre.
Phil Karlton a déclaré: "En informatique, il n’ya que deux choses difficiles: l’invalidation de la mémoire cache et la dénomination."
Même si nous nous moquons des caches, nous devons nommer nos classes, variables et méthodes. Nommer les interfaces peut être un défi.
A l'époque de la notation hongroise, nous l'aurions fait de cette façon.
Pour ce diagramme, nous avons utilisé les noms de classe / fichier actuels et la capitalisation réelle. L'interface s'appelle "IDisplay" avec un "I" majuscule devant "Display". Il existait en réalité des langages de programmation nécessitant un tel nommage pour les interfaces. Je suis sûr que quelques lecteurs les utilisent encore et sourient en ce moment..
Le problème avec ce schéma de nommage est la préoccupation mal placée. Les interfaces appartiennent à leurs clients. Notre interface appartient à Jeu
. Ainsi Jeu
ne doit pas savoir qu'il utilise une interface ou un objet réel. Jeu
ne doit pas être préoccupé par la mise en œuvre qu'il obtient réellement. De Jeu
Le point de vue de, il utilise simplement un "affichage", c'est tout.
Cela résout le Jeu
à Afficher
problème de nommage. L'utilisation du suffixe "Impl" pour la mise en œuvre est un peu mieux. Cela aide à éliminer le problème de Jeu
.
C'est aussi beaucoup plus efficace pour nous. Penser à Jeu
comme il semble en ce moment. Il utilise un Afficher
objet et sait comment l'utiliser. Si nous appelons notre interface "Affichage", nous réduirons le nombre de modifications nécessaires dans Jeu
.
Mais encore, cette dénomination est juste légèrement meilleure que la précédente. Il permet une seule mise en œuvre pour Afficher
et le nom de la mise en œuvre ne nous dira pas de quel type d'affichage nous parlons.
Maintenant c'est beaucoup mieux. Notre implémentation s'appelait "CLIDisplay", car elle sortait vers la CLI. Si nous voulons une sortie HTML ou une interface utilisateur de bureau Windows, nous pouvons facilement ajouter tout cela à notre architecture..
Comme nous disposons de deux types de tests, le maître lent et les tests unitaires rapides, nous souhaitons nous appuyer autant que possible sur les tests unitaires et le moins possible sur le maître doré. Donc, marquons nos tests Golden Master comme ignorés et essayons de nous appuyer sur nos tests unitaires. Ils sont en train de passer et nous voulons faire un changement qui les maintiendra. Mais comment pouvons-nous faire une telle chose, sans faire tous les changements proposés ci-dessus?
Existe-t-il un moyen de tester qui nous permettrait de faire un plus petit pas?
Il y a un tel chemin. En test, il existe un concept appelé "Mocking".
Wikipedia définit Mocking en tant que tel: "Dans la programmation orientée objet, les objets fictifs sont des objets simulés reproduisant de manière contrôlée le comportement d'objets réels."
Un tel objet serait d'une grande aide pour nous. En fait, nous n'avons même pas besoin de quelque chose d'aussi complexe que de simuler tous les comportements. Tout ce dont nous avons besoin est un faux objet stupide que nous pouvons envoyer à Jeu
au lieu de la vraie logique d'affichage.
Créons une interface appelée Afficher
avec toutes les méthodes publiques de la classe concrète actuelle.
Comme vous pouvez le constater, l'ancien Display.php
a été renommé en DisplayOld.php
. Ceci est juste une étape temporaire, qui nous permet de le sortir de la route et de nous concentrer sur l'interface.
interface d'affichage
C'est tout ce qu'il y a à créer une interface. Vous pouvez voir qu'il est défini comme "interface" et non comme une "classe". Ajoutons les méthodes.
interface Display function statusAfterRoll ($ rolledNumber, $ currentPlayer); fonction playerSentToPenaltyBox ($ currentPlayer); fonction playerStaysInPenaltyBox ($ currentPlayer); fonction statusAfterNonPenalizedPlayerMove ($ currentPlayer, $ currentPlace, $ currentCategory); fonction statusAfterPlayerGettingOutOfPenaltyBox ($ currentPlayer, $ currentPlace, $ currentCategory); fonction playerAdded ($ playerName, $ numberOfPlayers); fonction askQuestion ($ currentCategory); function correctAnswer (); function correctAnswerWithTypo (); function incorrectAnswer (); fonction playerCoins ($ currentPlayer, $ playerCoins);
Oui. Une interface est juste un tas de déclarations de fonctions. Imaginez-le comme un fichier d’en-tête en C. Aucune implémentation, juste des déclarations. Il ne peut pas tenir une implémentation du tout. Si vous essayez d'implémenter l'une des méthodes, cela entraînera une erreur.
Mais ces définitions très abstraites nous permettent quelque chose de merveilleux. Notre Jeu
la classe en dépend maintenant, au lieu d’une implémentation concrète. Cependant, si nous essayons d'exécuter nos tests, ils échoueront.
Erreur fatale: impossible d'instancier l'interface d'affichage
C'est parce que Jeu
tente de créer seul un nouvel affichage à la ligne 25, dans le constructeur.
Nous savons que nous ne pouvons pas faire cela. Une interface ou une classe abstraite ne peut pas être instanciée. Nous avons besoin d'un objet réel.
Nous avons besoin d'un objet factice pour nos tests. Une classe simple, mettant en œuvre toutes les méthodes de la Afficher
interface, mais ne fait rien. Écrivons-le directement dans notre test unitaire. Si votre langage de programmation n'autorise pas plusieurs classes dans le même fichier, n'hésitez pas à créer un nouveau fichier pour votre classe fictive..
La classe DummyDisplay implémente la méthode Display function statusAfterRoll ($ rolledNumber, $ currentPlayer) // TODO: Implemente statusAfterRoll (). function playerSentToPenaltyBox ($ currentPlayer) // TODO: Implémente la méthode playerSentToPenaltyBox (). function playerStaysInPenaltyBox ($ currentPlayer) // TODO: implémentez la méthode playerStaysInPenaltyBox (). function statusAfterNonPenalizedPlayerMove ($ currentPlayer, $ currentPlace, $ currentCategory) // TODO: implémente la méthode statusAfterNonPenalizedPlayerMove (). function statusAfterPlayerGettingOutOfPenaltyBox ($ currentPlayer, $ currentPlace, $ currentCategory) // TODO: implémente la méthode statusAfterPlayerGettingOutOfPenaltyBox (). function playerAdded ($ playerName, $ numberOfPlayers) // TODO: méthode Implement playerAdded (). function askQuestion ($ currentCategory) // TODO: Implémente la méthode askQuestion (). function correctAnswer () // TODO: Implémente la méthode correctAnswer (). function correctAnswerWithTypo () // TODO: Implémente la méthode correctAnswerWithTypo (). function incorrectAnswer () // TODO: Implémente la méthode incorrectAnswer (). function playerCoins ($ currentPlayer, $ playerCoins) // TODO: implémentez la méthode playerCoins ().
Dès que vous dites que votre classe implémente une interface, l'EDI vous permettra de renseigner automatiquement les méthodes manquantes. Cela rend la création de tels objets très rapide, en quelques secondes.
Maintenant, utilisons-le dans Jeu
en l'initialisant dans son constructeur.
function __construct () $ this-> players = array (); $ this-> places = array (0); $ this-> purses = array (0); $ this-> inPenaltyBox = array (0); $ this-> display = new DummyDisplay ();
Cela rend le test réussi, mais introduit un énorme problème. Jeu
doit savoir sur son test. Nous ne voulons vraiment pas cela. Un test n'est qu'un autre point d'entrée. le DummyDisplay
est juste une autre interface utilisateur. Notre logique métier, la Jeu
classe, ne devrait pas dépendre de l'interface utilisateur. Alors faisons en sorte que cela dépende uniquement de l'interface.
function __construct (Display $ display) $ this-> players = array (); $ this-> places = array (0); $ this-> purses = array (0); $ this-> inPenaltyBox = array (0); $ this-> display = $ display;
Mais pour tester Jeu
, nous devons envoyer l'affichage factice de nos tests.
fonction setUp () $ this-> game = new Game (nouveau DummyDisplay ());
C'est tout. Nous devions modifier une seule ligne dans nos tests unitaires. Dans la configuration, nous allons envoyer, en tant que paramètre, une nouvelle instance de DummyDisplay
. C'est une injection de dépendance. L'utilisation d'interfaces et l'injection de dépendance sont particulièrement utiles si vous travaillez en équipe. Chez Syneto, nous avons observé que spécifier un type d'interface pour une classe et l'injecter nous aiderait à mieux communiquer les intentions du code client. Toute personne regardant le client saura quel type d'objet est utilisé dans les paramètres. Et un bonus intéressant est que votre IDE complétera automatiquement les méthodes pour ces paramètres car il peut déterminer leurs types.
Le test du maître d'or, exécute notre code comme dans le monde réel. Pour le faire passer, nous devons transformer notre ancienne classe d’affichage en une implémentation réelle de l’interface et l’envoyer dans notre logique métier. Voici une façon de le faire.
La classe CLIDisplay implémente Display //… //
Renommez le en CLIDisplay
et le faire mettre en œuvre Afficher
.
function run () $ display = new CLIDisplay (); $ aGame = new Game ($ display); $ aGame-> add ("Chet"); $ aGame-> add ("Pat"); $ aGame-> add ("Sue"); est-ce que $ dés = rand (0, 5) + 1; $ aGame-> roll ($ dés); while (! didSomebodyWin ($ aGame, isCurrentAnswerCorrect ()));
Dans RunnerFunctions.php
, dans le courir()
fonction, créez un nouvel affichage pour CLI et transmettez-le à Jeu
quand il est créé.
Décommentez et exécutez vos tests Golden Master. Ils vont passer.
Cette solution conduit efficacement à une architecture comme dans le schéma ci-dessous.
Alors maintenant, notre coureur de jeu, qui est le point d'entrée de notre application, crée un béton CLIDisplay
et donc en dépend. CLIDisplay
dépend uniquement de l'interface située entre la présentation et la logique métier. Notre coureur dépend également directement de la logique commerciale. Voici à quoi ressemble notre application lorsqu'elle est projetée sur l'architecture claire avec laquelle nous avons commencé cet article.
Merci de votre lecture et ne manquez pas le prochain tutoriel où nous parlerons plus en détail de l'interaction moqueuse et de la classe.