Modifier le comportement d’une application modulaire grâce à des évènements

Par certains aspects, le développement JavaScript et ActionScript (Flex) ont certains points communs. Vous pouvez par exemple vous inscrire à certains évènements sur un composant comme « blur » sur un composant « input ». De la même manière, vous pouvez vous inscrire sur l’évènement « focusOut » sur un composant TextInput en Flex.

Les éléments du DOM envoient donc des évènements que vous pouvez utiliser dans votre application. Admettons que vous utilisiez jQuery, vous aurez une instruction bind du genre:

$('#foo').bind('click', function() {
  alert('User clicked on "foo."');
});

Tout ça c’est bien pratique mais si vous avez suivi mon article sur l’approche « composant » lors du  développement d’application web, vous avez compris que ce qui est encore plus intéressant est de pouvoir propager ses propres évènements. Vous ne pouvez pas vous restreindre aux évènements du DOM, vous devez propager vos propres évènements « métier ».

Pour cela, jQuery a aussi une méthode nommée trigger:

http://api.jquery.com/trigger/

Voici donc le même exemple mais cette fois-ci, on déclenche un évènement « click » à la main :

$('#foo').bind('click', function() {
      alert($(this).text());
    });
    $('#foo').trigger('click');

La méthode trigger prend en premier paramètre, le type de l’évènement. Vous n’êtes donc pas limité aux types d’évènement du DOM. Voyons maintenant quelles fonctionnalités nous apporte la classe EventDispatcher en ActionScript

EventDispatcher:  la version ActionScript 3

La classe EventDispatcher est sûrement une des classes les plus utilisées en ActionScript. Le plus souvent, on ne le voit pas car le tout se fait par héritage. Regardez donc la documentation qui donne toutes les classes héritant de EventDispatcher:

http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/events/EventDispatcher.html

Rajoutez en plus tous les objets graphiques car ils héritent de DisplayObject / UIComponent et vous verrez que c’est une classe centrale. Grâce à un objet de type EventDispatcher, vous pouvez:

  • Propager des évènements avec dispatchEvent
  • Ajouter un écouteur d’évènement avec addEventListener
  • Supprimer un écouteur d’évènement avec removeEventListener
  • Savoir si un type d’évènement à un écouteur avec hasEventListener

Stopper la propagation d’un évènement pour un code plus évolutif / modulaire

Dans un des derniers billets, je parlais aussi du chargement de module dans une application. En chargeant du code de manière dynamique, on peut ajouter des fonctionnalités mais l’on peut aussi modifier le comportement de l’application. Le « challenge » est que vous devez modifier le comportement de l’application sans que les composants impactés aient connaissance de ce que le module va faire.

La solution est de laisser des points d’entrée, alias « hooks ».

Il y a plusieurs manières de laisser des « hooks » dans le code, plus ou moins bonnes.

Classes « dynamic » qui peuvent être redéfinies à la volée

Au temps de Flex 2 chez Business Geografic, on avait commencé par rendre certaines classes dynamic (par le mot clé public dynamic class … que vous ne connaissez peut-être pas, mais c’est sûrement mieux comme ça!). De base, l’ActionScript est un langage dynamique (prototype, basé sur les specs ECMA) et si vous créez un « Object », vous pouvez lui ajouter dynamiquement n’importe quel propriété, de n’importe quel type. Viens ensuite le système de classes. Ces objets ne sont plus dynamiques et les méthodes / propriétés accessibles sont définies au niveau de la classe et vous ne pouvez plus en ajoutez / supprimer par la suite (sauf si vous utilisez le mot-clé « dynamic »).

En JavaScript, vous pouvez redéfinir le prototype de n’importe quel objet et donc ajouter des propriétés / méthodes à la volée. Il n’y a pas ce système de classes qui « scelle » à tout jamais les propriétés d’un objet en particulier.

Admettons que vous ayez de base une classe A qui a une méthode « getClients() » dans votre application. Si vous souhaitez modifier le comportement de cette méthode lorsque vous chargez un module, vous pourriez être tenté de remplacer la méthode getClients() de A par une méthode contenue dans votre module.

C’est une mauvaise décision pour plusieurs raisons:

  • Vous allez créer des liaisons fortes dans votre code entre le module et l’application qui l’accueille
  • Vous venez de supprimer complètement le traitement qui était fait dans la méthode getClients() originale. Vous n’avez pas le choix de faire autrement.
  • Si vous voulez faire un traitement avant ou après l’appel à getClients dans votre module et garder le traitement original, vous pourrez peut-être en faisant une feinte moche (en transformant getClients en une méthode proxy en gros).
  • Si vous chargez une 2ème module qui souhaite lui aussi modifier getClients, vous allez supprimer la modification de votre premier module.

Bref, ce n’est pas propre et vous allez vous en mordre les doigts plus tard lorsque vous devrez maintenir vos modules, voire lancer des tests.

Rendre son code modulaire grâce aux évènements

La solution qui s’est révélée être la plus viable pour le développement et la maintenance (par expérience) consiste à utiliser un modèle évènementiel. Ce sont en fait les évènements propagés par chaque composants qui seront vos points d’entrées pour vos modules.

Prenons un exemple simple. Je vais expliquer l’exemple tel qu’on le fait en AS3, pour que vous puissiez voir où cela coince en JavaScript.

Vous avez une application et lors d’une action utilisateur, vous allez ouvrir une fiche d’information sur un client, représenté par un objet Client. On va assumer que dans votre application, vous avez un objet qui s’en occupe, que l’on va appeler « FicheInfoManager ». Sur ce FicheInfoManager, vous avez une méthode qui a la signature suivante:

package {
  import flash.events.EventDispatcher;

  public class FicheInfoManager extends EventDispatcher {
    public function FicheInfoManager() {
      super();
    }

    public function showInfo(client:Client):void {
      // open
    }
  }
}

Pour ouvrir une fiche d’information, vous allez récupérer une instance de FicheInfoManager et appeler showInfo(client), très bien.

Vous avez maintenant un module qui charge une classe « SpecialClientModule », qui va devoir, pour un client particulier, ouvrir une fiche d’information différente de la fiche d’information de base. Si vous re-définissez la méthode showInfo à la volée, vous devrez prendre en charge l’affichage de la fiche d’information par défaut, vous aurez donc une duplication du code. Bad.

On va introduire notre « hook » où l’on en a besoin, c’est à dire lors de la demande de fiche d’information. Au lieu d’appeler directement la méthode « showInfo », on va propager un évènement de type « onShowInfo » sur la classe FicheInfoManager. Voici la définition de l’évènement:

package bg.event {
  import flash.events.Event;

  public class FicheInfoEvent extends Event {

    public static var ON_SHOW_INFO:String = "onShowInfo";
    public var client:Client;

    public function FicheInfoEvent(type:String, client:Client) {
      super(type);
      this.client = client;
    }
  }
}

Notez que pour pouvoir propager cet évènement, FicheInfoManager doit hériter de la classe EventDispatcher. L’instance de FicheInfoManager va s’inscrire à cet évènement et exécuter le comportement par défaut, c’est-à-dire exécuter la méthode showInfo:

package {
  import bg.event.FicheInfoEvent;

  import flash.events.Event;
  import flash.events.EventDispatcher;

  public class FicheInfoManager extends EventDispatcher {
    public function FicheInfoManager() {
      super();
      addEventListener(FicheInfoEvent.ON_SHOW_INFO, onShowInfoHandler);
    }

    protected function onShowInfoHandler(event:FicheInfoEvent):void {
      var client:Client = event.client;
      showInfo(client);
    }

    public function showInfo(client:Client):void {
      // open
    }
  }
}

Pour l’instant, vous avez exactement le même comportement qu’avant sauf que pour demander une fiche d’information, vous n’allez pas appeler directement showInfo mais propager un évènement. Vous utilisez votre point d’entrée comme ceci:

// au lieu de
ficheInfoManager.showClient(client);
// on a
ficheInfoManager.dispatchEvent(new FicheInfoEvent(FicheInfoEvent.ON_SHOW_INFO, client));

Maintenant, lors du chargement de « SpecialClientModule », vous allez vous inscrire à l’évènement « onShowInfo » de FicheInfoManager, mais avec une priorité plus haute (4 dans notre exemple, supérieur à 0 la valeur par défaut de addEventListener), ce qui est possible en ActionScript:

package {
  import bg.event.FicheInfoEvent;

  import flash.events.Event;

  public class SpecialClientModule {
    public function SpecialClientModule() {
      // récupérer une instance de FicheInfoManager sur votre application
      var ficheInfoManager:FicheInfoManager = ...;
      ficheInfoManager.addEventListener(FicheInfoEvent.ON_SHOW_INFO, onShowSpecialInfo, false, 4);
    }

    protected function onShowSpecialInfo(event:FicheInfoEvent):void {
      var client:Client = event.client;
    }
  }
}

L’écouteur d’évènement de SpecialClientModule sera donc appelé avant celui de FicheInfoManager. A ce moment-là, SpecialClientModule a le pouvoir et peut faire plusieurs choses.

Remplacer le comportement initial de manière arbitraire

Le comportement par défaut de FicheInfoManager ne sera effectué que s’il reçoit l’évènement onShowInfo. Grâce aux méthode stopPropagation() et stopImmediatePropagation(), SpecialClientModule peut stopper la propagation de l’évènement, qui ne va donc jamais arriver à FicheInfoManager:

protected function onShowSpecialInfo(event:FicheInfoEvent):void {
  var client:Client = event.client;
  // lalala, you won't get it anymore
  event.stopImmediatePropagation();
  event.stopPropagation();
}

Remplacer le comportement initial selon une certaine règle

Dans notre exemple initial, SpecialClientModule ne devait supprimer le traitement par défait que pour un certain client. Avant  de stopper la propagation, on peut donc faire une vérification:

protected function onShowSpecialInfo(event:FicheInfoEvent):void {
  var client:Client = event.client;
  if (client.name == "Fabien Nicollet"){
	event.stopImmediatePropagation();
	event.stopPropagation();
	// show something else instead
  }
}

Ici, on a donc introduit un traitement spécial, dans un cas particulier, sans jamais impacter le traitement original de l’application. Cool, non ?

Effectuer un traitement avant l’appel par défaut

Admettons que votre module doive logger toutes les ouvertures de fiches d’information, ce qui n’est pas fait par défaut par votre application (fonctionnalité Analytics payante par exemple). L’exemple est un peu nul mais vous avez compris. Il vous suffit de faire:

protected function onShowSpecialInfo(event:FicheInfoEvent):void {
  doSomeLogging(client);
  // let it be
}

Le problème de cette approche en JavaScript

Grâce à cette technique, vous avez introduit du code autour du comportement par défaut, sans jamais le modifier. Vous aurez peut-être aussi remarqué que cela fonctionne avec un module, mais aussi avec plusieurs modules. Tout est en fait une question de priorité, ce qui va décider qui sera appelé et dans quel ordre. L’ordre d’appel est important car vous pouvez couper la propagation d’un évènement.

Voilà donc comment cela fonctionne en ActionScript 3 / Flex. Voulant effectuer le même genre de manipulation en JavaScript, j’ai rencontré pas mal de problèmes:

Pas d’objet « EventDispatcher » en JavaScript

Il n’existe pas vraiment de classe spécialisée pour la gestion d’évènement comme EventDispatcher. Une solution intéressante est que vous pouvez  créer un « faux » objet jQuery qui va vous servir à faire transiter les évènements, car c’est dans les cordes de n’importe quel objet jQuery.

Cette méthode est expliquée dans cet article:

http://dailyjs.com/2009/11/14/jquery-custom-events/

Au delà de l’aspect « bricolage » de cette technique, on rencontre d’autres problèmes.

Pas de concept de priorité sur les évènements en JavaScript

Le concept de priorité n’existe pas sur les évènements en JavaScript. Les écouteurs sont appelés suivant leur ordre d’ajout. C’est moche, vous ne voulez pas qu’un module soit plus prioritaire car il a été chargé après un autre.

Les méthodes preventDefault() et stopPropagation() ne sont utiles que pour les évènements du DOM

Il existe bien des méthodes preventDefault(), stopPropagation() et stopImmediatePropagation() sur les évènements en JavaScript mais ceux-ci ne sont utiles que pour les évènements qui proviennent du DOM, comme pour stopper un « form » au moment du « submit ». Pour les évènements que l’on va créer soi-même, comme il n’y a pas de concept de priorité, ces méthodes ne peuvent être utilisées.

Les mécanismes de « pub / sub » ont les mêmes problèmes

Il y a pas mal de librairies pour faire du publisher / subscriber en JavaScript. Vous pouvez vous inscrire sur un « topic » et écouter ce qu’il s’y passe. Pratique pour faire du « broadcast » mais dans notre cas, l’EventDispatcher doit pouvoir rester local et pas global à l’application.

Solution : Créer son propre EventDispatcher JavaScript

On voit bien que le coeur du problème est cette classe EventDispatcher inexistante en JavaScript. L’alternative qui consiste à utiliser un objet jQuery n’est pas suffisante car il nous manque des fonctionnalités.

Il ne nous reste plus qu’à créer notre propre EventDispatcher!

Comme j’en avais besoin, j’ai écrit cet EventDispatcher JavaScript. Et oui, cet article était en fait une introduction! :)

2 réflexions au sujet de « Modifier le comportement d’une application modulaire grâce à des évènements »

  1. lionel

    Coucou,

    Pourquoi ne pas partir sur une approche type signal ?
    Sympa l’illustration des hooks… Ca illustre très bien l’utilisation de stopPropagation

    en complément, un article sur les problématique d’application à base de module (cycle de vie, communication, etc) http://addyosmani.com/largescalejavascript/ je recommande la lecture du blog de l’auteur car i ltraite aussi beaucoup de ces thèmes.

    Lionel

    Répondre
  2. Fabien

    Merci pour le lien Lionel, vraiment interessant cet article. On y retrouve toutes les problematiques du Flex egalement. Comme quoi le dev en entreprise, peu importe la technologie, les problematiques sont souvent les memes…

    Répondre

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *