Codingly

Quand les closures nous facilitent la vie

Posted in Posts by Romain Verdier on juin 3, 2008

Aujourd’hui j’ai une devinette pour vous. Elle est très simple, mais je suis obligé de l’introduire par une mise en situation un peu pénible. Soyez courageux, il n’y a rien à gagner.

Imaginez une application capable de recevoir des messages du réseau. Lorsqu’un message de type A est reçu, l’application doit afficher à l’utilisateur une fenêtre assez complexe construite à partir des données du message. L’utilisateur peut ensuite interagir avec la fenêtre en question, avant de la fermer.

Les contraintes :

  • Les fenêtres doivent être affichées très rapidement, l’utilisateur devant être averti le plus tôt possible de l’arrivée d’un message de type A.
  • Les fenêtres ne sont pas modales, il peut donc y en avoir plusieurs affichées simultanément.
  • La construction des fenêtres (création des différents contrôles graphiques) est relativement longue.
  • Les opérations de binding entre les données du message et les contrôles existants d’une fenêtre sont rapides.

Mais comment faire ?

Ce n’est pas la question, du moins pas la devinette du jour. On va utiliser une (pseudo) pool de fenêtres, pour n’avoir qu’à les afficher le moment venu, et ne pas subir le coût de la construction des contrôles systématiquement. Le principe est simple :

  • Au démarrage de l’application on crée un certain nombre de fenêtres, sans les afficher.
  • On conserve ces instances dans une pool.
  • Lorsqu’on reçoit un message de type A, on utilise une fenêtre de la pool. On la retire temporairement de cette dernière.
  • Lorsque l’utilisateur ferme la fenêtre – elle est cachée et non pas détruite – on la replace dans la pool afin qu’elle puisse être réutilisée.

En suivant le pattern MVP, on peut définir une interface pour nos vues :

public interface IView
{
    // ...

    Presenter Presenter { get; set; }

    void Show();
}

Ainsi qu’un presenter :

public class Presenter
{
    // ...
    
    private readonly IView view;

    public event EventHandler Stopped;

    public Presenter(IView view)
    {
        this.view = view;
    }

    public void Start()
    {
        if (this.view != null)
        {
            // ...
            view.Show();
        }
    }

    public void Stop()
    {
        // ...
        if (this.Stopped != null)
        {
            this.Stopped(this, EventArgs.Empty);
        }
    }
}

La vue connait son presenter, et le presenter connait sa vue. Ce genre de référence circulaire est généralement résolu par un conteneur IoC. Le presenter est injecté dans la vue via une propriété, la vue est injectée au presenter via le constructeur.

Remarquez bien l’évenement Stopped du presenter.

Je vous propose également, à titre informatif, une implémentation (très incomplète) de IView :

public class View : Form, IView
{
    // ...

    private Presenter presenter;

    public Presenter Presenter
    {
        get { return this.presenter; }
        set { this.presenter = value; }
    }

    public View()
    {
        // ...
        this.Closing += View_Closing;
    }

    private void View_Closing(object sender, CancelEventArgs e)
    {
        if (this.presenter != null)
        {
            this.presenter.Stop();
        }
        e.Cancel = true;
        Hide();
    }

    void IView.Show()
    {
        Show();
    }
}

C’est cette fenêtre dont la construction est théoriquement coûteuse qui s’affichera. Notez bien qu’ici je cherche simplement à mettre en avant le fait qu’à la fermeture de la fenêtre, on empêche la destruction automatique de cette dernière et on se contente de la masquer. On notifie également le presenter à ce moment.

Revenons à notre pool de fenêtres, et imaginons l’implémentation suivante :

public class ViewPool
{
    private readonly List<IView> viewPool = new List<IView>();

    public ViewPool(int capacity)
    {
        for (int i = 0; i < capacity; i++)
        {
            this.viewPool.Add(new View());
        }
    }

    public IView GetView()
    {
        IView view;
        lock (this.viewPool)
        {
            if (this.viewPool.Count == 0)
            {
                view = new View();
            }
            else
            {
                view = this.viewPool&#91;0&#93;;
                this.viewPool.RemoveAt(0);
            }
        }
        return view;
    }

    public void ReleaseView(IView view)
    {
        lock(this.viewPool)
        {
            this.viewPool.Add(view);
        }
    }
}
&#91;/code&#93;

On spécifie la capacité initiale de la pool à la création. Ensuite, on récupère une vue de la pool via la méthode <code>GetView</code>, et on la libère en utilisant la méthode <code>ReleaseView</code>.

La vue est instanciée directement par la pool lorsque nécessaire : c'est mal. Mais puisque ce qui compte aujourd'hui, c'est la devinette, essayons de faire comme si de rien n'était. Evidemment, n'utilisez pas ce code dans un vrai projet.

<h3>La devinette, la devinette, la devinette !</h3>

Je doute que vous soyez si enthousiastes, mais sait-on jamais... Bref, voici la seule brique manquante : le code qui sera appelé lors de la réception d'un message de type A. Dans l'exemple suivant, nous considèrerons donc que la méthode <code>OnNewMessageAReceived</code> est appelée à chaque fois qu'un message arrive :


public class SomewhereInMyApplication
{
    // ...

    private readonly ViewPool pool = new ViewPool(10);

    public void OnNewMessageAReceived(Message message)
    {
        IView view = this.pool.GetView();
        Presenter presenter = new Presenter(view);
        view.Presenter = presenter;
        // ...
        presenter.Start();
    }
}

Entre deux ricanements, vous pourriez me faire remarquer que le message ne sert à rien, et que c'est idiot. Vous auriez presque raison. N'oubliez pas que l'application n'existe pas d'une part, et que d'autre part, je fais ce que je veux. C'est ma devinette.

Je la partage juste magnanimement :

Comment utiliser l'évenement Stopped du presenter pour remettre la vue utilisée dans la pool (via la méthode ReleaseView) ?

Oui, ce n'est pas vraiment une devinette - la solution est assez évidente - mais je suis tout de même intéressé par vos propositions et remarques...

Tagged with: , ,

3 Réponses

Subscribe to comments with RSS.

  1. Romain Verdier said, on juin 12, 2008 at 2:23

    Comme vous insistez lourdement pour connaitre la réponse, je cède :

    public class SomewhereInMyApplication
    {
        // ...
    
        private readonly ViewPool pool = new ViewPool(10);
    
        public void OnNewMessageAReceived(Message message)
        {
            IView view = this.pool.GetView();
            Presenter presenter = new Presenter(view);
            view.Presenter = presenter;
            presenter.Stopped += delegate { this.pool.ReleaseView(view); };
            presenter.Start();
        }
    }
    
  2. grozeille said, on juin 13, 2008 at 4:56

    ??
    le « truc » c’était ça ?:
    Stopped += delegate { this.pool.ReleaseView(view); };

    J’ai rencontré le coup du « delegate {} » avec RhinoMock. En effet, quand on « s’attend à un appelle de la méthode MaClass.MaMethode() » ça s’écrit:
    Expect.Call(MaClasse.MaMethode());

    Mais si « MaMethode() » retourne un void? La méthode « Call » ne peut pas prendre un « void » et le code ne compile pas. L’astuce est donc de passer par ce « delegate »:
    Expect.Call(delegate{ MaClasse.MaMethode(); });

  3. Romain Verdier said, on juin 13, 2008 at 6:41

    Le truc à noter ici c’est surtout qu’en utilisant une méthode anonyme (on aurait pu aussi utiliser une expression lambda) il devient possible de faire appel à une méthode de this.pool, qui aurait été plus difficilement accessible si on s’était abonné différemment à l’évènement Stopped

    Dans Rhino Mocks, pour définir une expectation sur une méthode qui ne retourne rien, tu peux faire plus simple :

    // Appel direct de la méthode
    myObject.MyMethod();
    // Définition des contraintes éventuelles en utilisant la classe statique LastCall
    LastCall.Repeat.Twice();
    

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

%d blogueurs aiment cette page :