AngularJS-Shield-large
AngularJS

AngularJS expliqué en patrons de conception, épisode 2 : Les Services

Dans ce second épisode, nous allons voir comment les patrons de conception traditionnels sont utilisés dans les services d’AngularJS.

Cet article fait partie d’une série d’articles consacrés à l’utilisation des patrons de conceptions par et dans les composants d’AngularJS.

Le patron Singleton

Le patron Singleton vise à assurer qu’il n’y a toujours qu’une seule instance d’un objet en fournissant une interface pour la manipuler. C’est un des patrons les plus simples. L’objet qui ne doit exister qu’en une seule instance comporte une méthode pour obtenir cette unique instance et un mécanisme pour empêcher la création d’autres instances.

Ce patron est illustré dans le diagramme UML ci-dessous :

Singleton

Lorsqu’une dépendance doit être injectée par AngularJS dans un composant, voici l’algorithme utilisé par le framework :

  • Prendre le nom de la dépendance et le rechercher dans une hash map, qui est définit au sein de sa portée lexical (ainsi elle reste privée).
  • Si la dépendance existe, AngularJS la passe en tant que paramètre au composant qui l’a demandé.
  • Si la dépendance n’existe pas :
    • AngularJS créé une nouvelle instance de cette dépendance en invoquant la factory method de son provider: la méthode $get. A noter qu’au moment de l’instanciation, cette dépendance peut éventuellement déclencher un appel récursive de cet algorithme, afin de résoudre toutes les dépendances requises par cette dépendance. Ce qui peut conduire à un souci de dépendances circulaires.
    • AngularJS met en cache cette instance, dans la hash map mentionnée précédemment.
    • AngularJS transmet cette instance en tant que paramètre au composant qui a demandé cette dépendance.

Voici un aperçu du code source d’AngularJS, de la méthode getService :

function getService(serviceName) {
  if (cache.hasOwnProperty(serviceName)) {
    if (cache[serviceName] === INSTANTIATING) {
      throw $injectorMinErr('cdep', 'Circular dependency found: {0}', path.join(' <- '));
    }
    return cache[serviceName];
  } else {
    try {
      path.unshift(serviceName);
      cache[serviceName] = INSTANTIATING;
      return cache[serviceName] = factory(serviceName);
    } catch (err) {
      if (cache[serviceName] === INSTANTIATING) {
        delete cache[serviceName];
      }
      throw err;
    } finally {
      path.shift();
    }
  }
}

Nous pouvons dire que chaque service est un singleton car chaque service est instancié qu’une seule fois. Nous pouvons également considérer le cache comme un manageur de singletons. Il existe une légère variation du diagramme UML illustré ci-dessus parce qu’au lieu de garder une référence statique, au sein de son constructeur, nous conservons cette référence au sein du manager de singleton (illustré dans le bout de code ci-dessus en tant que cache).

De cette manière, les services sont réellement des singletons mais ne sont pas implémentés à travers le patron Singleton, ce qui offre quelques avantages par rapport à l’implémentation classique :

  • améliore la testabilité de votre code source
  • vous pouvez contrôler la création des objets singletons (dans notre cas, le conteneur IoC (Inversion de Contrôle) le contrôle pour nous, en instanciant le singleton).

Si vous voulez en savoir plus, je vous invite à lire l’article de Misko Hevery.

Factory Method

Le patron Factory Method fournit une interface pour créer un objet qui laisse la possibilité aux sous-classes de décider quel type d’objet créer. Ce patron est utilisé lorsque la classe d’un objet n’est pas connue au moment de la compilation. Une méthode pour créer un objet factory method est définie dans une classe abstraite, et implémentée dans les différentes sous-classes. La factory method peut également comporter une implémentation par défaut.

Factory Method

Considérons le code suivant :

myModule.config(function ($provide) {
  $provide.provider('foo', function () {
    var baz = 42;
    return {
      //Factory method
      $get: function (bar) {
        var baz = bar.baz();
        return {
          baz: baz
        };
      }
    };
  });
});

Dans le code ci-dessus, nous utilisons la callback config dans le but de définir un nouveau “provider” ou un “fournisseur”. Un “provider” est un objet qui possède une méthode $get. Puisque JavaScript n’offre pas d’interface, la convention veut que l’on nomme cette méthode de telle sorte.

Chaque service, filtre ou contrôleur possède un “provider” qui est responsable de créer une instance de ce composant.

Jetons un oeil sur l’implémentation d’AngularJS :

//...

createInternalInjector(instanceCache, function(servicename) {
  var provider = providerInjector.get(servicename + providerSuffix);
  return instanceInjector.invoke(provider.$get, provider, undefined, servicename);
}, strictDi));

//...

function invoke(fn, self, locals, serviceName){
  if (typeof locals === 'string') {
    serviceName = locals;
    locals = null;
  }

  var args = [],
      $inject = annotate(fn, strictDi, serviceName),
      length, i,
      key;

  for(i = 0, length = $inject.length; i < length; i++) {
    key = $inject[i];
    if (typeof key !== 'string') {
      throw $injectorMinErr('itkn',
              'Incorrect injection token! Expected service name as string, got {0}', key);
    }
    args.push(
      locals && locals.hasOwnProperty(key)
      ? locals[key]
      : getService(key)
    );
  }
  if (!fn.$inject) {
    // this means that we must be an array.
    fn = fn[length];
  }

  return fn.apply(self, args);
}

D’après ce code, nous remarquons que la méthode $get est utilisé à ce niveau :

instanceInjector.invoke(provider.$get, provider, undefined, servicename)

Dans ce bout de code, la méthode invoke de l’objet instanceInjector est invoquée avec la “factory method” $get en premier paramètre. Dans le corps de la fonction invoke la fonction annotate est appelée avec en premier paramètre la “factory method”. Cette fonction annotate permet de résoudre toutes les dépendances à travers le mécanisme d’injection de dépendance d’AngularJS, vu précédemment. Lorsque toutes les dépendances sont résolues, la “factory method” est invoquée : fn.apply(self, args).

Si nous faisons le lien avec le diagramme UML précédent (en figure 2), nous pouvons associer le “provider” à “ConcreteCreator” et le composant créé, un “Product”.

Utiliser le patron Factory Method offre des avantages dans notre cas grâce à l’indirection qu’il introduit. Cela permet au framework de d’avoir le contrôle de la création des nouveaux composants, comme par exemple :

  • Le moment approprié d’instancier un composant.
  • La résolution de toutes les dépendances requises par un composant.
  • Le nombre d’instance autorisé par composant : une seule pour les filtres et les services, et plusieurs instances pour les contrôleurs.

Decorator

Ce patron permet d’attacher dynamiquement des responsabilités à un objet. Une alternative à l’héritage. Ce patron est inspiré des poupées russes. Un objet peut être caché à l’intérieur d’un autre objet décorateur qui lui rajoutera des fonctionnalités, l’ensemble peut être décoré avec un autre objet qui lui ajoute des fonctionnalités et ainsi de suite. Cette technique nécessite que l’objet décoré et ses décorateurs implémentent la même interface, qui est typiquement définie par une classe abstraite.

Decorator

AngularJS fournit de base un moyen d’étendre et d’enrichir les fonctionnalités des services existants. En utilisant la méthode decorator de l’objet $provider, il est possible de créer des “wrapper” de n’importe quel service définit ou fournit par un module tiers :

myModule.controller('MainCtrl', function (foo) {
  foo.bar();
});

myModule.factory('foo', function () {
  return {
    bar: function () {
      console.log('I\'m bar');
    },
    baz: function () {
      console.log('I\'m baz');
    }
  };
});

myModule.config(function ($provide) {
  $provide.decorator('foo', function ($delegate) {
    var barBackup = $delegate.bar;
    $delegate.bar = function () {
      console.log('Decorated');
      barBackup.apply($delegate, arguments);
    };
    return $delegate;
  });
});

L’exemple ci-dessus définit un nouveau service appelé foo. Dans la phase de configuration, la méthode $provider.decorator est invoquée avec l’argument “foo”, qui est le nom du service que nous souhaitons décorer, ainsi qu’un second paramètre qui est l’implémentation de la décoration souhaitée. $delegate garde une référence du service original foo. En utilisant l’injection de dépendances d’AngularJS, nous récupérant cette référence dans le décorateur. Nous décorons ensuite le service en surchargeant sa méthode bar, en invoquant une instruction supplémentaire : console.log(‘Decorated’);. Puis nous donnons la main au service original.

Ce patron est utile si nous souhaitons modifier le comportement des services tiers. Dans certains cas où plusieurs décorations “similaires” sont nécessaires (mesure de performance, gestion des droits, gestion des logs, etc.), nous risquons d’introduire beaucoup de duplication de code et violer ainsi le principe DRY. Dans ces cas précis, il est recommandé de se recourir aux principe de la Programmation Orienté Aspects (AOP). Il existe un framework AOP pour AngularJS, vous pouvez le trouver à cette adresse.

Facade

Ce patron fournit une interface unifiée sur un ensemble d’interfaces d’un système. Il est utilisé pour réaliser des interfaces de programmation. Si un sous-système comporte plusieurs composants qui doivent être utilisés dans un ordre précis, une classe façade sera mise à disposition, et permettra de contrôler l’ordre des opérations et de cacher les détails techniques des sous-systèmes. Une façade peut :

  1. rendre une librairie plus simple à utiliser et à tester.
  2. rendre une librairie plus simple à comprendre.
  3. offrir une meilleure flexibilité dans le développement de la librairie.
  4. englober une collection d’API mal conçues en une seule API bien conçue (en foncton des tâches).

Facade

Il existe peut de façades dans AngularJS. A chaque fois que vous voulez fournir une API de haut niveau pour une certaine fonctionnalité, vous finissez par créer une façade.

Par exemple, regardons comment créer une requête POST avec XMLHttpRequest :

var http = new XMLHttpRequest(),

url = '/example/new',
params = encodeURIComponent(data);
http.open("POST", url, true);

http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
http.setRequestHeader("Content-length", params.length);
http.setRequestHeader("Connection", "close");
http.onreadystatechange = function () {
  if(http.readyState == 4 && http.status == 200) {
    alert(http.responseText);
  }
}

http.send(params);

Voici la même chose en utilisant le service $http d’AngularJS :

$http({
  method: 'POST',
  url: '/example/new',
  data: data
})
.then(function (response) {
  alert(response);
});

ou encore :

$http.post('/someUrl', data)
.then(function (response) {
  alert(response);
});

La seconde option offre une version pre-configurée qui créé une requête POST vers une URL donnée.

Le service $resource d’AngularJS est un service construit autour de $http et apporte une abstraction supplémentaire. Nous verrons ce nouveau dans les sections Enregistrement Actif (Active Record) et Proxy.

Proxy

Ce patron est un substitut d’un objet, qui permet de contrôler l’utilisation de ce dernier. Un proxy est un objet destiné à protéger un autre objet. Le proxy a la même interface que l’objet à protéger. Il peut être créé par exemple pour permettre d’accéder à distance à un objet (via un middleware). Un proxy, dans sa forme la plus simple, ne protège rien du tout et transmet tous les appels de méthode à l’objet cible.

Proxy

Nous pouvons différencier trois types de proxy :

  • Proxy virtuel
  • Proxy distant
  • Proxy de protection

Dans cette section, nous allons parler de l’implémentation du proxy virtuel dans AngularJS.

Dans le code suivant, il y a un appel vers la méthode get de l’instance $resource référencée par User :

var User = $resource('/users/:id'),
    user = User.get({ id: 42 });
console.log(user); // {}

L’appel de console.log affiche un objet vide. La requête AJAX, qui est émise lors de l’appel de la méthode User.get, est une requête asynchrone, nous n’avons pas vraiment l’objet user lorsque console.log est invoquée. Juste après que User.get déclenche la requête GET, elle retourne un objet vide et garde une référence vers ce dernier. Cet objet représente donc un proxy virtuel, qui sera mis à jour avec les données récupérées du serveur lors de la réception de la réponse.

Comment cela fonctionne dans AngularJS? Considérons le code suivant :

function MainCtrl($scope, $resource) {
  var User = $resource('/users/:id'),
  $scope.user = User.get({ id: 42 });
}
<span ng-bind="user.name"></span>

Pro tip : Il n’est pas conseillé d’utiliser $resource directement dans un contrôleur. Préférez le mettre dans une factory ou un service !

Lorsque ce code s’exécute, la propriété user de l’objet $scope est initialement vide ({}), ce qui signifie queuser.name sera undefined et rien ne sera rendu dans la vue. En interne, AngularJS garde une référence de cet objet vide. Lorsque le serveur répond à la requête GET, AngularJS met à jour l’objet avec les données reçues. Lors de l’itération suivante de la boucle du $digest, AngularJS détecte des changements dans l’objet $scope.user, ce qui déclenche le rafraichissement de la vue.

Enregistrement Actif (Active Record)

Active Record est une approche pour lire les données d’une base de données. Les attributs d’une table ou d’une vue sont encapsulés dans une classe. Ainsi l’objet, instance de la classe, est lié à un tuple de la base. L’objet Active Record encapsule donc les données ainsi que le comportement.

Active Record

AngularJS définit un service nommé $resource distribué dans un module additionnel. D’après la documentationd’AngularJS du service $resource :

Une factory pour la création d’objet permettant une interaction avec les données des sources RESTful.

L’objet retourné possède des méthodes d’action offrant une abstraction très haut niveau et ne nécessitant pas une interaction avec le service $http.

Voici comment le service $resource peut être utilisé :

var User = $resource('/users/:id'),
    user = new User({
      name: 'foo',
      age : 42
    });

user.$save();

l’appel à $resource retourne un constructeur permettant d’instancier des objets de notre modèle User. Chaque instance possède des méthodes correspondantes à des opérations de CRUD.

Le constructeur possède également des méthodes statiques équivalentes aux méthodes d’instances :

var user = User.get({ userid: userid });

Ce code retourne un proxy virtuel.

Les puristes dirons de suite que le service $resource n’implémente pas le parton Active Record, puisque ce dernier stipule que la responsabilité d’un tel patron de conception est de prendre en charge la communication avec la base de données. Or $resource communique lui avec des Web Services RESTful. A vrai dire, tout dépend de quel point de vue nous nous situons, pour une application SPA, une resource RESTful est considérée comme une source de données. Voilà ! problème résolue.

Vous pouvez trouver plus de détails concernant les pouvoirs du service $resource ici.

Intercepting Filters

Créé une chaîne de filtres composables afin d’implémenter des tâches de pré-processing et post-processing récurrentes lors de l’émission des requêtes.

Composite

Dans certains cas, il peut arriver que vous deviez traiter les requêtes HTTP sortantes et entrantes afin par exemple d’ajouter une gestion de logs, ajouter un mécanisme de sécurité, ou tout autre tâche concernée par le corps de la requête ou de ses entêtes. Les filtres d’interception inclus une chaîne de filtres pouvant chacun traiter des données dans un ordre définit. La sortie de chaque filtre est l’entrée du filtre suivant.

Dans AngularJS, nous rencontrons ce patron dans le service $httpProvider. Ce service possède un tableau de propriétés appelé interceptors qui contient une liste d’objet. Chaque objet peut posséder une ou toutes les propriété suivantes : request, response, requestError, responseError.

L’objet requestError est un intercepteur qui est appelé lorsque l’intercepteur request de la requête précédente jette une erreur ou un exception ou bien lorsqu’un promise a été rejetée. De même, responseError est appelé lorsque l’intercepteur response de la réponse précédente rencontre une erreur.

Voici un exemple basique :

$httpProvider.interceptors.push(function($q, dependency1, dependency2) {
  return {
   'request': function(config) {
       // same as above
    },
    'response': function(response) {
       // same as above
    }
  };
});

Voilà ! Nous arrivons à la fin de cet épisode.

Dans les prochains épisodes…

Dans le prochain, nous allons voir comment les patrons de conception classiques sont utilisés par les directives d’AngularJS.

Standard