--- title: "PdP: principes SOLID" date: 2024-03-28 tags: ["besoins", "UML", "developpement logiciel"] categories: ["Projet de programmation", "Cours"] --- C'est un ensemble de principe souvent associés à la programmation objet. Ils s'appliquent cependant à toute forme d'architecture. Il sont au nombre de 5 : * principe d'inversion des dépendances que nousa vons déjà vu précédement; * principe de responsablité unique; * principe de ségrégation d'interfaces; * principe ouvert/fermé * principe de substitution (de *Liskov*) ## Inversion des dépendances Utiliser des interfaces comme intermédiaire afin d'inverser les dépendances. LEs interfaces utilisées comme intermédiaires permettent de faire écran (masquer l'implementation). Elles permettent aussi de proteger les composants de la diffusion. Dans sa **version stricte**, les interfaces ne portent que ce qui est nécessaire, mais ça peut être lourd (multiplication des interfaces). Il est donc plus courant (et désirable) de généraliser les interfaces. ## Responsablité Il est question de décomposer pour maîtriser, mais aussi pour limiter les responsabilité des composants. La **responsabilité d'un composant** est un rôle, un objectif, un comportement, un ensemble de services qu'il fournit et dont il est le garant. Il faut alors favoriser la contruction de composants avec un nombre réduit de responsabilité. Dans sa version radicalisé (Martin, 2003), il est même question de favoriser les composant avec **une seule responsabilité**. Mais attention, il ne faut pas appliquer ce principe jusqu'à emietter complètement une architecture (code *ravioli*). Une responsabilité ne se résume pas à une seule fonction (ou une seule action) mais plutôt à un ensemble cohérent de fonctions / actions. Voici les critères permettant de la caractériser une responsabilité unique: * être décrite simplement en lien avec le domaine du programme (et en langage naturel); * être associé à un haut niveau de cohésion dans le composant (ce qui signifie plus de dépendance entre ses éléments); * être identifiée par rapport à la manière récurrente dont le composant est utilisé par les autres composants (féquement utilisé pour **tout** ce qu'il propose); * être associé à sa raison principale la plus probable d'être modifiée (une seule raison principale d'évoluer). ### Son anti-pattern juré: le **blob** C'est l'inverse de la responsabilité unique : un composant fourre-tout qui centralise beaucoup de responsabilité différentes et entourré de composants qui ne contiennent que des données ou des processus simples. ## Ségrégation d'interfaces Nous avons vu que la taille d'une dépendance entre deux composants est caractérisée par la quantité d'information qui transite entre eux. nous avonx vu que nous devons avant tout les minimiser. Le principe de ségrégation des interfaces est le principe selon lequel il faut favoriser la construction de composants dont les dépendances correspondent à ce qu'ils utilisent réellement (ni plus, ni mois). Nous parlons ici d'interfaces au sens large, mais au sens d'interfaces de classes. Il est donc parfois nécessaire de décomposer pour l'appliquer, quitte à recomposer ensuite. ## Ouvert / Fermer Pour tout logiciel, des transformations et des évolutions seront nécessaire et appliquées. Il sont dit *ouverts* Il est aussi préférable de ne pas toucher à ce qui fonctionne bien et est utilisé. Ceci pourrait pertuber un écosystème fragile. C'est la partie *fermée*. Nous préservons ainsi son fonctionnement, les liens entre les composants, les interfaces, les tests etc. Pour faire évoluer notre logiciel snas pour autant toucher aux composants fonctionnels, nous avons alors plusieurs pistes: * utiliser l'héritage et ainsi ne pas toucher au composant de base fonctionnel; * utiliser les interfaces pour, par exemple, de nouvelles implémentations (réalisations multiples) * utiliser la généricité, l'usage de composants paramétrés permettant de les adaptés selon l'instanciation de ces partamètres tout en les préservant; * utiliser les plugins. Il faut donc favoriser la construction de composants ouverts-fermés par rapport aux changements les plus probables, ouverts à l'extension et fermé à la modification. Toute la difficulté ici est d'obtenir des composants ouverts-fermés facile à faire évoluer. ## Substitution de Liskov Dans une architecture logicielle, il doit être possible d'effectuer des remplacement localiser de composants de manière à préserver son fonctionnement. La **substitution** est le remplacement d'un élément par un autre similaire indépendement du contexte (*context-free*) et sans induire d'autres changements. Il faut alors passer par une spécfication, ainsi la variation se fait sur la manière de l'implémenter. Il faut alors favoriser la **construction de composants spécifiés** de manière à pouvoir les substituer. Il faut alors faireparticulièrement attention à la **précision de la spécification** en fonction du contexte. ### substitution par sous-typage Un composant substituant sous une spécification \\(S\\) peuvent implémenter plus de propriété que requis par \\(S\\) sans pour autant metter à mal les propriétés de substitution. En clair *Qui peut le plus peut le moins*. Une spécification \\(S'\\) est compatible avec une autre \\(S\\) (noté \\(S' <: S\\)) si on peut déduire toutes les propriété de \\(S\\) à partir de \\(S'\\) et donc si \\(S' \implies S\\). De manière générale, lorsque \\(S' <: S\\) tout composants qui implémente \\(S'\\) implémente aussi \\(S\\). Ainsi si on considère les spécifications comme des types, la compatibilité définis ce qu'est le **sous-typage**. L'application de la substitution requiert une bonne connaissance de ce qui engendre des compatibilités. ### compatibilité La compatibilité entre **spécifications de fonctions** dépend en particulier de leurs domaines d'entrée et de sorties. La compatibilité entre spécfications de fonctions est donc : \\[ (In' \rightarrow Out') <: (In \rightarrow Out')\\\ \text{si } In <: In' \\\ \text{et } Out' <: Out \\] Donc: * \\(In'\\) a un ensemble d'instances **égal ou plus grand** que \\(In\\); * \\(Out'\\) a un ensemble d'instance **égal ou plus petit** que \\(Out\\); Cette règles relatives aux fonctions peuvent se généraliser **aux conditions**: * les **préconditions** \\(Pre\\) d'une spécification \\(S\\) sont les propriétés de notre spécification qui doivent être implémentées par un composant pour qu'ils soit exécuté, appliqué, activé. * les **postconditions** \\(Post\\) d'une spécification \\(S\\) sont les propriétés de notre spécification qui doivent être implémentées par les résultats, les services rendus, les effets obtenus d'un composant. \\[ (Pre', Post') <: (Pre, Post')\\\ \text{si } Pre <: Pre' \\\ \text{et } Post' <: Post \\] autrement dit: * les préconditions sont plus faible ou égale: **contravariance**; * les postconditions sont plus forte ou égale: **covariance** ### Vérification des substitutions Liskov introduit des bugs s'il n'est pas bien appliqué. La compréhension est donc essentielle sinon des bugs liés aux intentions feront leur apparition, et ils sont souvent difficile à trouver. ### Principe de substitution de Liskov Sans une architecture logicielle, il faut favoriser la construction de composants spécifiés au niveau adéquat de précision de manière à ce qu'ils soient substituables sous leur spécification de manière satisfaisante dans leur contexte de programmation. Il faudra considérer alors le bon choix de précision des spécifications et la vérification des spécifications, des compatibilités ainsi que des aproximations qui y sont associés.