Responsabilité unique (SRP), ouvert / fermé (OCP), substitution de Liskov, ségrégation d'interface et inversion de dépendance. Cinq principes agiles qui devraient vous guider à chaque fois que vous devez écrire du code.
Les entités logicielles (classes, modules, fonctions, etc.) doivent être ouvertes pour extension, mais fermées pour modification..
Le principe Open / Closed, OCP, est à mettre au crédit de Bertrand Mayer, un programmeur français, qui l’a publié pour la première fois dans son livre n Construction de logiciels orientés objet, en 1988..
Ce principe a gagné en popularité au début des années 2000, devenant l’un des principes SOLID définis par Robert C. Martin dans son livre intitulé Agile Software Development, Principes, modèles et pratiques, puis republié dans la version C # du livre Agile Principles, Patterns. et pratiques en C #.
Nous parlons ici essentiellement de concevoir nos modules, classes et fonctions de manière à ce qu’une nouvelle fonctionnalité soit nécessaire, nous ne devrions pas modifier notre code existant, mais plutôt écrire un nouveau code qui sera utilisé par le code existant. Cela semble un peu étrange, surtout si nous travaillons dans des langages tels que Java, C, C ++ ou C # où cela s’applique non seulement au code source lui-même mais aussi au binaire. Nous voulons créer de nouvelles fonctionnalités de manière à ne pas nécessiter de redéployer des fichiers binaires, exécutables ou DLL existants..
Au fur et à mesure que nous progressons dans ces tutoriels, nous pouvons placer chaque nouveau principe dans le contexte de ceux déjà discutés. Nous avons déjà discuté de la responsabilité unique (SRP) qui stipulait qu'un module ne devrait avoir qu'une seule raison de changer. Si nous pensons à OCP et à SRP, nous pouvons constater qu’ils sont complémentaires. Un code spécialement conçu avec SRP à l'esprit sera proche des principes de l'OCP ou facile à lui faire respecter. Lorsque le code a une seule raison de changer, l'introduction d'une nouvelle fonctionnalité crée une raison secondaire pour cette modification. Donc, SRP et OCP seraient violés. De la même manière, si nous avons un code qui ne devrait changer que lorsque sa fonction principale change et devrait rester inchangé quand une nouvelle fonctionnalité est ajoutée, respectant ainsi OCP, respectera principalement SRP..
Cela ne signifie pas que SRP mène toujours à OCP ou vice versa, mais dans la plupart des cas, si l'un d'entre eux est respecté, atteindre le second est assez simple..
D'un point de vue purement technique, le principe d'ouverture / fermeture est très simple. Une relation simple entre deux classes, comme celle ci-dessous, viole l'OCP.
le Utilisateur
la classe utilise le Logique
classe directement. Si nous devons mettre en place un deuxième Logique
classe d’une manière qui nous permettra d’utiliser à la fois l’actuel et le nouveau, l’existant Logique
la classe devra être changée. Utilisateur
est directement liée à la mise en œuvre de Logique
, il n'y a aucun moyen pour nous de fournir une nouvelle Logique
sans affecter l'actuel. Et quand on parle de langages statiquement typés, il est très possible que le Utilisateur
La classe nécessitera également des changements. Si nous parlons de langages compilés, très certainement les deux Utilisateur
exécutable et le Logique
Une bibliothèque exécutable ou dynamique nécessitera une recompilation et un redéploiement sur nos clients, processus que nous souhaitons éviter autant que possible..
En se basant uniquement sur le schéma ci-dessus, on peut en déduire que toute classe utilisant directement une autre classe violerait le principe Open / Closed. Et c'est vrai, à proprement parler. J'ai trouvé assez intéressant de trouver les limites, le moment où vous tracez la ligne et décidez qu'il est plus difficile de respecter OCP que de modifier le code existant, ou que le coût architectural ne justifie pas le coût de modification du code existant..
Supposons que nous voulions écrire une classe pouvant fournir une progression sous forme de pourcentage d'un fichier téléchargé via notre application. Nous aurons deux classes principales, une Le progrès
et un Fichier
, et j'imagine que nous voudrons les utiliser comme dans le test ci-dessous.
fonction testItCanGetTheProgressOfAFileAsAPercent () $ file = new File (); $ fichier-> longueur = 200; $ fichier-> envoyé = 100; $ progress = nouveau progrès (fichier $); $ this-> assertEquals (50, $ progress-> getAsPercent ());
Dans ce test, nous sommes un utilisateur de Le progrès
. Nous voulons obtenir une valeur sous forme de pourcentage, quelle que soit la taille du fichier. Nous utilisons Fichier
comme source d'information pour notre Le progrès
. Un fichier a une longueur en octets et un champ appelé envoyé
représentant la quantité de données envoyées à celui qui effectue le téléchargement. Nous ne nous soucions pas de la façon dont ces valeurs sont mises à jour dans l'application. Nous pouvons supposer qu’une logique magique le fait pour nous, donc dans un test, nous pouvons les définir explicitement.
class File public $ length; public $ envoyé;
le Fichier
La classe n'est qu'un simple objet de données contenant les deux champs. Bien sûr, dans la vie réelle, il contiendrait probablement d'autres informations et comportements, comme le nom de fichier, le chemin d'accès, le chemin relatif, le répertoire actuel, le type, les autorisations, etc..
class Progress private $ file; function __construct (Fichier $ fichier) $ this-> fichier = $ fichier; function getAsPercent () return $ this-> fichier-> envoyé * 100 / $ this-> fichier-> longueur;
Le progrès
est simplement une classe prenant un Fichier
dans son constructeur. Pour plus de clarté, nous avons spécifié le type de la variable dans les paramètres du constructeur. Il existe une seule méthode utile sur Le progrès
, getAsPercent ()
, qui prendra les valeurs envoyées et la longueur de Fichier
et les transformer en un pour cent. Simple, et ça marche.
Les tests ont commencé à 17h39… PHPUnit 3.7.28 de Sebastian Bergmann… Temps: 15 ms, Mémoire: 2.50 Mo OK (1 test, 1 assertion)
Ce code semble être correct, mais il enfreint le principe Open / Closed. Mais pourquoi? Et comment?
Chaque application devant évoluer dans le temps aura besoin de nouvelles fonctionnalités. Une nouvelle fonctionnalité de notre application pourrait être d'autoriser le streaming de musique au lieu de simplement télécharger des fichiers.. Fichier
La longueur de 's est représentée en octets, la durée de la musique en secondes. Nous voulons offrir une belle barre de progression à nos auditeurs, mais pouvons-nous réutiliser celle que nous avons déjà?
Non, nous ne pouvons pas. Notre progrès est lié à Fichier
. Il ne comprend que les fichiers, même s'il peut également être appliqué au contenu musical. Mais pour ce faire, nous devons le modifier, nous devons faire Le progrès
savoir a propos La musique
et Fichier
. Si notre conception devait respecter OCP, nous n'aurions pas besoin de toucher Fichier
ou Le progrès
. Nous pourrions simplement réutiliser le système existant Le progrès
et l'appliquer à La musique
.
Les langages à typage dynamique ont l'avantage de deviner les types d'objets au moment de l'exécution. Cela nous permet de supprimer le typehint de Le progrès
'constructeur et le code fonctionnera toujours.
class Progress private $ file; function __construct ($ file) $ this-> file = $ file; function getAsPercent () return $ this-> fichier-> envoyé * 100 / $ this-> fichier-> longueur;
Maintenant, nous pouvons tout jeter à Le progrès
. Et par n'importe quoi, je veux dire littéralement n'importe quoi:
classe Music public $ length; public $ envoyé; artiste public $; public $ album; public $ releaseDate; fonction getAlbumCoverFile () retourne 'Images / Covers /'. $ this-> artiste. '/' $ this-> album. '.png';
Et un La musique
Une classe comme celle ci-dessus fonctionnera parfaitement. Nous pouvons le tester facilement avec un test très similaire à Fichier
.
fonction testItCanGetTheProgressOfAMusicStreamAsAPercent () $ music = new Music (); $ music-> length = 200; $ music-> envoyé = 100; $ progress = nouveau progrès ($ music); $ this-> assertEquals (50, $ progress-> getAsPercent ());
Donc, fondamentalement, tout contenu mesurable peut être utilisé avec le Le progrès
classe. Peut-être devrions-nous exprimer cela en code en changeant le nom de la variable également:
classe Progress privé $ mesurableContent; fonction __construct ($ mesurableContent) $ this-> mesurableContent = $ mesurableContent; function getAsPercent () return $ this-> mesurableContent-> envoyé * 100 / $ this-> mesurableContent-> length;
Bien, mais cette approche nous pose un gros problème. Quand nous avions Fichier
spécifié comme une police de caractères, nous étions convaincus de ce que notre classe peut gérer. C'était explicite et si quelque chose d'autre arrivait, une belle erreur nous le disait.
L'argument 1 passé à Progress :: __ construct () doit être une instance de File, une instance de Music donnée.
Mais sans le typehint, nous devons nous appuyer sur le fait que ce qui entre aura deux variables publiques de noms exacts comme "longueur
" et "envoyé
". Sinon, le legs sera refusé.
Legs refusé: classe qui substitue une méthode d'une classe de base de telle sorte que le contrat de la classe de base ne soit pas honoré par la classe dérivée. ~ Source Wikipedia.
C'est l'un des le code sent présenté de manière beaucoup plus détaillée dans le cours premium Detecting Code Smells. En bref, nous ne voulons pas finir par essayer d'appeler des méthodes ou d'accéder à des champs sur des objets non conformes à notre contrat. Quand nous avons eu une frappe, le contrat était spécifié par elle. Les champs et méthodes de la Fichier
classe. Maintenant que nous n’avons plus rien, nous pouvons tout envoyer, même une chaîne, ce qui entraînerait une erreur laide..
fonction testItFailsWithAParameterThatDoesNotRespectTheImplicitContract () $ progress = new Progress ('une chaîne'); $ this-> assertEquals (50, $ progress-> getAsPercent ());
Un test comme celui-ci, où nous envoyons une simple chaîne, produira un legs refusé:
Essayer d'obtenir une propriété de non-objet.
Alors que le résultat final est le même dans les deux cas, ce qui signifie que le code est rompu, le premier a généré un beau message. Celui-ci est cependant très obscur. Il n'y a aucun moyen de savoir quelle est la variable - une chaîne dans notre cas - et quelles propriétés ont été recherchées et non trouvées. Il est difficile de déboguer et de résoudre le problème. Un programmeur doit ouvrir le Le progrès
classe et le lire et le comprendre. Le contrat, dans ce cas, lorsque nous ne spécifions pas explicitement le typehint, est défini par le comportement de Le progrès
. C’est un contrat implicite, connu seulement des Le progrès
. Dans notre exemple, il est défini par l'accès aux deux champs, envoyé
et longueur
, dans le getAsPercent ()
méthode. Dans la vie réelle, le contrat implicite peut être très complexe et difficile à découvrir en cherchant quelques secondes à la classe..
Cette solution est recommandée uniquement si aucune des suggestions ci-dessous ne peut être facilement mise en œuvre ou si elles infligeaient des modifications architecturales sérieuses ne justifiant pas l'effort..
C’est la solution la plus courante et probablement la plus appropriée pour respecter OCP. C'est simple et efficace.
Le modèle de stratégie introduit simplement l'utilisation d'une interface. Une interface est un type particulier d’entité en programmation orientée objet (OOP) qui définit un contrat entre un client et une classe de serveur. Les deux classes adhéreront au contrat pour assurer le comportement attendu. Il peut y avoir plusieurs classes de serveur, non liées, respectant le même contrat, pouvant ainsi servir la même classe de client..
interface Mesurable function getLength (); fonction getSent ();
Dans une interface, nous ne pouvons définir que le comportement. C'est pourquoi, au lieu d'utiliser directement des variables publiques, nous devrons penser à utiliser des accesseurs et des setters. L'adaptation des autres classes ne sera pas difficile à ce stade. Notre IDE peut faire la plupart du travail.
fonction testItCanGetTheProgressOfAFileAsAPercent () $ file = new File (); $ fichier-> setLength (200); $ fichier-> setSent (100); $ progress = nouveau progrès (fichier $); $ this-> assertEquals (50, $ progress-> getAsPercent ());
Comme d'habitude, nous commençons par nos tests. Nous devrons utiliser des paramètres pour définir les valeurs. Si cela est jugé obligatoire, ces régleurs peuvent également être définis dans Mesurable
interface. Cependant, faites attention à ce que vous mettez là. L'interface est à définir le contrat entre la classe de client Le progrès
et les différentes classes de serveur comme Fichier
et La musique
. Est-ce que Le progrès
besoin de définir les valeurs? Probablement pas. Il est donc très peu probable que les setters soient définis dans l'interface. De même, si vous définissez les paramètres à cet endroit, vous obligeriez toutes les classes de serveur à implémenter les paramètres. Pour certains d'entre eux, il peut être logique d'avoir des setters, mais d'autres peuvent se comporter de manière totalement différente. Et si nous voulons utiliser notre Le progrès
classe pour montrer la température de notre four? le FourTempérature
La classe peut être initialisée avec les valeurs du constructeur ou obtenir les informations d'une troisième classe. Qui sait? Avoir des setters sur cette classe serait étrange.
class File implémente Measurable private $ length; $ privé envoyé; public $ filename; public $ owner; fonction setLength ($ length) $ this-> length = $ length; function getLength () return $ this-> length; function setSent ($ envoyé) $ this-> envoyé = $ envoyé; function getSent () return $ this-> envoyé; function getRelativePath () return dirname ($ this-> nom_fichier); function getFullPath () return realpath ($ this-> getRelativePath ());
le Fichier
La classe est légèrement modifiée pour répondre aux exigences ci-dessus. Il met maintenant en œuvre le Mesurable
interface et a setters et getters pour les domaines qui nous intéressent. La musique
est très similaire, vous pouvez vérifier son contenu dans le code source joint. On a presque fini.
classe Progress privé $ mesurableContent; fonction __construct (Mesurable $ mesurableContent) $ this-> mesurableContent = $ mesurableContent; function getAsPercent () return $ this-> mesurableContent-> getSent () * 100 / $ this-> mesurableContent-> getLength ();
Le progrès
également besoin d'une petite mise à jour. Nous pouvons maintenant spécifier un type, en utilisant typehinting, dans le constructeur. Le type attendu est Mesurable
. Maintenant nous avons un contrat explicite. Le progrès
peut être sûr que les méthodes utilisées seront toujours présentes car elles sont définies dans le Mesurable
interface. Fichier
et La musique
peut également être sûr qu'ils peuvent fournir tout ce qui est nécessaire pour Le progrès
en implémentant simplement toutes les méthodes sur l'interface, une exigence lorsqu'une classe implémente une interface.
Ce modèle de conception est expliqué plus en détail dans le cours Agile Design Patterns..
Les gens ont tendance à nommer les interfaces avec une capitale je
devant eux, ou avec le mot "Interface
"attaché à la fin, comme IFile
ou FileInterface
. Il s’agit d’une notation à l’ancienne imposée par certaines normes obsolètes. Nous avons tellement dépassé les notations hongroises ou la nécessité de spécifier le type d'une variable ou d'un objet dans son nom afin de l'identifier plus facilement. Les IDE identifient n'importe quoi en une fraction de seconde pour nous. Cela nous permet de nous concentrer sur ce que nous voulons réellement résumer.
Les interfaces appartiennent à leurs clients. Oui. Lorsque vous voulez nommer une interface, vous devez penser au client et oublier l’implémentation. Lorsque nous avons nommé notre interface Mesurable, nous avons réfléchi au progrès. Si j'étais un progrès, de quoi aurais-je besoin pour pouvoir fournir le pourcentage? La réponse est simple, quelque chose que nous pouvons mesurer. Ainsi le nom Mesurable.
Une autre raison est que la mise en œuvre peut provenir de différents domaines. Dans notre cas, il y a des fichiers et de la musique. Mais nous pouvons très bien réutiliser nos Le progrès
dans un simulateur de course. Dans ce cas, les classes mesurées seraient Vitesse, Carburant, etc. Nice, n’est-ce pas??
Le modèle de conception de la méthode du modèle est très similaire à la stratégie, mais au lieu d'une interface, il utilise une classe abstraite. Il est recommandé d'utiliser un modèle de méthode de modèle lorsque nous avons un client très spécifique à notre application, avec une capacité de réutilisation réduite et lorsque les classes de serveur ont un comportement commun..
Ce modèle de conception est expliqué plus en détail dans le cours Agile Design Patterns..
Alors, comment tout cela affecte notre architecture de haut niveau?
Si l'image ci-dessus représente l'architecture actuelle de notre application, l'ajout d'un nouveau module avec cinq nouvelles classes (les bleues) devrait affecter notre conception de manière modérée (classe rouge)..
Dans la plupart des systèmes, vous ne pouvez vous attendre à aucun effet sur le code existant lorsque de nouvelles classes sont introduites. Cependant, le respect du principe d'ouverture / fermeture réduira considérablement le nombre de classes et de modules nécessitant des modifications constantes..
Comme pour tout autre principe, essayez de ne pas penser à tout avant. Si vous le faites, vous obtiendrez une interface pour chacune de vos classes. Une telle conception sera difficile à maintenir et à comprendre. Habituellement, le moyen le plus sûr d’aller est d’envisager les possibilités et de déterminer s’il existe d’autres types de classes de serveurs. Plusieurs fois, vous pouvez facilement imaginer une nouvelle fonctionnalité ou en trouver une sur le backlog du projet qui produira une autre classe de serveurs. Dans ces cas, ajoutez l'interface depuis le début. Si vous ne pouvez pas déterminer, ou si vous n'êtes pas sûr - la plupart du temps - omettez-le simplement. Laissez le prochain programmeur, ou peut-être même vous-même, ajouter l'interface lorsque vous avez besoin d'une deuxième implémentation..
Si vous suivez votre discipline et ajoutez des interfaces dès qu'un second serveur est requis, les modifications seront peu nombreuses et faciles. N'oubliez pas que si le code requis change une fois, il y a de fortes chances pour qu'il soit à nouveau nécessaire. Lorsque cette possibilité se concrétisera, OCP vous fera économiser beaucoup de temps et d’efforts..
Merci pour la lecture.