UP | HOME

Les architectures en couches - Méthodes de factorisation

Table of Contents

Une architecture en couches contient plusieurs couches, au moins deux, chaque couche utilisant la couche au-dessous, plus interne. Ce type d'architectures présente un caractère universel, puisqu'on peut considérer que toute classe possède une telle architecture. En effet, on a vu qu'il était possible de classer les méthodes d'une classe en deux couches :

Il existe de nombreux exemples d'architectures en couches. Un des plus connus concerne les protocoles de communication, par exemple ceux utilisés pour Internet. Le principe général est le suivant : une couche du protocole implémente une interface de communication en utilisant la couche inférieure du protocole (et possiblement un état interne). Ainsi l'utilisateur recourant pour communiquer à la couche la plus haute n'a pas à se préoccuper des détails des couches inférieures.

Data_Flow_of_the_Internet_Protocol_Suite.PNG

Figure 1: Communication dans les protocoles Internet (Wikipedia)

Il est possible de réaliser des factorisations dans une architecture en couches en développant de manière séparée les différentes couches puis en les composant. Imaginons \(h\) implémentations de la couche haute, \(H_1, \ldots, H_h\), et \(b\) implémentations de la couche basse, \(B_1, \ldots, B_b\). Comme chaque implémentation de la couche haute utilise la couche basse, pour obtenir une implémentation complète de la couche haute, on doit considérer un couple \((H_i, B_j)\).

Composition des implémentations
(cas de deux couches)

couches basses
couches hautes
...
\(H_i\) ...
... ... ... ...
\(B_j\) ... \(H_i\)
\(B_j\)
...
... ... ... ...

Examinons les efforts de codage pour réaliser les \(h.b\) combinaisons possibles, pour chaque méthode de factorisation. Précisément, pour mesurer le degré de factorisation, on cherche à évaluer la quantité de code totale à écrire, suivant les différentes méthodes. On met ainsi en évidence les répétitions, qui sont à éviter : en effet, toute évolution dans du code répété oblige à modifier toutes ses occurrences.

On note \(|X|\) la quantité de code pour réaliser l'implémentation \(X\).

Absence de factorisation

En l'absence de toute factorisation, pour chaque couple, on doit répéter le code des deux implémentations, ce qui donne au total la quantité suivante de code.

\[ \sum_{1 \leq i \leq h} \ \sum_{1 \leq j \leq b} (|H_i| + |B_j|) = b.\sum_{1 \leq i \leq h} |H_i| + h. \sum_{1 \leq j \leq b} |B_j| \]

Héritage - Approche ascendante - Factorisation de la couche basse

Avec cette méthode, on implémente d'abord la couche basse. Pour produire une implémentation complète, on hérite d'une implémentation de la couche basse en l'étendant par une implémentation de la couche haute. On répète donc les implémentations de la couche haute, ce qui donne au total la quantité suivante de code.

\[ \sum_{1 \leq j \leq b} (|B_j| + \sum_{1 \leq i \leq h} |H_i|) = b.\sum_{1 \leq i \leq h} |H_i| + \sum_{1 \leq j \leq b} |B_j| \]

Héritage - Approche descendante - Factorisation de la couche haute

Avec cette méthode, on implémente d'abord la couche haute, en utilisant la couche basse qui n'est pas encore implémentée. Ainsi, toute implémentation est partielle : on dit qu'elle est abstraite. Pour produire une implémentation complète, on hérite d'une implémentation de la couche haute, abstraite, en l'étendant par une implémentation de la couche basse pour ainsi la rendre concrète. On répète donc les implémentations de la couche basse, ce qui donne au total la quantité suivante de code.

\[ \sum_{1 \leq i \leq h} (|H_i| + \sum_{1 \leq j \leq b} |B_j|) = \sum_{1 \leq i \leq h} |H_i| + h.\sum_{1 \leq j \leq b} |B_j| \]

Héritage multiple

Avec cette méthode, on implémente séparément la couche basse et celle haute, puis on compose les implémentations par un héritage double. En Java, l'héritage multiple est impossible pour les classes : ainsi, on ne peut cumuler les avantages des deux méthodes précédentes, en composant une classe implémentant la couche basse et une classe abstraite implémentant la couche haute. En revanche, depuis la version 8 de Java, il est possible dans une interface de fournir pour chaque méthode une implémentation dite par défaut. On peut ainsi réaliser un héritage multiple entre

  • d'une part, une classe implémentant la couche basse,
  • d'autre part, une interface implémentant la couche haute en utilisant la couche basse, non implémentée.

Cette forme simplifiée d'héritage multiple est parfaitement adaptée aux architectures en couches et permet d'éviter les complications de l'héritage multiple sous sa forme la plus générale 1.

Avec l'héritage multiple, aucune répétition ne se produit, ce qui donne au total la quantité suivante de code, qui est minimale.

\[ \sum_{1 \leq i \leq h} |H_i| + \sum_{1 \leq j \leq h} |B_j| \]

Agrégation avec délégation

Avec cette méthode, on implémente séparément la couche basse et celle haute. Toute implémentation de la couche haute intègre un mécanisme de composition : elle agrège (c'est-à-dire possède comme attribut) une implémentation de la couche basse à laquelle elle délègue l'implémentation de la couche basse. Ainsi, l'implémentation devient complète, de la couche haute à la couche basse. Lorsqu'on crée un objet de l'implémentation de la couche haute, on passe au constructeur un objet d'une implémentation de la couche basse : c'est l'injection de dépendance. Avec l'agrégation avec délégation, aucune répétition ne se produit, ce qui donne au total la quantité suivante de code, qui est minimale.

\[ \sum_{1 \leq i \leq h} |H_i| + \sum_{1 \leq j \leq h} |B_j| \]

Comparaison des méthodes

L'approche ascendante pour l'héritage est couramment utilisée lorsqu'on étend une interface en rajoutant une couche haute : c'est une méthode simple pour définir une ou plusieurs implémentations de l'interface étendue à partir d'une implémentation de la couche basse.

L'approche descendante pour l'héritage est couramment utilisée lorsqu'on souhaite fournir une implémentation de la couche haute. Ensuite chaque utilisateur peut hériter de cette implémentation en ajoutant une implémentation de la couche basse. Il s'agit d'un patron de conception ("design pattern") bien connu, le patron de méthode ("method template", soit plus littéralement le guide de méthode).

Les deux autres méthodes de factorisation,

  • l'héritage multiple et
  • l'agrégation avec délégation,

se distinguent parce qu'elles sont optimales. Ce sont elles qui doivent être préférées dès lors qu'une factorisation est souhaitée pour les deux couches. Elles diffèrent par la manière dont se réalise l'injection de dépendance. Avec l'héritage multiple, l'injection est réalisée statiquement, lors de la compilation : on fait dépendre une implémentation de la couche haute d'une implémentation de la couche basse, de manière permanente, par l'héritage. Avec l'agrégation avec délégation, l'injection est réalisée dynamiquement, pendant l'exécution : lors de la construction d'un objet d'une implémentation de la couche haute, on fournit un objet d'une implémentation de la couche basse. Cette dépendance peut d'ailleurs évoluer lors de l'exécution, s'il est possible de modifier l'attribut pointant vers l'objet agrégé donnant l'implémentation de la couche basse.

Exemple des entiers naturels

Voir les variations du TD1.

Footnotes:

1

Les complications viennent principalement de l'héritage des attributs formant l'état, comme expliqué dans cette note du tutoriel officiel dédié à Java.

Version history: v1: 2016-10-10.
Comments or questions: Send a mail.
The webpage content is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.