mardi 11 décembre 2012

Décoration de services AngularJS

C'est bientôt Noël, mais il ne s'agit pas d'accrocher des guirlandes aux services d'une application AngularJS. Cherchez plutôt du côté du design pattern Decorator.

Pour compléter l'article précédent sur les différentes façons de créer un service, il existe aussi une technique dans AngularJS pour modifier un service existant lors de son instanciation, ou pour substituer à l'instance du service un objet différent mais similaire.

Cette fois ce n'est pas via une méthode de l'objet module. La méthode decorator() à utiliser se trouve dans le service standard $provide, qu'il faut bien sûr injecter. On peut appeler cette méthode dans la méthode config du module principal de l'application, ou d'un autre module.



Elle ne crée pas un nouveau service, elle permet de modifier ou remplacer un service déjà référencé. Il faut donc pour pouvoir appeler cette méthode $provide.decorator(name, decorator), avoir créé avant un service qui a précisément ce nom-là. Plus précisément, AngularJS va modifier la méthode $get du provider enregistré sous le nom passé comme premier paramètre suffixé par 'Provider', comme on le voit dans le code du framework :

function decorator(serviceName, decorFn) {
    var origProvider = providerInjector.get(serviceName + providerSuffix),
        orig$get = origProvider.$get;

    origProvider.$get = function() {
        var origInstance = instanceInjector.invoke(orig$get, origProvider);
        return instanceInjector.invoke(decorFn, null, {$delegate: origInstance});
    };
}

Ca veut dire que decorator() ne peut être appelé que si un service de ce nom là a déjà été créé dans un module par l'une de ses méthodes provider()factory()service() ou value(). Mais pas par la méthode constant() ! Si vous avez lu attentivement le post précédent, vous aurez retenu que la méthode constant() est la seule qui ne crée pas un vrai provider, car il n'a pas de méthode $get. La conséquence est que les services créés avec la méthode constant() ne peuvent pas être décorés.

Lors du premier accès au service, la fonction passée à decorator() se voit injecter comme un paramètre $delegate l'instance du service d'origine fraîchement créé par son provider d'origine. Elle peut soit la modifier, soit créer une nouvelle instance qui s'appuie sur l'ancienne. Et elle renvoie l'objet qui sera réellement publié comme instance du service, qu'il s'agisse de la  même instance modifiée ou d'un nouvel objet.

Bien, mais à quoi ça sert ? Car on pourrait directement créer le service correspondant à ce qu'on veut... sauf si on n'a pas la main sur le service à modifier. Ça peut être un service standard du framework AngularJS, ou encore un service utilisé dans d'autres applications et dont on ne veut pas modifier le code source. L'usage est celui du design pattern Decorator, on va pouvoir dans la configuration d'une application faire un tour de passe-passe consistant à modifier le comportement d'un service sans que les autres services qui l'utilisent ne s'en rendent compte, et sans aucune modification du code source du service d'origine.

Voici un exemple de code - parfaitement inutile mais c'est juste pour montrer la syntaxe - où le service standard $log est remplacé par un log décoré (jsFiddle ici) :

var app = angular.module('app', []);
app.config(['$provide', function($provide) {
    $provide.decorator('$log', ['$delegate', function ($delegate) {
        return {
            error: function (text) { $delegate.error("***" + text + "***"); },
            info: function (text) { $delegate.info("***" + text + "***"); },
            log: function (text) { $delegate.info("***" + text + "***"); },
            warn: function (text) { $delegate.info("***" + text + "***"); },
        };
    }]);
}]);

function Ctrl($scope, $log) {
    $log.info('ok');
}


3 commentaires:

  1. Bonjour,
    J'avais parcouru votre article il y a quelques jours et avait été bluffé sur le coup (cet article et celui sur les promise sont des pépites !).

    Juste pour vous dire que je me suis servi de votre technique du decorator pour reprendre un code fourni par Vojta pour le chargement de tous les template d'une application en un seul fichier :
    https://gist.github.com/vojtajina/3354046

    Ca donne un exemple réel un poil plus pertinent que la décoration de log et montre ainsi la toute puissance de la décoration (non on ne mettra pas de stickers sur les murs, tu sors Valérie !)

    RépondreSupprimer
  2. Merci Romain, c'est un exemple bien plus utile que le mien.

    D'ailleurs comme tu l'as fait, ce n'est pas le pattern Decorator classique, puisque tu ne crées pas un nouveau service mais tu remplaces la méthode get du service existant. Ca peut être plus pratique, puisqu'il n'y a même pas besoin d'écrire les délégations pour les autres méthodes.

    RépondreSupprimer
  3. Bonjour Thierry,
    Pour faire suite à mon poste de début d'année, j'ai continué à réfléchir sur le GIST de Vojta. Vous y trouverez notamment le decorator évoqué précédemment, cette fois-ci encapsulé dans son module pour faciliter sont utilisation.
    Je pense que cela peut vous intéresser, n'hésitez pas à me faire toute remarque.

    RépondreSupprimer