finish first part of software architecture

This commit is contained in:
Yorick Barbanneau 2024-04-14 23:58:09 +02:00
parent 5c5aba4871
commit eda9fc8abd

View file

@ -1,57 +1,241 @@
---
title: "PdP: Architecture loficielle"
title: "PdP: Architecture logicielle"
date: 2024-03-14
tags: ["besoins", "UML", "developpement logiciel"]
categories: ["Projet de programmation", "Cours"]
---
## Introduction
une architecture logicielle est une structure composite basée sur une
décomposition de composants distincts et une ensemble de dépendance et/ou
d'interaction entre ces composants.
La finalité est de répondre un ensemble de besoins logiciels et d'offrir un
ensemble de services.
> Engineering is all about breaking down big problems into smaller ones
> and putting the solutions for those problems back together
Boswell and Fouchet, 2011
Il est donc question ici de maitriser la taille et la complexité pour assurer le
**passage à l'échelle** : décomposer pour maîtriser et faire évoluer. En effet
les architectures logicielles doivent aussi être modifiables, extensible et
évolutives. Les composants et leurs dépendances doivent **permetttre et
faciliter le changement**.
## Dépendances et composants
Un composant, aussi appelé aussi *bloc de construction* est tout élément
syntaxique qui participe à la structure de son architecture : les intructions,
les expressions, fonctions, classes, modules, paquetages, etc.
Un composant \\(C_1\\) est dépendant de \\(C_2\\) si \\(C_1\\) utilise \\(C_2\\)
dans sa définition ou implémentation.
![Représentation des dépendences](./imgs/dependence_c1_c2.svg)
Voici comment représenter les dépendances entre deux classes, notez le sens de
la flèche, elle pointe vers \\(C_2\\) : *"dépends de".
Nous avons trois grande famille de dépendances:
* Les **utilisations simples** d'éléments de composants. Par exemple une
fonction \\(f_1\\) appelle une autre fonction \\(f_2\\);
* Les **associations** entre composants par imbrications. Par exemple une
classe \\(C_1\\) inclut un attribut de la classe \\(C_2\\);
* Les **inclusions** ou **héritages** entre composants par réutilisation. Par
exemple un sources C \\(s_1\\) inclut via `#include` un autre fichier
\\(s_2\\).
### Taille -- flux et débit -- des dépendances
Les dépendances d'une architecture doivent être utilisable, utilisées et utiles.
Elles doivent transmettre que le stricte minimum d'informations et de qualité
suffisante (contrôle et maîtrise).
La **taille** d'une dépendance entre \\(c_{1}\\) et \\(c_{2}\\) reflète la
quantité d'information qu'elle peut faire transiter ce qui inclus:
* Le nombre d'éléments distincts transmis à \\(c_{1}\\) par \\(c_{2}\\);
* Le volume des éléments distincts transmis à \\(c_{1}\\) par \\(c_{2}\\);
* La complexité des éléments distincts transmis à \\(c_{1}\\) par \\(c_{2}\\).
Cette mesure n'as pas vocation être précise.
Dasn une architecture logicielle, il faut favoriser la définition d'une
dependance **avec une taille minimale**
### Diffusion des changements
Une architecture où les changements dasn les composants sont diffusés aux
autres composants n'est qu'une *architecture factice*.
Nous devons donc introduire la notion de **force d'une dépendance** entre deux
composants \\(c_{1}\\) et \\(c_{2}\\):
* Elle est **forte** si une modification dans \\(c_{2}\\) entraine une
modification dans \\(c_{1}\\).
* Elle est faible dans le cas contraire.
La diffusion des changement va toujours dans le *sens inverse* des dépendances.
Dans une architecture logicielle, il faut limiter les dépendances forte pour les
changements les plus probables dans cette architecture. On est obligé de se
concentrer sur les changements les plus probables (on ne pourra considérer tous
les changements). Il faut identifier ces changement possibles par le contexte,
les besoins non fonctionnels, l'expérience, etc.
Plusieurs techniques d'implémentation sont disponible pour appliquer le principe
de diffusion minimisée des changements comme:
* Les mécanismes de cloisonnement des langages (expression du privée / public);
* En appliquant le principe de *taille minimisé*;
* En utilisant les moyens d'expression d'abstration des langages comme *les
interfaces*.
### Dépendances et utilisation des interfaces
spécification d'un composant -> description abstraite et rigoureuse de ses
qualités, de sa manière de se comporter, des services qu'il est censé rendre.
Un composant qui satisfait une spécification S est dit réaliser S
La spécification d'un composant représente la description abstraite et rigoureuse de
ses qualités, de sa manière de se comporter, des services qu'il est censé
rendre. Un composant qui satisfait une spécification \\(S\\) est dit *réaliser*
\\(S\\)
Utilisation de l'interface au lieu d'une classe
```c++
class C2 {
IC1 obj;
// ...
// This is a simple class
}
interface IC1 {
// Interface - kind of specification
}
class C1 implement IC1 {
// ...
// then iplementation.
}
```
![Introduction d'une interface](./imgs/interface.svg)
Ainsi `C2` ne dépends pas de `C1` mais de l'interface `IC1`. En plus de décrire
abstraitement les composants à implémenter, elles servent d'intermédiaire dans
les dépendances.
Les interfaces contrôlent une part des dépendances fortes, on y spécifie les
éléments les moins susceptibles de changer. Les interfaces ont deux rôles
important dans une architecture logicielle:
* Décrire de manière abstraite les composants à implémenter;
* Servir d'intermédiaire dans les dépendances et ainsi contrôler la force ou la
faiblesse des dépendances.
#### Principe d'inversion des dépendances.
**Une dependance implicite** est une dépendance qui ne se voit pas directement
dans le source d'un programme pas ses appels et ses annotations. Elle est
toujours forte par rapport au changement qui la détermine.
> Dans une architecture logicielle, il est préférable de faire dépendre les
> ocmposants d'interfaces et non les composants implémentés.
> composants d'interfaces et non les composants implémentés.
Principe d'inversion des dépendances.
si nous reprenons le schéma précédent, alors \\(IC_{1}\\) a été écrite par
rapport à l'usage de \\(C_{2}\\) par \\(C_{1}\\), nous pouvons alors en déduire
que \\(C_{2}\\) dépends de la paire \\((C_{1},IC_{1})\\). C'est notre
**inversion de dépendance**
Une dependance implicite -> qui ne se voit pas directement dans le source d;un
programme pas ses appels et ses annotations. Elle est toujours forte par rapport
au chamgement qui la détermine.
### Notion étendue des dépendances
Nous avons vu des *dépendances explicites* qui se matérialise par les appels de
méthodes, les classes etc. Mais il existe aussi les **dépendances implicite**:
c'est une dépendance qui ne se voit pas directement dans le code source.
Elles peuvent prendre des formes très différentes des dépendances explicite par
* la duplication de code
* les similarité de services
* <cf slide>
* La duplication de code;
* les similarité de services offerts par des composants distincts;
* les similarité de structure entre composants distincts.
Les dépendances implicites sont toujours des dépendances fortes car se sont les
changements qui les définissent. Il est donc préférable de minimiser leur
apparition. (on minimise alors la duplication de code et les similarité par
des techniques de factorisation de code)
LEs archilogicielles sont généralement des structures hiérarchisées.
## Granularité de structure, cohésion et couplage
niveau de granularité défini par
Les architecture logicielles sont généralement des structures hiérarchisées de
composants, c'est une conséquence naturelle du *décomposer pour maîtriser*.
Cette hiérarchisation est généralement simplifiée car elle peut être constituée
essentiellement de niveaux homogène de structure de composants.
Gq et G2 deux niveau de granilarité alirs couplage : ensemble des dépendances
entre les composants de G2
Son niveau de granularité est déterminé par des composants de même espèce
capable d'inclure des composants du niveau inférieur (s'il y en a). Les niveaux
habituels de granularité offerts par les langages de programmation sont:
* Les valeurs, données, instructions;
* Les variables, attributs, fonctions (ou méthodes);
* Les modules, classes, objets,
* Les paquetages, clusters, namespace
* Les super-paquetages;
* Les couches.
Pour des raison de simplification et d'intelligibilité, les dépendances peuvent
se restreindre à ne s'énoncer qu'au sein de chaque niveau de granularité : un
composant \\(C1\\) est explicitement dépendant de \\(C2\\) si \\(C1\\) utilise
\\(C2\\) dans sa définition ou son implémentation et si tous les deux sont au
même niveau de granularité. Les dépendances sont donc à considérer entre
fonctions, entre classes, entre paquetages, etc.
### Graphes de dépendances
Les dépendances dans une granularité fixée induisent des **graphes** dont les
sommets sont d'une seule espèce de composants et les arcs des dépendances
explicites.
Dans la mesure du possible, il faut favoriser la construction de niveau de
granularité homogènes donnant lieu à des graphe de dépendances distincts. C'est
un principe de simplification et d'intelligibilité des architectures logicielles
mais qui n;est pas applicable tout le temps (et c'est parfois souhaitable). Les
langages permettent souvent des moyens de déroger à ce principe (*nested
classes* en Java, lambda expression ...).
### Suites de granularités, consécutivité
Les granularités peuvent donc former des suites constitués des éléments que nous
avons évoqué plus haut (données, variables, fonctions etc.). Deux **granularité
consécutives** sont deux granularité qui se suivent dans cette liste, elles
engendre des **graphes consécutifs de dépendances**.
Il faut veiller à ce que chaque graphes de consécutivité soit plus simple que le
précédent, ils dessinent alors **une pyramide**. Chaque niveaux est une
opportunité de simplifier, synthétiser la description d'une architecture. Bien
entendu il existe des contre-exemples, comme **les classes qui ne contiennent
qu'une seule méthode**.
Il serait naturel de regrouper un ensemble de méthodes très dépendantes en elles
dans une seule classe, ou encore des classes très dépendances dans un
paquetage. C'est le principe **de simplification min-max**: il faut en sorte
que les composants du niveau de granularité \\(G2\\) englobent le **maximum** de
dépendances de \\(G1\\) tel qu'il reste un **minimum** de dépendances en
composants de \\(G2\\).
### Couplage et cohésion
Le couplage représente l'ensemble des dépendances entre composants de \\(G2\\),
la cohésion celles au sein de chacun des composants de \\(G2\\) entre les
éléments de \\(G1\\).
Si \\(G2\\) est un ensemble de classes et \\(G1\\) un ensemble d'attributs et de
méthodes de ces classes alors:
* le couplage correspond à l'ensemble des dépendances entre classe de \\(G2\\);
* la cohésion définie pour chaque classe de \\(G2\\) correspond à l'ensemble
des dépendances entre ses attributs et méthodes.
Pour reprendre notre **principe min-max**: dans une architecture logicielle, pour
toute paire de niveau de granularité, il faut privilégier à la fois **la
cohésion la plus forte** et **le couplage le plus faible**