Le Pattern Observateur en C#
Date de publication : 12 Mai 2008
Par
Pierre Caboche (autres articles)
Ceci n'est pas un n-ième article sur "Comment implémenter le pattern X
dans le langage Y". Le but de cet article est de montrer combien il est
facile d'implémenter le pattern Observateur en C# en se servant de certaines
spécificités du langage. Ainsi cela aura peut-être pour effet d'encourager
l'usage de ce pattern très intéressant.
Prérequis
Principe
Le pattern Observateur
Intérêt de la version C#
Static / non static, visibilité
Implémentation
Sujet
Observateur
Résumé
Discussion
5.1. Déclaration de l'événement
5.2. Le sender
5.3. Les événements : précautions à prendre
Vers une classe générique
Utilisation du pattern Observateur
Conclusion
Remerciements
Prérequis
Les prérequis pour comprendre cet article sont :
-
Maîtriser la Programmation Orientée-Objets
-
Connaître les Design Patterns
-
Connaître le pattern Observateur
Une description rapide du pattern Observateur sera donnée en introduction de
l'article.
-
Connaître le langage C#
Principe
Le pattern Observateur
Voici en résumé le principe du pattern Observateur...
Les protagonistes :
-
Un objet (appelé "Sujet") est susceptible de changer d'état
-
Un ou plusieurs objets (appelés "Observateurs") veulent être tenus au courant
des changements d'état du sujet (afin, par exemple, de pouvoir changer leur
propre état)
Pour cela, on implémente le pattern Observateur :
-
les Observateurs indiquent au Sujet qu'ils veulent être tenus au courant
des changements d'état du Sujet
-
lorsqu'un changement d'état survient chez le Sujet, celui-ci avertit ses
Observateurs
Grace aux EventHandlers et au mot-clef event de C#, la relation "qui
observe quoi" est entièrement gérée par le langage, ce qui facilite
considérablement l'implémentation du pattern Observateur.
Tout ce qu'il reste à faire, c'est :
-
définir les Observateurs qui doivent être notifiés
-
implémenter les comportement des Observateurs lorsqu'un changement survient
Intérêt de la version C#
En C#, le pattern Observateur est particulièrement facile à mettre en place car
le langage dispose de certaines fonctionnalités propres au langage (que nous
présenterons par la suite).
Une implémentation basique tient en quelques lignes de code seulement :
Sujet |
public class Subject
{
public event System. EventHandler SubjectChangedState;
public void UneMethode ()
{
. . .
if (SubjectChangedState ! = null )
SubjectChangedState (this , EventArgs. Empty);
. . .
}
}
|
Observateur |
public class Observer
{
private void OnSubjectChanged (object sender, EventArgs e)
{
. . .
}
private void Initialisation ()
{
. . .
mySubject. SubjectChangedState + = new Subject. SubjectChangedState (this . OnSubjectChanged);
. . .
}
}
|
Et c'est tout !
Tout le reste est géré par C#...
Static / non static, visibilité
Static / non static :
Un Observateur peut être placé soit au niveau de l'objet (non static),
soit au niveau de la classe (static).
S'il est placé au niveau de l'objet (non static), l'Observateur permet de
rapporter les changements d'état survenus sur l'objet en question.
S'il est placé au niveau de la classe (static), l'Observateur permet de
rapporter des événements plus généraux sur un ensemble d'objets
(création/suppression d'objets, etc.)
Un Observateur implémenté au niveau de la classe se caractérise par le fait que
toutes les méthodes et propriétés qu'il utilise sont déclarés en static
(c'est-à-dire au niveau de la classe)
Visibilité :
Un Observateur peut être indifféremment déclaré public, protected
ou private.
Cependant, dans la pratique, un Observateur sert le plus souvent à faciliter
la communication entre différents objets (Observateurs et Sujets) et donc
l'Observateur est le plus souvent rendu public (à l'inverse, un
Observateur déclaré private par exemple, ne permet d'assurer la
communication qu'entre objets de la même classe).
Dans la section suivante, nous allons entrer dans le détail de l'implémentation
et expliquer le fonctionnement de celle-ci.
Implémentation
Au niveau du Sujet:
-
définir, au moyen d'un delegate, la signature de la méthode qui sera appelée
lors du changement d'état (= EventHandler)
-
à partir de l'EventHandler précédemment créé, définir l'événement
qui sera appelé
-
partout où cela est nécessaire, déclencher l'événement
Au niveau de l'Obervateur:
-
Implémenter le comportement de l'Observateur lorsque l'événement sera déclenché
-
Ajouter la liaison entre l'Observateur et le Sujet observé
Sujet
1. Définition d'un delegate :
Un delegate permet de définir la signature d'une méthode (nombre d'arguments,
type des arguments, type retourné par la méthode).
Ici, il s'agit de définir la signature de la méthode qui sera appelée chez un
Observateur lorsque l'événement sera déclenché par le Sujet :
public delegate void SubjectChangedState_EventHandler (Object sender, EventArgs e);
|
Le delegate ainsi créé dérive de EventHandler et comporte 2 arguments :
- Object sender : l'objet ayant déclenché l'événement
-
EventArgs e : un objet permettant d'apporter des précisions concernant
le changement d'état (dérive de la classe EventArgs)
2. Définition de l'événement :
L'événement est défini à partir du delegate :
public event SubjectChangedState_EventHandler SubjectChangedState;
|
|
Il existe des manières de définir l'événement sans avoir besoin de déclarer de
delegate. Nous exposerons ces méthodes dans la partie "Discussion".
|
3. Déclenchement de l'événement :
Partout où cela est nécessaire, on déclenche l'événement :
public void UneMethode ()
{
. . .
if (SubjectChangedState ! = null )
SubjectChangedState (this , EventArgs. Empty);
. . .
}
|
Ceci aura pour effet de propager l'événement chez chacun des Observateurs.
|
Si l'événement n'a pas besoin d'argument, utilisez EventArgs.Empty
plutôt que null.
|
Observateur
1. Implémentation de l'événement :
private void OnSubjectChanged (object sender, MyEventArgs e)
{
. . .
}
|
2. Liaison entre le Sujet et un Observateur :
mySubject. SubjectChangedState + = new Subject. SubjectChangedState (this . OnSubjectChanged);
|
Résumé
Au niveau du Sujet :
- on définit le delegate
- on définit l'événement
- on déclenche l'événement
Sujet (C# 2.0 et supérieur) |
public class Subject
{
public delegate void SubjectChangedState_EventHandler (object sender, MyEventArgs e);
public event SubjectChangedState_EventHandler SubjectChangedState;
public void UneMethode ()
{
. . .
if (SubjectChangedState ! = null )
SubjectChangedState (this , EventArgs. Empty);
. . .
}
}
|
Au niveau de l'Observateur :
- on implémente l'événement
- on effectue la liaison entre le Sujet et l'Observateur
Observateur |
public class Observer
{
private void OnSubjectChanged (object sender, MyEventArgs e)
{
Subject mySubject = (Subject) sender;
. . .
}
private void Initialisation ()
{
. . .
mySubject. SubjectChangedState + = new Subject. SubjectChangedState (this . OnSubjectChanged);
. . .
}
}
|
|
Dans le code précédent :
- MyEventArgs est de type EventArgs
- mySubject est un objet de type Subject
|
Discussion
5.1. Déclaration de l'événement
Il existe plusieurs façons de déclarer notre événement.
1 : La première consiste à définir un delegate puis déclarer notre événement à
partir de ce delegate :
public delegate void SubjectChangedState_EventHandler (object sender, EventArgs e);
public event SubjectChangedState_EventHandler SubjectChangedState;
|
2 : Si on n'a pas besoin de passer de paramètre à l'événement, on peut utiliser le
delegate System.EventHandler pour déclarer :
public event System. EventHandler SubjectChangedState;
|
3 : Si on a besoin de passer des paramètres à l'événement (grâce à une sous-classe
de EventArgs), à partir du framework 2.0 il est possible d'utiliser le
template System.EventHandler<T> :
public event System. EventHandler< MyEventArgs> SubjectChangedState;
|
La première solution est certes plus verbeuse. En contrepartie, elle semble plus
facile à relire que la solution avec template System.EventHandler<T>.
J'ai donc une préférence pour la première écriture dans le cas où l'on a besoin
d'un delegate autre que EventHandler.
5.2. Le sender
D'après la documentation C#, un délégué d'événement doit avoir les
caractéristiques suivantes :
-
ne pas retourner de valeur (void)
-
le premier paramètre est du type Object, c'est le sender
-
le second paramètre est simplement une instance de EventArgs
|
Pour information, voici exactement ce que dit la documentation :
La signature standard d'un délégué de gestionnaire d'événements définit une
méthode qui ne retourne pas de valeur, dont le premier paramètre est du type
Object et fait référence à l'instance qui déclenche l'événement, et dont le
second paramètre est dérivé du type EventArgs et détient les données d'événement.
Si l'événement ne génère pas de données d'événement, le second paramètre est
simplement une instance de EventArgs. Sinon, le second paramètre est un type
personnalisé dérivé de EventArgs et fournit tous les champs ou les propriétés
nécessaires pour conserver les données d'événement.
http://msdn2.microsoft.com/fr-fr/library/system.eventhandler(VS.80).aspx
|
Les délégués d'événement standard ont donc comme premier paramètre une variable
de type Object. La différence entre deux délégués d'événement ne peut se
faire que sur le deuxième paramètre qui est une sous-classe d'EventArgs.
Dans le cas de notre Observateur, on sait que les événements ne seront appelés
que par les Sujets, et donc que le sender sera de type Subject (ou une sous-classe).
Le fait de déclarer le paramètre sender de type Object a les conséquences suivantes :
-
on perd l'information concernant le type de l'objet déclencheur
-
pour accéder aux méthodes propres au Sujet, on est obligé de faire une
conversion de type (cast). J'aime quand les variables sont fortement typées,
donc je déteste les cast
-
IntelliSense ne peut plus faire la distinction que sur le deuxième argument.
IntelliSense ne peut réduire le nombre de choix possibles.
En fait, il est tout à fait possible de déclarer un délégé d'événement dont le
sender est d'un type autre que Object.
|
Voici ce que dit la documentation à ce sujet :
Bien que les événements des classes que vous définissez puissent être basés sur
tout type délégué valide, y compris les délégués qui retournent une valeur, il
est généralement recommandé de baser les événements sur le modèle du .NET Framework
en utilisant EventHandler...
http://msdn2.microsoft.com/fr-fr/library/w369ty8x(VS.80).aspx
|
Il est donc tout à fait envisageable de spécifier le type exact de sender :
public delegate void SubjectChangedState_EventHandler (Subject sender, EventArgs e);
public event SubjectChangedState_EventHandler SubjectChangedState;
|
... ce code ne pose aucun problème à la compilation et s'exécute correctement,
par contre cela va à l'encontre des recommendations données par Microsoft.
Je trouve un peu dommage qu'en suivant à la lettre les recommandations on perde
de l'information sur le type du sender, qu'on soit obligé de faire des conversions
de type "à la sauvage" et qu'IntelliSense soit moins précis. Je trouve qu'on a
plus à y perdre qu'à y gagner. Donc, à vous de voir si vous souhaitez suivre
à la lettre ces recommandations...
5.3. Les événements : précautions à prendre
Notre implémentation du pattern Observateur a été grandement simplifiée par
l'utilisation des événements du langage C#. En effet, c'est grâce à ce mécanisme
que l'on gère la relation "qui observe quoi" et c'est C# qui gère tous les
appels de méthodes.
Dans notre implémentation, l'usage qui est fait des événements C# est vraiment
très basique : le Sujet notifie ses Observateurs de manière synchrone, les
Observateurs enregistrent immédiatement le changement.
Cependant, il faut garder à l'esprit qu'une gestion plus complexe des événements
fait apparaître de nouvelles problématiques, notamment en matière de
synchronisation des processus. Sans oublier non plus que la programmation basée
sur les événements rendent les programmes beaucoup plus difficiles à comprendre
(la logique générale du programme est disséminée sur plusieurs événements).
Il faut prendre en compte ces considérations lorsque l'on développe avec des
événements, ne pas oublier qu'un événement peut éventuellement générer d'autres
événements (c'est notamment le cas quand un Observateur est lui-même le Sujet
d'observation d'un autre objet), que l'on peut avoir une cascade d'événements,
voire même une boucle. C'est là que la synchronisation devient nécessaire
(les variables nécessaires à la synchronisation peuvent apparaître dans l'objet
EventArgs passé en paramètre lors de l'invocation de l'événement).
Vers une classe générique
Comme nous l'avons vu dans cet article, C# facilite énormément l'implémentation
du pattern Observateur grâce aux événements. Il est cependant possible de rendre
l'implémentation encore plus simple.
En effet, pourquoi effectuer des copier/coller de code alors que l'on pourrait
tout aussi bien écrire une classe générique pour répondre à notre besoin ?
Une telle classe permettrait de s'affranchir de la déclaration des délégués, des
événements ainsi que de la mise en place de certains mécanismes (car cela serait
au niveau de la classe).
Lorsqu'on souhaite mettre en place le pattern Observateur, on a juste besoin de connaître :
- le type du Sujet
-
le type de l'EventArgs qui permettra de transmettre des données aux
Observateurs
Pour le reste, l'implémentation reste la même. On peut donc encapsuler ces
mécanismes au sein d'une classe générique.
Pour l'implémentation de cette classe, nous allons tenir compte des remarques
qui ont été faites dans la partie "Discussion", notamment concernant le type
du sender.
Classe générique pour notification des Observateurs |
public class Notifier< TSender, TEventArgs>
where TEvenArgs : EventArgs
{
public delegate void Callback (TSender sender, TEventArgs e);
private event Callback callbacks;
< summary >
< / summary >
< param name = " sender " > < / param >
< param name = " e " > < / param >
public void Notify (TSender sender, TEventArgs e)
{
if (callbacks ! = null )
callbacks (sender, e);
}
< summary >
< / summary >
< param name = " c " > < / param >
public void AddSubscription (Callback c)
{
callbacks + = c;
}
< summary >
< / summary >
< param name = " c " > < / param >
public void RemoveSubscription (Callback c)
{
callbacks - = c;
}
}
|
Vous voyez, ce n'est pas une classe très compliquée...
Voici comment l'utiliser :
Le sujet MySubject créé un Notifier pour assurer la communication
avec ses observateurs :
private Notifier< MySubject, MyEventArgs> _eventNotifier = new Notifier< MySubject, MyEventArgs> ();
public Notifier< MySubject, MyEventArgs> EventNotifier
{
get
{
return _eventNotifier;
}
}
|
Un objet (Observateur) peut s'abonner au Notifier pour recevoir de l'information :
mySubject. EventNotifier. AddSubscription ( this . UneMethode );
|
Le sujet MySubject utilise son Notifier pour envoyer un message à
tous ses abonnés :
_eventNotifier. Notify (this , new MyEventArgs ( liste_des_parametres ));
|
Un Observateur peut se désabonner à tout moment :
mySubject. EventNotifier. RemoveSubscription ( this . UneMethode );
|
Utilisation du pattern Observateur
Le pattern Observateur permet à un objet (le Sujet) d'avertir d'autres objets
(ses Observateurs) d'un changement d'état.
Le traitement lié au changement d'état peut être différé (et même exécuté
beaucoup plus tard). C'est une différence fondammentale par rapport à la
programmation événementielle, où les traitements sont exécutés dès le déclenchement
de l'événement (ce qui peut poser d'énormes problèmes de synchronisation et
de maintenance).
Avec le pattern Observateur, on cherche avant tout à être averti des changements
d'état. Dans cet article, les événements C# ne sont qu'un outil facilitant la
diffusion des informations liées à un changement d'état. On ne cherche pas à
faire de la programmation événementielle.
Dans cette optique, le pattern Observateur est très intéressant car il permet
de définir des relations dynamiques entre les objets. Les Sujets informent leurs
Observateurs d'un changement d'état; ceux-ci en tiennent compte (sans forcément
effectuer immédiatement le traitement qui peut se révéler très lourd). Au moment
propice, l'Observateur peut tenir compte des derniers changements survenus et
lancer le(s) traitement(s) appropriés.
Conclusion
Je trouve que le pattern Observateur est un pattern très intéressant. Il permet
de définir des interactions entre plusieurs objets sans pour autant devoir
"casser" l'encapsulation de ces objets : le Sujet n'a pas besoin de connaître
l'interface de ses Observateurs pour pouvoir communiquer avec eux et signaler
un changement d'état.
Par ailleurs ces interactions peuvent être gérées dynamiquement
(on peut à loisir ajouter ou enlever des Observateurs).
Le langage C# permet de simplifier considérablement l'implémentation
du pattern Observateur par rapport à d'autres langages. En effet, dans d'autres
langages il est nécessaire de gérer soi-même les relations entre objets (relation
Observateur-Sujet, appels de méthodes...) alors qu'en C# ce mécanismes sont déjà
implémentés par l'intermédiaire des événements.
Ceci devrait encourager l'adaption du pattern Observateur en C#.
Par ailleurs, l'utilisation des événements C# pour implémenter le pattern
Observateur encourage l'utilisation d'une interface commune (celle des
EventHandlers) dans l'ensemble des projets faisant usage du pattern
Observateur, ce qui est une excellente chose pour faciliter la compréhension
du code.
À partir de tous ces éléments, nous proposons une classe générique
permettant de simplifier encore plus l'implémentation du pattern
Observateur.
Remerciements
Copyright © 2007 pcaboche.
Aucune reproduction, même partielle, ne peut être faite
de ce site ni de l'ensemble de son contenu : textes, documents, images, etc.
sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à
trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.