Articles

Ouvert / Fermé, selon le principe SOLID

Les entités logicielles (classes, modules, fonctions, etc.) doivent être ouvertes pour extension, mais fermées pour édition.

Concevoir le logiciel: modules, classes et fonctions de telle manière que lorsque de nouvelles fonctionnalités sont nécessaires, nous ne devons pas modifier le code existant mais plutôt écrire un nouveau code qui sera utilisé par le code existant. Cela peut sembler étrange, en particulier avec des langages comme 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 d'une manière qui ne nécessite pas de redistribution des binaires, exécutables ou DLL existants.
OCP dans le contexte SOLID

 

SRP et OCP complémentaires

Nous avons déjà vu le principe SRP de responsabilité unique qui stipule qu'un module ne doit avoir qu'une seule raison de changer. Les principes OCP et SRP sont complémentaires. Le code conçu selon le principe SRP respectera également les principes OCP. Lorsque nous avons du code qui n'a qu'une seule raison de changer, l'introduction d'une nouvelle fonctionnalité créera une raison secondaire pour ce changement. Ainsi, SRP et OCP seraient violés. De même, si nous avons du code qui ne devrait changer que lorsque sa fonction principale change et qui devrait rester inchangé lorsque de nouvelles fonctionnalités sont ajoutées, respectant ainsi l'OCP, il respectera également 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'eux est respecté, la réalisation du second est assez simple.

 

Exemple de violation du principe OCP

D'un point de vue purement technique, le principe ouvert / fermé est très simple. Une simple relation entre deux classes, comme celle ci-dessous, viole le principe OCP.

La classe User utilise directement la classe Logic. Si nous devons implémenter une deuxième classe Logic d'une manière qui nous permet d'utiliser à la fois l'actuelle et la nouvelle, la classe Logic existante devra être modifiée. L'utilisateur est directement lié à la mise en œuvre de la logique, il n'y a aucun moyen pour nous de fournir une nouvelle logique sans affecter la logique actuelle. Et lorsque nous parlons de langages typés statiquement, la classe User est très susceptible de nécessiter également des modifications. Si nous parlons de langages compilés, il est certain que l'exécutable User et l'exécutable Logic ou la bibliothèque dynamique nécessiteront une recompilation et une livraison, qu'il est préférable d'éviter lorsque cela est possible.

En référence au schéma précédent, nous pouvons en déduire que toute classe qui utilise directement une autre classe, pourrait conduire à la violation du principe Open / Closed. 
Supposons que nous voulions écrire une classe capable de fournir la progression "en pourcentage" d'un fichier téléchargé, via notre application. Nous aurons deux classes principales, une progression et un fichier, et je suppose que nous aimerions les utiliser comme suit:

 

function testItCanGetTheProgressOfAFileAsAPercent () {
     $ fichier = nouveau fichier ();
     $ fichier-> longueur = 200;
     $ fichier-> envoyé = 100;
     $ progress = nouvelle progression ($ fichier);
     $ this-> assertEquals (50, $ progress-> getAsPercent ());
}

Dans ce code, nous sommes des utilisateurs Progress. Nous voulons obtenir une valeur en pourcentage, quelle que soit la taille réelle du fichier. Nous utilisons File comme source d'informations. Un fichier a une longueur en octets et un champ appelé envoyé qui représente la quantité de données envoyées au téléchargeur. Peu importe comment ces valeurs sont mises à jour dans l'application. Nous pouvons supposer qu'il y a une logique magique qui fait cela pour nous, donc dans un test, nous pouvons les définir explicitement.

 

Fichier de classe {
     public $ length;
     public $ envoyé;
}

 

La classe File n'est qu'un simple objet de données contenant les deux champs. Bien sûr, il doit également contenir d'autres informations et comportements, tels que le nom de fichier, le chemin, le chemin relatif, le répertoire actuel, le type, les autorisations, etc.

 

Progression de la classe {

     fichier $ privé;

     function __construct (Fichier $ fichier) {
          $ this-> fichier = $ fichier;
     }

     function getAsPercent () {
          return $ this-> file-> sent * 100 / $ this-> file-> length;
     }

}

Progress est simplement une classe qui accepte un fichier dans son constructeur. Pour plus de clarté, nous avons spécifié le type de variable dans les paramètres du constructeur. Il existe une seule méthode utile sur Progress, getAsPercent (), qui prendra les valeurs envoyées et la longueur de File et les transformera en pourcentage. Simple et ça marche.

Ce code semble correct, mais il viole le principe Ouvert / Fermé.

Mais pourquoi

Et comment

 

Essayons de changer les exigences

Chaque application pour évoluer dans le temps aura besoin de nouvelles fonctionnalités. Une nouvelle fonctionnalité de notre application pourrait être d'autoriser la diffusion de musique au lieu de simplement télécharger des fichiers. La longueur du fichier est représentée en octets, la durée de la musique en secondes. Nous voulons offrir une barre de progression à nos auditeurs, mais pouvons-nous réutiliser la classe écrite ci-dessus?

Non, nous ne pouvons pas. Notre progression est liée à File. Il ne peut gérer que les informations sur les fichiers, bien qu'il puisse également être appliqué au contenu musical. Mais pour ce faire, nous devons le modifier, nous devons faire connaître à Progress la musique et les fichiers. Si notre conception était conforme à OCP, nous n'aurions pas besoin de toucher Fichier ou Progression. Nous pourrions simplement réutiliser les progrès existants et les appliquer à la musique.

 

Bulletin d'innovation
Ne manquez pas les nouvelles les plus importantes sur l'innovation. Inscrivez-vous pour les recevoir par email.

Solution possible

Les langages à typage dynamique ont l'avantage de gérer les types d'objets au moment de l'exécution. Cela nous permet de supprimer le typehint du constructeur Progress et le code continuera à fonctionner.

Progression de la classe {

     fichier $ privé;

     function __construct ($ fichier) {
         $ this-> fichier = $ fichier;
     }

    function getAsPercent () {
         return $ this-> file-> sent * 100 / $ this-> file-> length;
     }

}

Nous pouvons maintenant lancer n'importe quoi chez Progress. Et par n'importe quoi, je veux dire littéralement n'importe quoi:

Musique de classe {

public $ length;
public $ envoyé;

public $ artiste;
public $ album;
public $ releaseDate;

function getAlbumCoverFile () {
retournez 'Images / Couvertures /'. $ this-> artiste. '/'. $ cet-> album. '.png';
}
}

Et la classe de musique comme celle ci-dessus fonctionnera parfaitement. On peut facilement le tester avec un test très similaire à File.
function testItCanGetTheProgressOfAMusicStreamAsAPercent () {
$ musique = nouvelle musique ();
$ musique-> longueur = 200;
$ musique-> envoyé = 100;

$ progress = nouveau progrès ($ musique);

$ this-> assertEquals (50, $ progress-> getAsPercent ());
}

Donc, fondamentalement, tout contenu mesurable peut être utilisé avec la classe Progress. Peut-être devrions-nous le mettre dans le code en changeant également le nom de la variable:

Progression de la classe {

private $ mesurableContent;

function __construct ($ mesurableContent) {
$ this-> mesurableContent = $ mesurableContent;
}

function getAsPercent () {
return $ this-> mesurableContent-> envoyé * 100 / $ this-> mesurableContent-> length;
}

}

Lorsque nous avons spécifié File comme typehint, nous étions optimistes quant à ce que notre classe peut gérer. C'était explicite et si autre chose arrivait, une grosse erreur nous le dirait.

Uune classe qui remplace une méthode d'une classe de base de telle sorte que le contrat de classe de base ne soit pas honoré par la classe dérivée. 

Nous ne voulons pas finir par essayer d'appeler des méthodes ou d'accéder à des champs sur des objets qui ne sont pas conformes à notre contrat. Lorsque nous avions une indication de type, le contrat était spécifié par lui. Les champs et méthodes de la classe File. Maintenant que nous n'avons rien, nous pouvons envoyer n'importe quoi, même une chaîne et cela entraînerait une mauvaise erreur.

Alors que le résultat final est le même dans les deux cas, ce qui signifie que le code se casse, le premier a produit un beau message. Ceci, cependant, est très sombre. 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 la classe Progress, la lire et la comprendre. Le contrat, dans ce cas, lorsque vous ne spécifiez pas explicitement le typehint, est definié par le comportement de Progress. Il s'agit d'un contrat implicite connu de Progress uniquement. Dans notre exemple, c'est defiterminé en accédant aux deux champs, envoyé et longueur, dans la méthode getAsPercent(). Dans la vraie vie, le contrat implicite peut être très complexe et difficile à découvrir en cherchant simplement quelques secondes en classe.

Cette solution n'est recommandée que si aucun des autres conseils ci-dessous ne peut être facilement implémenté ou s'ils infligeraient des changements architecturaux sérieux qui ne justifient pas l'effort.

Ercole Palmeri

Bulletin d'innovation
Ne manquez pas les nouvelles les plus importantes sur l'innovation. Inscrivez-vous pour les recevoir par email.

Articles récents

Les éditeurs et OpenAI signent des accords pour réguler les flux d'informations traitées par l'intelligence artificielle

Lundi dernier, le Financial Times a annoncé un accord avec OpenAI. FT autorise son journalisme de classe mondiale…

30 avril 2024

Paiements en ligne : voici comment les services de streaming vous font payer pour toujours

Des millions de personnes paient pour des services de streaming en payant des frais d’abonnement mensuels. Il est communément admis que vous…

29 avril 2024

Veeam offre la prise en charge la plus complète contre les ransomwares, de la protection à la réponse et à la récupération.

Coveware by Veeam continuera à fournir des services de réponse aux incidents de cyberextorsion. Coveware offrira des capacités d’investigation et de remédiation…

23 avril 2024

Révolution verte et numérique : comment la maintenance prédictive transforme l'industrie pétrolière et gazière

La maintenance prédictive révolutionne le secteur pétrolier et gazier, avec une approche innovante et proactive de la gestion des installations.…

22 avril 2024