cours/content/projet_programmation/9_principes_SOLID/index.md

185 lines
7.7 KiB
Markdown

---
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.