IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

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 :
  1. définir les Observateurs qui doivent être notifiés

  2. 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
{
	// Définition de l'événement
	public event System.EventHandler SubjectChangedState;


	public void UneMethode()
	{
		...
	
		// Déclenchement de l'événement
		if (SubjectChangedState != null)
			SubjectChangedState(this, EventArgs.Empty);

		...
	}
}
Observateur
public class Observer
{
	// Implémentation de l'événement
	private void OnSubjectChanged(object sender, EventArgs e)
	{
		// Actions à effectuer lors du déclenchement de l'événement
		...
	}

	private void Initialisation()
	{
		...
	
		// Liaison entre le Sujet un Observateur
		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:
  1. définir, au moyen d'un delegate, la signature de la méthode qui sera appelée lors du changement d'état (= EventHandler)

  2. à partir de l'EventHandler précédemment créé, définir l'événement qui sera appelé

  3. partout où cela est nécessaire, déclencher l'événement

Au niveau de l'Obervateur:
  1. Implémenter le comportement de l'Observateur lorsque l'événement sera déclenché

  2. 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 :
// Création du delegate
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 :
// Définition de l'événement
public event SubjectChangedState_EventHandler SubjectChangedState;
info 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()
{
    ...
	
    // Déclenchement de l'événement
    if (SubjectChangedState != null)
        SubjectChangedState(this, EventArgs.Empty);

    ...
}
Ceci aura pour effet de propager l'événement chez chacun des Observateurs.

info 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 :

// Implémentation de l'événement
private void OnSubjectChanged(object sender, MyEventArgs e)
{
    // Actions à effectuer lors du déclenchement de l'événement
    ...
}
2. Liaison entre le Sujet et un Observateur :

// Liaison entre le Sujet 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
{
    // Définition du delegate
    public delegate void SubjectChangedState_EventHandler(object sender, MyEventArgs e);

    // Définition de l'événement
    public event SubjectChangedState_EventHandler SubjectChangedState;
	
	
    public void UneMethode()
    {
        ...
	
        // Déclenchement de l'événement
        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
{
	// Implémentation de l'événement
	private void OnSubjectChanged(object sender, MyEventArgs e)
	{
		// Conversion de type
		Subject mySubject = (Subject) sender;
	
		// Actions à effectuer lors du déclenchement de l'événement
		...
	}



	private void Initialisation()
	{
		...
	
		// Liaison entre le Sujet un Observateur
		mySubject.SubjectChangedState += new Subject.SubjectChangedState(this.OnSubjectChanged);

		...
	}
}
info 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 :
// Création du delegate
public delegate void SubjectChangedState_EventHandler(object sender, EventArgs e);

// Définition de l'événement
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 :
// Définition de l'événement
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> :
// Définition de l'événement
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

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

info 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 :
// Création du delegate
public delegate void SubjectChangedState_EventHandler(Subject sender, EventArgs e);

// Définition de l'événement
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>
    /// Notify all subscribers
    /// </summary>
    /// <param name="sender">Object that sends notifications to subscribers ('this' object)</param>
    /// <param name="e">Arguments to be passed to callback methods</param>
    public void Notify(TSender sender, TEventArgs e)
    {
        if (callbacks != null)
            callbacks(sender, e);
    }

    /// <summary>
    /// Add a subscription
    /// </summary>
    /// <param name="c">Method called when Notify() is called</param>
    public void AddSubscription(Callback c)
    {
        callbacks += c;
    }

    /// <summary>
    /// Remove a subscription
    /// </summary>
    /// <param name="c">Method called when Notify() is called</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







Valid XHTML 1.1!Valid CSS!

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.