diff --git a/source/part-1-workspace/maintainable-applications/solid.adoc b/source/part-1-workspace/maintainable-applications/solid.adoc index 8015520..c6abf40 100644 --- a/source/part-1-workspace/maintainable-applications/solid.adoc +++ b/source/part-1-workspace/maintainable-applications/solid.adoc @@ -1,21 +1,63 @@ -=== SOLID +. SRP - Single responsibility principle +. OCP - Open-closed principle +. LSP - Liskov Substitution +. ISP - Interface ségrégation principle +. DIP - Dependency Inversion Principle -. S : SRP (Single Responsibility -. O : Open closed -. L : LSP (Liskov Substitution) -. I : Interface Segregation -. D : Dependency Inversion +En plus de ces principes de développement, il faut ajouter des principes architecturaux au niveau des composants: + +. Reuse/release équivalence principle, +. Common Closure Principle, +. Common Reuse Principle. + +La même réflexion sera appliquée au niveau architectural, et se basera sur ces mêmes primitives. ==== Single Responsibility Principle Le principe de responsabilité unique définit que chaque concept ou domaine d'activité ne s'occupe que d'une et d'une seule chose. -En prenant l'exemple d'une méthode qui communique avec une base de données, ce ne sera pas à cette méthode à gérer l'inscription d'une exception à un emplacement quelconque. -Cette action doit être prise en compte par une autre classe (ou un autre concept), qui s'occupera elle de définir l'emplacement où l'évènement sera enregistré (dans une base de données, une instance Graylog, un fichier, ...). +Traduit autrement, SRP nous dit qu’une classe ne devrait pas contenir plus d’une raison de changer. +Traduit encore autrement, «  for software systems to be easy to change, they must be designed to allow the behavior to change +by adding new code instead of changing existing code. -Cette manière d'organiser le code ajoute une couche de flemmardise (ie. Une fonction ou une méthode doit pouvoir se dire "_I don't care_" et s'occuper uniquement de ses propres oignons) sur certains concepts. Ceci permet de centraliser la configuration d'un type d'évènement à un seul endroit, ce augmente la testabilité du code. +Aussi : A module should be responsible to one and only one actor + +Le principe de responsabilité unique diffère d’une vue logique qui tendrait intuitivement à centraliser le maximum de code. +A l’inverse, il faut plutôt penser à différencier les acteurs. +Un exemple donné consiste à identifier le CFO, CTO et COO qui ont tous besoin de données et informations relatives à une même +base de données des membres du personnel, mais ils ont chacun besoin de données différents +(ou en tout cas, d’une représentation différente de ces données). +Nous sommes d’accord qu’il s’agit à chaque fois de données liées aux employés, mais elles vont un cran plus loin et +pourraient nécessiter des ajustements spécifiques en fonction de l’acteur concerné. + +L’idée sous-jacente est simplement d’identifier dès maintenant les différents acteurs, en vue de +prévoir une modification qui pourrait être demandée par l’un d’entre eux et qui pourrait avoir un +impact sur les données utilisées par les autres. + +En prenant l'exemple d'une méthode qui communique avec une base de données, +ce ne sera pas à cette méthode à gérer l'inscription d'une exception à un emplacement quelconque. +Cette action doit être prise en compte par une autre classe (ou un autre concept), qui s'occupera elle de définir +l'emplacement où l'évènement sera enregistré (dans une base de données, une instance Graylog, un fichier, ...). + +Cette manière d'organiser le code ajoute une couche de flemmardise +(ie. Une fonction ou une méthode doit pouvoir se dire "_I don't care_" et s'occuper uniquement de ses propres oignons) +sur certains concepts. +Ceci permet de centraliser la configuration d'un type d'évènement à un seul endroit, ce qui augmente la testabilité du code. + +Au niveau des composants, le SRP deviendra le CCP. +Au niveau architectural, ce sera la définition des frontières (boundaries). ==== Open Closed +L’objectif est de rendre le système facile à étendre, en évitant que l’impact d’une modification ne soit trop grand. + +Les exemples parlent d’eux-mêmes: des données doivent être présentées dans une page web. +Et demain, ce seras dans un document PDF. +Et après demain, ce sera dans un tableur Excel. +La source de ces données restent la même (au travers d’une couche de présentation), mais la mise en forme diffère à chaque fois. + +L’application n’a pas à connaître les détails d’implémentation: elle doit juste permettre une forme d’extension, +sans avoir à appliquer une modification (ou une grosse modification) sur son cœur. + Un des principes essentiels en programmation orientée objets concerne l'héritage de classes et la surcharge de méthodes: plutôt que de partir sur une série de comparaisons pour définir le comportement d'une instance, il est parfois préférable de définir une nouvelle sous-classe, qui surcharge une méthode bien précise. Pour l'exemple, on pourrait ainsi définir trois classes: @@ -72,14 +114,24 @@ class GoldCustomer(Customer): 10 ---- -En anglais, dans le texte : "Putting in simple words, the “Customer” class is now closed for any new modification but it’s open for extensions when new customer types are added to the project.". En résumé: nous fermons la classe `Customer` à toute modification, mais nous ouvrons la possibilité de créer de nouvelles extensions en ajoutant de nouveaux types [héritant de `Customer`]. +En anglais, dans le texte : "Putting in simple words, the “Customer” class is now closed for any new modification +but it’s open for extensions when new customer types are added to the project.". +En résumé: nous fermons la classe `Customer` à toute modification, mais nous ouvrons la possibilité de +créer de nouvelles extensions en ajoutant de nouveaux types [héritant de `Customer`]. -De cette manière, nous simplifions également la maintenance de la méthode `get_discount`, dans la mesure où elle dépend directement du type dans lequel elle est implémentée. +De cette manière, nous simplifions également la maintenance de la méthode `get_discount`, +dans la mesure où elle dépend directement du type dans lequel elle est implémentée. Ce point sera très utile lorsque nous aborderons les https://docs.djangoproject.com/en/3.1/topics/db/models/#proxy-models[modèles proxy]. ==== Liskov Substitution +NOTE: Dans Clean Architecture, ce chapitre ci (le 9) est sans doute celui qui est le moins complet. +Je suis d’accord avec les exemples donnés, dans la mesure où la définition concrète d’une classe doit dépendre +d’une interface correctement définie (et que donc, faire hériter un carré d’un rectangle, n’est pas adéquat +dans le mesure où cela induit l’utilisateur en erreur), mais il y est aussi question de la définition d'un +style architectural pour une interface REST, mais sans donner de solution… + Le principe de substitution fait qu'une classe héritant d'une autre classe doit se comporter de la même manière que cette dernière. Il n'est pas question que la sous-classe n'implémente pas certaines méthodes, alors que celles-ci sont disponibles sa classe parente. @@ -90,7 +142,9 @@ Il n'est pas question que la sous-classe n'implémente pas certaines méthodes, Ce n'est donc pas parce qu'une classe **a besoin d'une méthode définie dans une autre classe** qu'elle doit forcément en hériter. Cela bousillerait le principe de substitution, dans la mesure où une instance de cette classe pourra toujours être considérée comme étant du type de son parent. -Petit exemple pratique: si nous définissons une méthode `walk` et une méthode `eat` sur une classe `Duck`, et qu'une réflexion avancée (et sans doute un peu alcoolisée) nous dit que "Puisqu'un `Lion` marche aussi, faisons le hériter de notre classe `Canard`": +Petit exemple pratique: si nous définissons une méthode `walk` et une méthode `eat` sur une classe `Duck`, +et qu'une réflexion avancée (et sans doute un peu alcoolisée) nous dit que +"Puisqu'un `Lion` marche aussi, faisons le hériter de notre classe `Canard`": [source,python] ---- @@ -110,14 +164,25 @@ class Lion(Duck): ---- -UnNous vous laissons tester la structure ci-dessus en glissant une antilope dans la boite à goûter du lion, ce qui nous donnera quelques trucs bizarres (et un lion atteint de botulisme). +Nous vous laissons tester la structure ci-dessus en glissant une antilope dans la boite à goûter du lion, ce qui nous donnera quelques trucs bizarres (et un lion atteint de botulisme). ==== Interface Segregation +L’objectif est de limiter la nécessité de compilation en n’exposant que les opérations nécessaires +à l’exécution d’une classe, et pour éviter d’avoir à redéployer l’ensemble d’une application. +En Python, c’est inféré lors de l’exécution, et donc pas vraiment d’application pour notre contexte d'étude. +De manière générale, les langages dynamiques sont plus flexibles et moins couples que les langages statiquement +typés, pour lesquels il serait possible de mettre à jour une DLL ou un JAR sans que cela n’ait d’impact sur le +reste de l’application. + +> The lesson here is that depending on something that carries baggage that you don’t need can cause you troubles that you didn’t except. + Ce principe stipule qu'un client ne peut en aucun cas dépendre d'une méthode dont il n'a pas besoin. -Plus simplement, plutôt que de dépendre d'une seule et même (grosse) interface présentant un ensemble conséquent de méthodes, il est proposé d'exploser cette interface en plusieurs (plus petites) interfaces. -Ceci permet aux différents consommateurs de n'utiliser qu'un sous-ensemble précis d'interfaces, répondant chacune à un besoin précis. +Plus simplement, plutôt que de dépendre d'une seule et même (grosse) interface présentant un ensemble conséquent de méthodes, +il est proposé d'exploser cette interface en plusieurs (plus petites) interfaces. +Ceci permet aux différents consommateurs de n'utiliser qu'un sous-ensemble précis d'interfaces, +répondant chacune à un besoin précis. Un exemple est d'avoir une interface permettant d'accéder à des éléments. Modifier cette interface pour permettre l'écriture impliquerait que toutes les applications ayant déjà accès à la première, obtiendraient (par défaut) un accès en écriture, ce qui n'est pas souhaité/souhaitable. @@ -127,11 +192,29 @@ Pour contrer ceci, on aurait alors une première interface permettant la lecture * A : lecture * B (héritant de A) : lecture (par A) et écriture. -==== Dependency inversion +Mais ceci s'applique finalement à n'importe quel composant: votre système d'exploitation, les librairies et dépendances tierces, +les variables déclarées, ... + +==== Dependency inversion Principle Dans une architecture conventionnelle, les composants de haut-niveau dépendent directement des composants de bas-niveau. L'inversion de dépendances stipule que c'est le composant de haut-niveau qui possède la définition de l'interface dont il a besoin, et le composant de bas-niveau qui l'implémente. +> The dependency inversion principle tells us that the most flexible systems are those in which source code dependencies +refer only to abstractions, not to concretions. + +L’objectif est que les interfaces soient stables, et de réduire au maximum les modifications qui pourraient y être appliquées. +De la meme manière, il convient d’éviter de surcharger des fonctions ou des classes concrètes (= non abstraites). + +En termes architecturaux, ce principe définira les frontières dont il a déjà été question, +en demandant à ce qu’une délimitation ne se base que sur une dépendance qui soit … +cela permet notamment une séparation claire au niveau des frontières, en inversant le flux de dépendance et en faisant en sorte (par exemple) que les règles métiers n’aient aucune connaissance des interfaces graphiques qui les exploitent. Ces interfaces pouvant être desktop, web, … cela n’a pas vraiment d’importance. Cela autorise une for.e d’immunité entre les composants. + +Ne pas oublier qu’écrire du nouveau code est toujours plus facile que de modifier du code existant. + +> The database is really nothing more than a big bucket of bits where we store our data on a long term basis (the database is a detail, chap 30, page 281) +D’un point de vue architectural, nous ne devons pas nous soucier de la manière dont les données sont stockées, s’il s’agit d’un disque magnétique, de ram, … en fait, on ne devrait même pas savoir s’il y a un disque du tout. + Le composant de haut-niveau peut définir qu'il s'attend à avoir un `Publisher`, afin de publier du contenu vers un emplacement particulier. Plusieurs implémentation de cette interface peuvent alors être mise en place: @@ -148,3 +231,54 @@ L'injection de dépendances est un patron de programmation qui suit le principe * http://en.wikipedia.org/wiki/Software_craftsmanship[Software Craftmanship] * http://lostechies.com/derickbailey/2011/09/22/dependency-injection-is-not-the-same-as-the-dependency-inversion-principle/[Dependency Injection is NOT the same as dependency inversion] * http://en.wikipedia.org/wiki/Dependency_injection[Injection de dépendances] + + +=== au niveau des composants + +De la même manière que pour les principes définis ci-dessus, +Mais toujours en faisant attention qu’une fois que les frontières sont implémentés, elles sont coûteuses à maintenir. +Cependant, il ne s’agit pas une décision à réaliser une seule fois, puisque cela peut être réévalué. + +Et de la même manière que nous devons délayer au maximum les choix architecturaux et techniques, + +> but this is not a one time decision. You don’t simply decide at the start of a project which boundaries to implémentent and which to ignore. Rather, you watch. You pay attention as the system evolves. You note where boundaries may be required, and then carefully watch for the first inkling of friction because those boundaries don’t exist. +> at that point, you weight the costs of implementing those boundaries versus the cost of ignoring them and you review that decision frequently. Your goal is to implement the boundaries right at the inflection point where the cost of implementing becomes less than the cost of ignoring. + +En gros, il faut projeter sur la capacité à s’adapter en minimisant la maintenance. +Le problème est qu’elle ne permettait aucune adaptation, et qu’à la première demande, l’architecture se plante complètement sans aucune malléabilité. + + +==== Reuse/release equivalence principle + +[quote] +---- +Classes and modules that are grouped together into a component should be releasable together +-- (Chapitre 13, Component Cohesion, page 105) +---- + +==== CCP + +(= l’équivalent du SRP, mais pour les composants) + +> If two classes are so tightly bound, either physically or conceptually, that they always change together, then they belong in the same component + +Il y a peut-être aussi un lien à faire avec « Your code as a crime scene » 🤟 + + La définition exacte devient celle-ci: « gather together those things that change at the same times and for the same reasons. Separate those things that change at different times or for different reasons ». + + ==== CRP + + … que l’on résumera ainsi: « don’t depend on things you don’t need » 😘 +Au niveau des composants, au niveau architectural, mais également à d’autres niveaux. + +==== SDP + +(Stable dependency principle) qui définit une formule de stabilité pour les composants, en fonction de sa faculté à être modifié et des composants qui dépendent de lui: au plus un composant est nécessaire, au plus il sera stable (dans la mesure où il lui sera difficile de changer). En C++, cela correspond aux mots clés #include. +Pour faciliter cette stabilité, il convient de passer par des interfaces (donc, rarement modifiées, par définition). + +En Python, ce ratio pourrait être calculé au travers des import, via les AST. + +==== SAP + +(= Stable abstraction principle) pour la définition des politiques de haut niveau vs les composants plus concrets. +SAP est juste une modélisation du OCP pour les composants: nous plaçons ceux qui ne changent pas ou pratiquement pas le plus haut possible dans l’organigramme (ou le diagramme), et ceux qui changent souvent plus bas, dans le sens de stabilité du flux. Les composants les plus bas sont considérés comme volatiles