Académique Documents
Professionnel Documents
Culture Documents
Master SIW 2
1
Chapitre 1 : Les architectures des systèmes
I. Systèmes centralisés :
Un système centralisé c'est un système où tout le monde dépend d'une même autorité, un
serveur a priori dans le cas informatique. En informatique, un serveur central centralise un
service. Les serveurs centraux sont utilisés généralement dans une architecture centralisée a
contrario d'une architecture décentralisée (dite distribuée). On parle de serveur central dans un
réseau d'utilisateurs ou de machines. Il existe différents types de centralisation :
la centralisation de données - qui stocke l'ensemble des données sur un serveur concernant
l'ensemble des éléments du réseau informatique ;
la centralisation de direction - où un seul serveur décide de quel machine ou quel
utilisateur fait quoi et quand ;
la centralisation de communication - toutes les communications passent par le serveur
central.
III.1 La performance :
a) Répartition des données
Un système devient plus performant lorsqu’on distribue les données sur plusieurs serveurs
(Fig1). On va distribuer les données sur plusieurs serveurs au lieu d’un seul serveur qui
héberge la base de données. On aura plusieurs machines et chacun héberge une partie de la
base de données. On monte en capacité de stockage. On a besoin de machines serveurs avec
des processeurs performants (5 Ghz). Si on atteint la limite du processeur il faut utiliser des
applications multithreadées c’est-à-dire plusieurs processeurs qui font la tâche donc on a une
seule machine avec plusieurs processeurs. On aura un système distribué au niveau processeur.
D’autre part on va avoir un nombre élevé de tâches donc on va opter à les répartir.
Une condition doit être satisfaite est que les données soient en bloc indépendants. Si on
exécute une requête sur plusieurs machines on doit par la suite récupérer le résultat final.
Exemple : si on cherche les dix plus gros utilisateurs (on fait un filtrage). On cherche les dix
sur chaque machine mais il y’a un qui n’apparait pas donc on cherche les cents utilisateurs sur
chaque serveur puis on trouve les dix premiers.
La mise à jour se fait en parallèle sur tous les nœuds donc un utilisateur
(administrateur) fait une demande d’écriture. On va arrêter les requêtes puis exécuter la
demande. On va répliquer l’information sur toutes les bases. On acquitte l’écriture à
l’utilisateur et on permet l’accès à cette donnée entre temps il faut arrêter toutes les requêtes.
Pour assurer la cohérence on procède à mettre à jour nœud par nœud sans bloquer tout le
système. On est proche du temps réel. On veut garantir que les résultats représentent l’état du
système à un instant t-Δt avec Δt 0. Par conséquent on aura un fonctionnement presque
asynchrone . Il faut noter que plus Δt autorisé est grand plus on améliore les performances.
Le processeur ou CPU (Central Processing Unit) est le qui exécute les instructions qui lui
sont données par votre système d’exploitation (Windows). Quand vous lancez un logiciel,
regardez une vidéo Haute Définition ou lorsque vous jouez à un jeu vidéo, vous faites
travailler en priorité le processeur ! Pour répondre à vos demandes les plus exigeantes,
le processeur peut être doté des plusieurs cœurs.
Plusieurs instructions peuvent être traitées par le cœur d’un processeur mais ce sera
toujours une par une, en série. Avant l’apparition des processeurs multi-cœurs, on avait
l’impression que les processeurs simple cœur étaient multi-tâches tellement ils passaient
d’une instruction à une autre rapidement mais il n’en était rien.
On parle alors de processeur SISD, signifiant single instruction single data stream. La
plupart des ordinateurs monoprocesseurs conventionnels (par exemple IBM 370, DEC VAX,
SUN) fonctionnent, plus ou moins, sur ce principe. À chaque cycle d'horloge, l'unité logique et
arithmétique (ULA) du processeur exécute une opération élémentaire indiquée par le
programme-machine. Si par exemple on veut ajouter deux nombres, d'abord l'adresse
mémoire du premier est mise dans un registre lu par le processeur pour aller chercher à la
bonne place de la mémoire RAM ; ensuite le nombre est rappelé de la mémoire dans le registre
d'addition. La même opération est recommencée avec l'autre opérande. Ensuite, l'opération
d'addition est exécutée, le résultat remplace le contenu du registre correspondant et finalement
est sauvé dans la mémoire RAM. Évidemment, les registres, qui sont des mémoires très
rapides, ne peuvent contenir qu'une très petite quantité de données et rappeler chaque fois que
le besoin se présente la donnée nécessaire. Comme les accès à la mémoire RAM sont plus lents
que les accès des registres, plusieurs cycles d'horloge s'écoulent pour le rappel des données
pendant lesquels le processeur reste inactif. Quelques règles de bon sens et l'utilisation des
compilateurs de plus en plus optimisés peuvent minimiser ce va-et-vient incessant.
Si l'addition de deux nombres scalaires nécessite des cycles d'horloge, l'addition de deux
vecteurs à composantes nécessite cycles sur un processeur scalaire. En effet, la boucle
précédente est décomposée en instructions machine plus élémentaires pour sommer deux à
deux chaque élément.
Il est donc évident pourquoi l'addition de deux vecteurs nécessite plusieurs cycles sur un
processeur scalaire. Les processeurs vectoriels disposent, outre les registres scalaires, des
registres vectoriels dont les contenus peuvent être additionnés ou multipliés en un cycle. Si
Dans la course vers la performance calculatoire, les ordinateurs parallèles furent l'étape
de sophistication suivante. Dans cette architecture, plusieurs processeurs exécutent des tâches
identiques ou comparables sur des données partagées tant qu'il n'y ait pas de conflit d'accès et
de préséance des opérations sur les données partagées. Cette architecture est appelée MIMD de
multiple instruction multiple data stream. Des ordinateurs multiprocesseurs avec des
mémoires partagées (comme le SYMMETRY) ou des systèmes avec des mémoires locales
(comme l'Intel iPSC) fonctionnent sur ce principe.
Pour une taxonomie plus approfondie sur les diverses architectures, on peut consulter l'article
de [FLYNN]. Paradoxalement, aujourd'hui la technologie avance plus rapidement que la
conception de nouveaux algorithmes parallèles efficaces.
Une instruction
Un flot de données
Ordinateur séquentiel
Pas de parallélisme
Exemple :
Il semble que la question se pose à une époque où il existe encore des machines
multiprocesseurs de la génération antérieure avec des processeurs double cœurs de la
génération en cours (quadcore d’Intel) ou les multiprocesseurs à base de multicoeurs.
Les questions légitimes portent la plupart du temps sur l’aspect performance des choses, et de
faire une comparaison entre les deux architectures comme si il suffisait de faire l’équation
Deux Cœurs = Deux processeurs Simple Cœur, alors que rien n’est moins évident que ce
raccourci et ce autant au niveau matériel que logiciel même…
Commençons d’un point de vue basique, tout le monde peut imaginer assez facilement
qu’entre une machine avec une carte mère ayant plusieurs emplacements pour des processeurs
et une avec un seul il y a des différences techniques qui peuvent déjà commencer à expliquer
les différences de performance entre les deux plateformes.
La première différence concerne les échanges d’informations entre les processeurs, sur une
carte mère multiprocesseur, c’est à elle qu’incombe ces échanges, et c’est par son
intermédiaire qu’ils seront réalisés. Bien entendu et malgré les différentes plateformes
technologiques existantes, les canaux utilisés sont beaucoup plus petits et lents que ne le sont
ceux existant entre deux cœurs sur une même puce.
En effet les échanges entre cœurs sont souvent faits à la vitesse du processeur et souvent sur
des liaisons internes à très grande vitesse.
A propos d’échanges, une différence existe aussi concernant les accès mémoire puisque c’est
le processeur qui les gère sur une machine multicoeurs, ils sont plus facilement accessibles et
les chevauchements sont gérés par le processeur pour éviter que les données ne soient lus ou
écrites à des endroits où d’autres cœurs sont en train de lire ou d’écrire.
Dans le cas du multiprocesseur, c’est la carte mère et son chipset qui gère ceci, et là encore les
goulots d’étranglement sont dans les canaux utilisés pour gérer ça.
Concernant maintenant l’utilisation de tous les jours, quelles sont les différences et surtout
laquelle des différentes plateformes peut être la plus performante?
Tout d’abord il faut savoir qu’une limitation existe suivant le système d’exploitation et du
nombre de processeurs, en effet que ça soit sur des systèmes Windows serveur ou même
Vista, le prix entre une version monoprocesseur (voir parfois biprocesseurs) et
multiprocesseurs et sans commune mesure.
Mais il faut savoir que pour Microsoft un processeur double ou quadruple cœurs n’est rien
d’autre qu’un processeur unique (ce qui est physiquement exact puisque chaque cœur n’est en
fait qu’une unité de calcul et pas un processeur à part entière) et donc que vous pouvez utiliser
un Vista standard sur une machine quadruple cœurs, alors que sur une machine
multiprocesseurs vous devrez vous acquitter d’une version spéciale sans quoi un seul des
processeurs sera pris en compte.
A l’époque ou n’existait pas les multicoeurs, le multitâche était produit par ce que l’on appelle
les threads, ces processus partagent le temps processeur de manière équitable en fonction des
priorités et des nécessités, chaque thread utilisation quelques microsecondes avant de laisser
la main au thread suivant.
Dans tous les systèmes récents, ces threads sont au cœur même du système, une des
différences entre multicoeurs et multiprocesseurs consiste en la répartition de ces threads
entre les unités de calcul, en effet concernant le multiprocesseur, il faut que le système assure
correctement la répartition afin que les différents processeurs soient utilisés à leur maximum
de performance.
Concernant les logiciels aussi, il faut qu’ils utilisent des instructions particulières du système
de manière à faire répartir justement les traitements correctement entre les différents
processeurs, alors que dans le cas du multicoeur, c’est lui qui assure ce travail et qui réparti
justement entre les différents cœurs en fonction du traitement.
Et c’est là l’une des différences essentielle qui fait que pour une utilisation classique d’un
ordinateur (pas une utilisation professionnelle ou spécifique comme un serveur) une machine
multicoeur est plus performante puisque elle optimise en temps réel la charge sur les
différents cœurs.
Ces différences sont bien entendu réduites pour peu que l’on utilise un système réellement
multiprocesseur et des logiciels eux aussi conçus et programmés pour prendre en compte
plusieurs processeurs physiques.
Les multicoeurs vont devenir de plus en plus courants et ce pour plusieurs raisons qui sont
une question de coût, un multicoeur coutant moins à fabriquer que deux processeurs par
exemple. De plus leur puissance fait qu’ils sont un choix bien plus intéressant.
D’ailleurs les machines multiprocesseurs à base de processeurs multicoeurs sont aussi l’avenir
des solutions nécessitant une puissance de calcul importante comme pour les serveurs et leurs
applications diverses.
Ce petit laïus n’est pas exhaustif, il ne couvre pas toutes les différences, mais permet de se
faire une idée assez précise, de tout temps les gens ont toujours été attirés par les
multiprocesseurs, mais il est de cette attirance comme de beaucoup de mythe elle ne repose
que sur le fait que les systèmes multiprocesseurs sont plus rares et plus chers, mais pour les
particuliers ils ne sont et ne seront jamais d’une utilité quelconque, sauf dans le cas ou ces
machines sont utilisées pour leurs réels objectifs et sur des systèmes adéquats.
VI. Le multithreading
Chap2 : Les architectures 16
1. Parallélisme et concurrence
On dit que deux processus s'exécutent en parallèle lorsqu'ils s'exécutent sur des CPU
différents. Ils sont concurrents lorsqu'ils concourent pour l'obtention d'une même ressource
CPU. Leur exécution est alors entrelacée. Chacun dispose à son tour d'un quantum de temps
calcul.
2. Définition du thread :
3. Fonctionnement du thread :
La ressource CPU doit être partagée de manière équilibrée entre les différents threads
et l’ordonnanceur gère cette répartition.
L’algorithme de l’ordonnanceur diffère selon les plate-formes (Unix, Windows,
Macintosh)
Pour un CPU unique, il fonctionne en accordant successivement à chaque thread un
quantum de temps
Chaque thread (processus léger) possède une priorité propre (attribuée par défaut ou
bien choisie par le programmeur). La priorité varie de 1 à 10 (par défaut 5). La
méthode setPriority(int p) permet de fixer la priorité.
L'ordonnanceur attribue généralement le CPU au processus de plus forte priorité. Dès
qu'un quantum se termine, le processus auquel avait été attribué ce quantum, est de
nouveau en compétition pour l'attribution du CPU pendant que le processus de plus
forte priorité reçoit le CPU.
A tout moment, un processus peut décider lui-même de céder le CPU pour donner une
chance aux autres de s'exécuter.
Au début des années 80, des bus de terrain ont été développés par les constructeurs
automobiles et équipementiers : bus CAN par Bosch, Van par Renault et J1850 par les
constructeurs américains. Ces bus filaires dédiés aux environnements perturbés tels que les
automobiles permettent de relier entre eux les calculateurs. Grâce à ses bus, les applications
temps réel embarquées des architectures que l’on qualifie ici de faiblement distribuées. Elles
sont constituées d’un ensemble de calculateurs et il est possible de partager des capteurs et
actionneurs entre les calculateurs.
2. Architecture embarquée fortement distribuée
Les capteurs et actionneurs deviennent intelligents, ils peuvent directement être connectés sur
le bus. Cette approche permet de réduire considérablement tous les câblages, car tous les
organes électriques du système embarqué peuvent être reliés au bus. C’est aussi l’approche la
plus complexe à mettre en œuvre au niveau logiciel. Il faut gérer efficacement le multiplexage
des données issues des capteurs et des calculateurs sur le bus de telle sorte que les contraintes
temporelles de chacun des signaux soient satisfaites.
Les architectures temps réels réel embarqués sont actuellement non seulement fortement
distribuées, mais elles sont aussi hétérogènes. Une même application peut intégrer une dizaine
de calculateurs. Pour garantir une exécution temps réel des algorithmes il est parfois
nécessaire de disposer d’une grande puissance de calcul fournie par exemple par des Digital
Signal Processor ou des microprocesseurs performants. Certaines fonctions peuvent aussi être
programmées sur des FPGA ou des ASIC. Des microcontrôleurs sont aussi utilisés pour leur
capacité à gérer un grand nombre d’entrées et de sorties avec un minimum de composants
périphériques
Séquence de 30 images/s
Les systèmes informatiques temps réel sont aujourd'hui présents dans de nombreux secteurs
d'activités :
Introduction :
UML2 permet de modéliser quelques aspects temps réel. UML2 ajoute des concepts de
base pouvant être utilisé pour modéliser certaines caractéristiques particulières des
applications temps réel et embarqué. Il ne satisfait pas complètement les besoins de
modélisation de ces catégories du système. Pour combler cette lacune un ensemble
d’extensions a été ajouté à UML. Les extensions courantes et normées sont dédiées à
l’analyse, la modélisation et l’implémentation d’applications temps réel et embarquées.
Certains véhicules sont équipés d’un système capable de réguler la vitesse courante de
la voiture autour d’une vitesse, dite vitesse de consigne. Cette vitesse est fixée par
l’utilisateur et représente la vitesse à laquelle il désire rouler automatiquement. Cet exemple
se veut simple mais cependant suffisamment représentatif des problèmes que l’on peut
rencontrer dans la modélisation d’applications embarquées pour une automobile. C’est un
système qui doit être réactif, qui présente des comportements cycliques et qui doit
être en forte interaction avec son environnement.
Le système que l’on veut réaliser est donc un régulateur d’allure capable de
maintenir la vitesse d’un véhicule à une vitesse consigne cible fixée par le
conducteur. Le maintien de la vitesse s’effectuera par envoi d’une variation de couple au
système de contrôle du moteur. La loi de commande que le régulateur d'allure
utilise pour calculer la variation du couple est la suivante :
Les temps de réponse attendus pour l’arrêt de la régulation sont : de 0,5 s par appui sur le
frein ou par action sur le bouton Marche/Arrêt; ou le bouton Allumer/Eteindre ; et de 100
ms si la vitesse du véhicule devient inférieure à 50 km/h.
De plus, lorsque le conducteur appuie sur la pédale d’accélération alors que la régulation est
activée, la régulation de vitesse est suspendue jusqu’à ce que le conducteur relâche
l’accélérateur. La suspension doit être réalisée en au plus 200 ms et la reprise en au
plus 250 ms. Le système reprend alors le contrôle de la vitesse du véhicule et l’amène
progressivement à la vitesse de consigne fixée avant l’accélération.
En cas de simultanéité des actions de décélération et d’accélération, c’est l’action de
décélération qui doit être prise en compte prioritairement. La régulation doit alors être
arrêtée et non suspendue avec le délai d’un arrêt par appui sur la pédale de frein.
Le régulateur d’allure est connecté à un écran qui affiche la vitesse de consigne et l’état
d’activation de la régulation (désactivée, activée ou suspendue). Il fonctionne à la demande
avec un temps de réponse attendu de 0,5 s.
Les valeurs de temps peuvent être représentées par un stéréotype particulier de valeurs,
appelé RTime regroupant différents formats
1. Définition
L’origine des Design Patterns remonte au début des années 70 avec les travaux de l’architecte
Christopher Alexander. Celui-ci remarque que la phase de conception en architecture laisse
apparaître des problèmes récurrents. Il cherche alors à résoudre l’ensemble de ces problèmes
liés à des contraintes interdépendantes (solidité de la structure, étanchéité...). Pour cela
Alexander établi un langage de 253 patterns, qui couvrent tous les aspects de la construction
(comme par exemple la façon de concevoir une charpente).
Un Design Pattern est une solution à un problème récurrent dans la conception d’applications
orientées objet. Un patron de conception décrit alors la solution éprouvée pour résoudre ce
problème d’architecture de logiciel. Comme problème récurrent on trouve par exemple la
conception d’une application où il sera facile d’ajouter des fonctionnalités à une classe sans la
modifier (voir la solution du Design Pattern Visiteur). A noter qu’en se plaçant au niveau de
la conception les Design Patterns sont indépendants des langages de programmation utilisés.
De plus, les Design Patterns sont réutilisables et permettent de mettre en avant les bonnes
pratiques de conception. Les Design Patterns étant largement documentés et connus d’un
grand nombre de développeurs ils permettent également de faciliter la communication. Si un
développeur annonce que sur ce point du projet il va utiliser le Design Pattern Observateur il
est compris des informaticiens sans pour autant rentrer dans les détails de la conception
(diagramme UML, objectif visé...).
Le profil SPT (Schedulability, Performance and Time) est proposé par l'OMG comme
un standard pour modéliser les systèmes temps réel [6]. Cette norme propose une façon
standard d'annoter les modèles UML avec des caractéristiques de QdS ("Qualité de
services"). Elle définit aussi une façon commune pour la représentation des informations
temporelles. Cette standardisation permet de faciliter l'interaction entre les différents
modèles UML et outils d'analyse de performance. Il est alors possible de vérifier et valider
des propriétés extra fonctionnelles comme les temps de réponse ou la taille des files
d'attente en se basant sur les données comme les échéances, le temps d’exécution de pire cas
(WCET) ou les politiques d'ordonnancement.
Plusieurs outils pour développer les STRE : STARUML Papyrus Poseidon ArgoUML
3. Les avantages
L’utilisation des Design Patterns offre de nombreux avantages. Tout d’abord cela
permet de répondre à un problème de conception grâce à une solution éprouvée et validée par
des experts. Ainsi on gagne en rapidité et en qualité de conception ce qui diminue également
les coûts.
De plus, les Design Patterns sont réutilisables et permettent de mettre en avant les bonnes
pratiques de conception. Les Design Patterns étant largement documentés et connus d’un
grand nombre de développeurs ils permettent également de faciliter la communication. Si un
développeur annonce que sur ce point du projet il va utiliser le Design Pattern Observateur il
est compris des informaticiens sans pour autant rentrer dans les détails de la conception
(diagramme UML, objectif visé...).
Description du problème
On trouve des classes possédant des attributs dont les valeurs changent régulièrement. De
plus, un certain nombre de classes doit être tenu informé de l’évolution de ces valeurs. Il n’est
pas rare d’être confronté à ce problème notamment en développant une classe métier et les
classes d’affichages correspondantes.
On peut identifier deux solutions. Soit la classe d’affichage se charge de demander à la classe
HeurePerso la valeur de son attribut soit c’est la classe HeurePerso qui informe la classe
AfficheHeure lors de changements.
Il est facile de s’apercevoir que la première solution n’est pas la meilleure. En effet, quand la
classe AfficheHeure devra t-elle questionner HeurePerso pour obtenir l’heure courante ?
Toutes les minutes ? Toutes les secondes ? Quelque soit l’intervalle choisi, soit l’heure ne sera
pas précise soit on surchargera d’appels inutiles la classe HeurePerso.
Définition de la solution
Le diagramme UML du pattern Observateur définit deux interfaces et deux classes. L’interface
Observateur sera implémenté par toutes classes qui souhaitent avoir le rôle d’observateur. C’est le
cas de la classe ObservateurConcret qui implémente la méthode actualiser(Observable). Cette
méthode sera appelée automatiquement lors d’un changement d’état de la classe observée.
On trouve également une interface Observable qui devra être implémentée par les classes
désireuses de posséder des observateurs. La classe ObservableConcret implémente cette interface,
ce qui lui permet de tenir informer ses observateurs. Celle-ci possède en attribut un état (ou
plusieurs) et un tableau d’observateurs. L’état est un attribut dont les observateurs désirent suivre
l’évolution de ses valeurs. Le tableau d’observateurs correspond à la liste des observateurs qui sont
à l’écoute. En effet, il ne suffit pas à une classe d’implémenter l’interface Observateur pour être à
l’écoute, il faut qu’elle s’abonne à un Observable via la méthode ajouterObservateur(Observateur).
En effet, la classe ObservableConcret dispose de quatre méthodes que sont
ajouterObservateur(Observateur), supprimerObservateur(Observateur), notifierObservateurs() et
getEtat(). Les deux premières permettent, respectivement, d’ajouter des observateurs à l’écoute
de la classe et d’en supprimer. En effet, le pattern Observateur permet de lier dynamiquement
(faire une liaison lors de l’exécution du programme par opposition à lier statiquement à la
compilation) des observables à des observateurs. La méthode notifierObservateurs() est appelée
lorsque l’état subit un changement de valeur. Celle-ci avertit tous les observateurs de cette mise à
jour. La méthode getEtat() est un simple accesseur en lecture pour l’état. En effet, les
observateurs récupèrent via la méthode actualiser(Observable) un pointeur vers l’objet observé.
Puis, grâce à ce pointeur, et à la méthode getEtat() il est possible d’obtenir la valeur de l’état.
Approfondissement de la solution
Une des questions récurrente face à ce pattern est pourquoi ces deux interfaces ? D’ailleurs on
trouve sur Internet des implémentations de ce pattern sans ces deux interfaces... Mais l’utilisation
de ces interfaces permet de coupler faiblement l’observable à ses observateurs. En effet, un
principe de conception est de lier des interfaces plutôt que des classes afin de pouvoir faire évoluer
le modèle facilement. L’utilisation de ces deux interfaces n’est donc pas obligatoire mais elle est
vivement conseillée.
Cependant, il existe une variation possible lors de l’utilisation de ce pattern. Dans la solution
présentée ci dessous, une référence vers l’objet observable est mis à disposition de chaque
observateur. Ainsi les observateurs peuvent l’utiliser pour appeler la méthode getEtat() et ainsi
obtenir l’état de l’observable. Cette solution est nommée « TIRER » car c’est aux observateurs, une
fois avertis de l’évolution, d’aller chercher l’information sur l’état. Mais il existe la solution inverse
appelée « POUSSER ». Dans ce cas, on passe directement l’état actuel de l’observable dans la
méthode actualiser(TypeEtat). Ainsi les observateurs disposent directement de l’état. Mais
pourquoi avoir présenté la solution nommée « TIRER » plutôt que l’autre ? Parce qu’elle permet
une fois de plus de lier faiblement l’observable à ses observateurs. En effet, si l’observateur
dispose d’un pointeur vers l’objet observable et que la classe observable évolue en ajoutant un
deuxième état. L’observateur souhaitant se tenir informé de ce deuxième état aura juste à appeler
l’accesseur correspondant. Alors que si on « POUSSER » il faudrait changer la signature de la
méthode ce qui peut s’avérer plus dommageable.
Conséquences
Un exemple que l’on rencontre souvent pour illustrer ce pattern est une représentation entre une
entreprise qui diffuse un magazine et des personnes qui souhaitent s’y abonner (observateurs) et
donc le recevoir régulièrement.
Le pattern observateur permet de lier de façon dynamique un observable à des observateurs. Cette
solution est faiblement couplée ce qui lui permet d’évoluer facilement avec le modèle. D’ailleurs le
pattern Observateur est très utilisé. Il fait partie, par exemple, des patterns indispensables pour
mettre en place le modèle MVC (Modèle Vue Contrôleur) très en vogue actuellement.
Exemples d'implémentations:
Description du problème
Dans la programmation orientée objet, la façon la plus classique d’ajouter des fonctionnalités à une
classe est d’utiliser l’héritage. Pourtant il arrive parfois de vouloir ajouter des fonctionnalités à une
classe sans utiliser l’héritage. En effet, si l’on hérite d’une classe la redéfinition d’une méthode peut
entraîner l’ajout de nouveaux bugs. On peut aussi être reticent à l’idée que des méthodes de la
classe mère soient appelées directement depuis notre nouvelle classe.
De plus, l’héritage doit être utilisé avec parcimonie. Car si on abuse de ce principe de la
programmation orientée objet, on aboutit rapidement à un modèle complexe contenant un grand
nombre de classes.
Un autre souci de l’héritage est l’ajout de fonctionnalités de façon statique. En effet, l’héritage de
classe se définit lors de l’écriture du programme et ne peut être modifié après la compilation. Or,
dans certains cas, on peut vouloir rajouter des fonctionnalités de façon dynamique.
D’une manière générale on constate que l’ajout de fonctionnalités dans un programme s’avère
parfois délicat et complexe. Ce problème peut être résolu si le développeur a identifié, dès la
conception, qu’une partie de l’application serait sujette à de fortes évolutions. Il peut alors faciliter
ces modifications en utilisant le pattern Décorateur. La puissance de ce pattern qui permet
d’ajouter (ou modifier) des fonctionnalités facilement provient de la combinaison de l’héritage et de
la composition. Ainsi les problèmes cités ci-dessus ne se posent plus lors de l’utilisation de ce
pattern.
Diagramme UML
Suivant les besoins spécifiques de chacun ce pattern peut être adapté. En effet, il est tout à fait
possible d’utiliser des interfaces pour le composant et le décorateur. Dans ce cas, les attributs et
les méthodes seront définis dans les sous classes.
Bien sûr ce pattern utilise largement l’héritage mais il utilise aussi la composition grâce à l’attribut
Composant présent dans le décorateur. C’est l’alliance de ces deux procédés qui permet à ce
pattern d’être si efficace.
Conséquences
Comme tous les patrons de conception, Décorateur ne doit pas être utilisé à tord et à travers. Mais
lors de la conception de classes qui risquent d’évoluer fortement (ajout ou modification de
fonctionnalités) celui-ci sera très utile. Il est donc important de bien réfléchir aux points sensibles
de l’application qui risquent d’évoluer au fil du temps et cela dès la phase d’analyse.
Attention tout de même à l’utilisation des types concrets. Si votre application se base sur les types
concrets d’objets utilisés dans le pattern décorateur cela posera des problèmes. En effet, une fois
De plus, lors de l’utilisation du pattern Décorateur, on constate qu’il est fastidieux de gérer tous les
objets créés et de les décorer. C’est pour cette raison que ce pattern est souvent utilisé avec le
pattern Fabrique ou Monteur qui répondent à cette problématique.
Exemples d'implémentations:
Le design pattern Fabrique (Factory Method) définit une interface pour la création d'un
objet en déléguant à ses sous-classes le choix des classes à instancier.
Description du problème
Il est fréquent de devoir concevoir une classe qui va instancier différents types d'objets suivant un
paramètre fourni. Par exemple une usine va fabriquer des produits en fonction du modèle qu'on lui
indique.
L'idée la plus simple pour répondre à ce besoin est d'écrire une succession de conditions qui
suivant le modèle demandé, instancie et retourne l'objet correspondant.
Le problème avec cette implémentation, c'est que la classe correspondant à l'usine va être
fortement couplée à tous les produits qu'elle peut instancier car elle fait appel à leur type concret.
Or ce code va être amené à évoluer régulièrement lors de l'ajout de nouveaux produits à fabriquer
ou de la suppression de certains produits obsolètes.
De plus, il est fort probable que l'instanciation des différents produits soit également réalisée dans
d'autres classes par exemple pour présenter un catalogue des produits fabriqués.
On se retrouve alors avec du code fortement couplé, qui risque d'être dupliqué à plusieurs endroits
de l'application.
Début de solution
La première solution est de regrouper l'instanciation de tous les produits dans une seule classe
chargée uniquement de ce rôle. On évite alors la duplication de code et on facilite l'évolution au
niveau de la gamme des produits.
Cette solution appelée Fabrique Simple est une bonne pratique de conception mais pas un design
pattern. En effet, le design pattern Fabrique est plus évolué et offre plus de flexibilité, donc
attention à ne pas les confondre. Pour autant cette bonne pratique est régulièrement utilisée et
s'avère efficace dans les cas les plus simples (voir son diagramme UML ci-dessous).
Si par la suite l'entreprise évolue et a besoin de plusieurs usines, chacune spécialisée dans la
fabrication de certains produits, Fabrique Simple ne va plus suffire.
Dans ce cas il faut prévoir l'utilisation du design pattern Fabrique dont le diagramme UML est
présenté ci-dessous.
Diagramme UML
Définition de la solution
Le créateur contient toutes les méthodes permettant de manipuler les produits exceptée la
méthode creerProduit qui est abstraite. Les créateurs concrets implémentent la méthode
creerProduit qui instancie et retourne les produits. Chaque créateur concret peut donc créer des
produits dont il a la responsabilité. Pour finir tous les produits implémentent la même interface afin
que les classes utilisant les produits (comme le créateur) puissent s'y référer sans connaître les
types concrets.
Si une partie de l'implémentation est identique à tous les produits concrets, alors l'interface Produit
peut être une classe abstraite afin d'intégrer ce code partagé dans celle-ci. Il est également
bénéfique d'utiliser ce pattern même si l'on a qu'un seul CreateurConcret ou qu'un CreateurConcret
n'instancie qu'un seul Produit car les avantages liés au découplage des produits et du créateurs
sont conservés.
A noter que les créateurs concrets utilisent la bonne pratique Fabrique Simple présentée
précédemment. Mais comparé à cette bonne pratique qui ne sert qu'une fois, le design pattern
Fabrique permet de créer une structure où l'ajout d'une sous classe (c'est à dire un créateur
concret) permet de choisir les produits qui seront utilisés. C'est d'ailleurs ce qui explique la
définition de ce pattern.
Pour créer des familles de produits cohérentes, on sera amené à utiliser le design pattern Fabrique
Abstraite qui répond spécifiquement à ce besoin.
Exemples d'implémentations: