Le Design Pattern “Observer” : l’implémentation proposée par Microsoft en .NET 4.0

Introduction

Microsoft .NETEn ingénierie logicielle, un design pattern (ou patron de conception) est une solution «abstraite» pour des problèmes communs dans l’architecture logicielle.

Ce n’est pas une architecture utilisable directement dans le code, mais plutôt une simple description (ou Template) qui peut être utilisée dans différentes situations.

Techniquement, les design patterns sont des solutions orientées objets qui montrent des relations et des interactions entre des classes ou objets sans les spécifier concrètement.

Pourquoi ai-je eu l’idée de cet article ?

Je voulais montrer que l’implémentation d’un design pattern pourra être totalement différente de ce qu’on apprend en théorie.

Microsoft .NET a implémenté ce pattern dans sa version 4.0 et donc deux nouvelles interfaces ont vu le jour :

  • System.IObserver<T>
  • System.IObservable<T>

Avant de passer à l’implémentation du pattern en .NET 4.0, je vais revenir rapidement sur sa définition et son utilité.

Le Design Pattern « Observer » : un peu de théorie

Définition :

Le patron de conception “Observateur” est utilisé dans le cas où on a un « Objet A » (subject) qui sera modifié lors de l’exécution d’un programme. A chaque modification il doit notifier d’autres objets (observers) pour leur indiquer ce changement.

Donc ces « observers » n’ont pas besoin de perpétuellement demander l’état de cet « Object A » (l’observable), ils doivent tout simplement s’inscrire à ses notifications.

Diagramme de classe (le template) :

Observer : Diagramme UML

« ConcreteSubject»

C’est l’objet observé. Celui qui va être modifié au cours de l’exécution du programme. Et quand je parle de modification, je veux dire modification de son état, donc de ses attributs.

  • addObserver (…) : permet d’ajouter un objet « observer » à la collection des objets qui veulent être notifiés.
  • removeObserver (…) : permet de supprimer un « observer » depuis cette collection et donc le désinscrire.
  • notifierObservers (…) : c’est la méthode la plus importante de ce pattern. Elle doit être appelée à chaque fois qu’il y a eu une modification sur l’observable. Elle permet donc de passer sur tous les objets qui souhaitent être notifiés et leurs fournir les informations pour lesquelles ils se sont inscrits.

« ConcreteObserverOne » :

Un objet parmi les objets qui se sont inscrits pour être notifié lors du changement du « ConcreteSubject »

  • Update (…) : c’est la méthode qui sera appelée lors de la notification, pour donner la nouvelle information venue du “ConcreteSubject” vers ce “ConcreteObserverOne”

Exemple d’utilisation quotidienne : “Le Système de Push des réseaux sociaux”

Facebook Push

Oui je sais, ce n’est pas exactement le Design Pattern observer qui est utilisé par le système de Push des réseaux sociaux. Mais j’ai pris cet exemple pour vous faire comprendre l’utilité de ce pattern et surtout étudier sur son implémentation dans le Framework .NET 4.0.

On sait tous qu’on n’a pas besoin de rafraîchir notre page web Facebook ou notre application Smartphone pour voir les nouvelles notifications en temps réel. On reçoit automatiquement une notification en rouge qui apparaît nous indiquant qu’il y a une mise à jour dans notre boite de réception.
Je vais me baser sur cet exemple pour étudier l’implémentation du design pattern Observer en .NET 4.0.

Implémentation en .NET 4.0 : Diagramme de classe UML

Design Pattern - Observer Microsoft

System.IObserver, System.IObservable sont les 2 nouvelles interfaces proposées par Microsoft en .NET 4.0 pour aider à implémenter le design pattern Observer

Observable

  • System.IObservable :
    • Subscribe(…) : c’est la méthode équivalente de « addObserver » citée dans le template au début de cet article. La différence, c’est le type de retour de cette méthode. On verra par la suite l’utilité de l’interface IDisposable comme type de retour.
  • FBNotificationCenter : C’est l’observable, l’objet qui va notifier tous ces observateurs inscrits quand une notification arrive, en utilisant la méthode NotifyObservers(…).
  • Le Type générique T : représente le type d’objet qui va contenir les nouvelles informations de la notification. En d’autre termes, c’est le type d’objet qui va servir à la communication entre l’observable et ses observers. Dans notre example ça sera FBNotificationInfo.
  class FBNotificationCenter : IObservable<FBNotificationInfo>
  {
    private List<IObserver<FBNotificationInfo>> _observers = new List<IObserver<FBNotificationInfo>>();
    private List<FBNotificationInfo> _notifications = new List<FBNotificationInfo>();

    public List<FBNotificationInfo> Notifications
    {
      get { return _notifications; }
    }

    public IDisposable Subscribe(IObserver<FBNotificationInfo> observer)
    {
      if (!_observers.Contains(observer))
        _observers.Add(observer);

      return new FBConnection<FBNotificationInfo>(_observers, observer);
    }

    public void AddNotification(string notificationInfo)
    {
        FBNotificationInfo info = new FBNotificationInfo(notificationInfo);
        _notifications.Add(info);
        NotifyObservers(info);
    }

    private void NotifyObservers(FBNotificationInfo info)
    {
      foreach (var observer in _observers)
      {
        if (string.IsNullOrEmpty(info.Info))
        {
          observer.OnError(new ArgumentNullException("info", "Le Paramètre ne peut pas être vide ou null"));
        }
        else observer.OnNext(info);
      }
    }
  }

L’Objet qui va transmettre la notification :

class FBNotificationInfo
{
    private DateTime dateTime;
    private string info;
    private bool viewed;

    public bool Viewed
    {
      get { return viewed; }
      set { viewed = value; }
    }

    public DateTime DateTime
    {
      get { return dateTime; }
    }

    public string Info
    {
      get { return info; }
    }

    public FBNotificationInfo(String info)
    {
      dateTime = DateTime.Now;
      this.info = info;
      viewed = false;
    }
}
Remarque :

Jusqu’ici, il n’y a pas vraiment de différences entre le Template du Design Pattern Observer et l’implémentation .NET, à part le type de retour de la méthode  “Subscribe(…)  : IDisposable”. Et là, on aperçoit la première touche personnelle dans cette implémentation en .NET 4.0.

FBConnection

La classe FBConnection joue le rôle du “Unsubscriber” qui sert à désinscrire un observer pour ne plus recevoir les notifications. L’idée proposée dans le Framework .NET 4.0 donne la possibilité à chaque observer de se désinscrire lui-même, par opposition du Template qui propose une seule façon de se désinscrire : l’utilisation de la méthode removeObserver(…), qui est propre à l’observable seulement.

  class FBConnection : IDisposable
  {
    private List _observers;
    private IObserver _observer;

    public FBConnection(List observers, IObserver observer)
    {
      this._observers = observers;
      this._observer = observer;
    }

    public void Dispose()
    {
      if (_observers.Contains(_observer))
      {
        _observers.Remove(_observer);
      }
    }
  }

On verra par la suite l’utilisation de la méthode Dispose() par l’observer.

Observer

Observer UML

  • System.IObserver :
    • OnNext : c’est la méthode équivalente de « update(…) » décrite dans le Template au début de cet article. La différence est seulement dans la nomenclature. Je ne sais d’ailleurs pas pourquoi Microsoft a choisi ce nom pour cette méthode ? (j’aurais aimé « OnUpdate » à la place)
    • OnError : Une nouvelle méthode ajoutée par rapport au Template,  pour faire communiquer l’erreur du sujet vers ses observateurs.
    • OnCompleted : Cette méthode peut être utilisée pour montrer qu’il n’y a plus de notification à transmettre. L’observateur pourra alors se désinscrire par exemple.
  • WebBrowser, Smartphone : Les observateurs qui seront notifiés par FBNotificationCenter.
  abstract class FacebookView : IObserver
  {
    private IDisposable _connection;
    protected List _myNotifications = new List();

    public void OnCompleted()
    {
      // Je n'ai pas utilisé cette méthode dans cet exemple.
      // Elle est utilisée quand l'Observable envoie un
      // ensemble d'informations en boucle, et après avoir fini,
      // il appellera cette méthode pour informer ses observateurs
      // de la fin d'envoi de notifications.
    }

    public void OnError(Exception error)
    {
      Console.WriteLine(string.Format("{0} > {1}",this.GetType().Name, "Oops !! Erreur : " + error.Message));
    }

    public void OnNext(FBNotificationInfo notification)
    {
      _myNotifications.Add(notification);
      ShowNewNotificationCount();
    }

    public void ConnectTo(FBNotificationCenter provider)
    {
      if (provider != null)
      {
        _connection = provider.Subscribe(this);
        // mettre à jour le nouvel inscrit
        foreach (var notification in provider.Notifications)
          _myNotifications.Add(notification);
      }
    }

    public void Disconnect()
    {
      if (_connection != null)
      {
        _myNotifications.Clear();
        _connection.Dispose();
      }
    }

    private void ShowNewNotificationCount()
    {
      Console.WriteLine(string.Format("{0} Notifié > Vous avez {1} nouvelle(s) notification(s) !", this.GetType().Name, GetNewNotificationCount()));
    }

    private int GetNewNotificationCount()
    {
      int count = 0;
      foreach (var info in _myNotifications)
        if (!info.Viewed) count++;

      return count;
    }

    public abstract void ShowNotifications();
  }

Chaque Observer affichera l’information avec sa propre manière en implémentant la méthode abstraite ShowNotifications()

le WebBrowser :
  class WebBrowser : FacebookView
  {
    public override void ShowNotifications()
    {
      Console.WriteLine(this.GetType().Name + "> Affichage des notifiations :");
      string newNotification = "Ancien";
      foreach (var notification in _myNotifications.Reverse() )
      {
        if (!notification.Viewed)
        {
          newNotification = "Nouveau";
          notification.Viewed = true;
        }
        else newNotification = "Ancien";
        Console.WriteLine(string.Format("\t {0}> {1} {2} {3}", newNotification, notification.DateTime.ToString("dd MMM"), notification.Info, notification.DateTime.ToString("hh:mm:ss")));
      }
    }
  }
et la SmartphoneApplication :
  class SmartphoneApplication : FacebookView
  {
    public override void ShowNotifications()
    {
      Console.WriteLine(this.GetType().Name + "> Showing notifications :");
      string newNotification = "Old";
      foreach (var notification in _myNotifications.Reverse())
      {
        if (!notification.Viewed)
        {
          newNotification = "New";
          notification.Viewed = true;
        }
        else newNotification = "Old";
        Console.WriteLine();
        Console.WriteLine("\t" + notification.DateTime.ToString("dd MMM"));
        Console.WriteLine("\t" + notification.DateTime.ToString("hh:mm:ss"));
        Console.WriteLine("\t" + string.Format("({0}) {1}", newNotification, notification.Info));
        Console.WriteLine();
      }
    }
  }
Le main :

 class Program
  {
    static void Main(string[] args)
    {
      FBNotificationCenter notificationCenter = new FBNotificationCenter();
      WebBrowser browser = new WebBrowser();
      SmartphoneApplication app = new SmartphoneApplication();
      browser.ConnectTo(notificationCenter);
      notificationCenter.AddNotification("Messi aime votre photo");//1
      notificationCenter.AddNotification("Xavi a commenté votre publication");//2
      app.ConnectTo(notificationCenter);
      browser.ShowNotifications();//3
      notificationCenter.AddNotification("Puyol vous invite à aimer sa page");//4
      browser.ShowNotifications();//5
      notificationCenter.AddNotification("Ronaldinho a accepté votre invitation");//6
      app.ShowNotifications();//7
      app.Disconnect();
      notificationCenter.AddNotification("");//8
      browser.ShowNotifications();//9
      Console.ReadKey();
    }
  }

Le Résultat :

Result_2

Conclusion

Le but de cet article était d’une part de comprendre le design pattern observer, et d’autre part de montrer que l’implémentation concrète de n’importe quel design pattern est souvent différente du template (théorie). Et c’est là qu’apparaît la difficulté d’utilisation et du choix d’un design pattern pour un problème. Le facteur expérience a une grande importance pour prendre la bonne solution au bon problème d’architecture.

Nombre de vue : 1236

COMMENTAIRES 2 commentaires

  1. Je suis surpris, je ne m’attendais pas à trouver des interfaces pour ce pattern en dotNet. Dans 99% des cas, le couple Event – Delegate répond de façon bien plus élégante et concise aux problématiques de notification.

    J’aurai aimé en savoir un peu plus sur les cas où l’usage de ces nouvelles interfaces amènent un plus. Cet article m’a permis de me poser la question et de trouver des éléments de réponse, donc merci.

    Complément MSDN sur l’existance des interfaces IObservable/IObserver et leur rôle :
    http://msdn.microsoft.com/en-us/library/hh242974(v=vs.103).aspx

  2. Nicolas Kyriazopoulos-Panagiotopoulos dit :

    IObserver IObservable était ajouter pour rendre possible Linq To Events (Rx, Reactive Extensions) – ce qui va au délà des possibilités des events classiques.

    http://blog.soat.fr/2013/04/le-design-pattern-observer-limplementation-proposee-par-microsoft-en-net-4-0/

AJOUTER UN COMMENTAIRE