AngularJS-Shield-large
AngularJS

AngularJS expliqué en patrons de conception, épisode 6 : bonus

Dans ce dernier épisode, nous allons voir comment des patrons de conception tels que le Module Pattern ou l’Observer sont utilisés dans 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.

Module Pattern

Ce patron de conception ne figure ni dans le catalogue des Gang of Four, ni dans celui du P of EAA. Par contre, c’est un patron de conception classique en JavaScript, dont le rôle est d’apporter une certaine forme d’encapsulation.

En se basant sur le patron Module, il est possible de tirer avantage de la puissance des fermetures ( closure) et de la portée lexicale qui limite la visibilité des variables aux fonctions dans lesquelles elles ont été déclarées.

Chaque module peut avoir zéro ou plusieurs membres privés, accessibles uniquement dans la portée locale de la fonction. Cette fonction retourne un objet qui expose une API publique du module en question. Voici un exemple :

var Page = (function () {

  var title;

  function setTitle(t) {
    document.title = t;
    title = t;
  }

  function getTitle() {
    return title;
  }

  return {
    setTitle: setTitle,
    getTitle: getTitle
  };

}());

Dans cet exemple, nous avons déclaré une IIFE (Immediately-Invoked Function Expression), une fonction auto-invoquée qui, une fois invoquée retourne un objet avec deux méthodes : setTitle() et getTitle(). Cet objet est ensuite affecté à la variable Page.

L’entité manipulant la variable Page n’a pas accès à la variable title par exemple, qui est définit dans la IIFE.

Ce patron est également très intéressant lorsque nous devons définir des services en AngularJS. Nous pouvons rendre privée une partie de la logique :

app.factory('foo', function () {

  function privateMember() {
    //body...
  }

  function publicMember() {
    //body...
    privateMember();
    //body
  }

  return {
    publicMember: publicMember
  };
});

Une fois le service foo injecté dans un composant, il ne nous sera pas possible d’utiliser directement les méthodes privées, mais uniquement celles exposées par l’objet retourné. Cette solution est extrêmement puissante surtout si nous développons une librairie JavaScript.

Data Mapper

Un Data Mapper est une couche d’accès au données qui réalise un transfert bi-directionnel entre la persistance de données (souvent une base relationnelle) et une représentation des données en mémoire (la couche métier). Le but de ce patron est de garder ces deux couches indépendantes les unes des autres, ainsi que du Data Mapper lui-même.

Data Mapper

Comme cité dans la définition, le Data Mapper est utilisé pour le transfert bi-directionnel de données entre la couche de persistance et la couche métier. Dans AngularJS, l’application communique avec un serveur exposant une API RESTful. AngularJS propose le service $resource qui permet de communiquer avec le serveur dans un style Active Record. Souvent, les entités retournées par l’API ne sont toujours pas formatées de la façon qui nous arrange, pour être directement exploitées.

Prenons par exemple ce cas de figure, supposons que l’on dispose d’un modèle User avec les attributs suivants :

  • un nom.
  • une adresse.
  • une liste d’amis

Voici l’API exposant les ressources suivantes :

  • GET /users/:id : retourne le nom et l’adresse d’un utilisateur.
  • GET /friends/:id : retourne la liste d’amis d’un utilisateur.

Une première solution naïve consisterait à avoir deux services, un pour la première ressource et un autre pour la seconde. Mais ce serait mieux si nous avions qu’un seul et unique service, User par exemple :

app.factory('User', function ($q) {

  function User(name, address, friends) {
    this.name = name;
    this.address = address;
    this.friends = friends;
  }

  User.get = function (params) {
    var user = $http.get('/users/' + params.id),
        friends = $http.get('/friends/' + params.id);
    $q.all([user, friends])
    .then(function (user, friends) {
      return new User(user.name, user.address, friends);
    });
  };
  return User;
});

Avec cette solution, nous avons créé un pseudo Data Mapper, qui a pour rôle d’adapter l’API en fonction des contraintes de notre SPA.

Nous pouvons utiliser le service User comme ceci :

function MainCtrl($scope, User) {
  User.get({ id: 1 }).then(function (data) {
    $scope.user = data;
  });
}

Et la vue :

<div>
  <div>
    Name: {{user.name}}
  </div>
  <div>
    Address: {{user.address}}
  </div>
  <div>
    Friends with ids:
    <ul>
      <li ng-repeat="friend in user.friends">{{friend}}</li>
    </ul>
  </div>
</div>

Le patron Observer grâce à un Service Commun

L’exemple de code a été récupéré depuis ce projet. Cet une factorie AngularJS qui crée un service implémentant le patron Observer. Ce patron fonctionne très bien avec la syntaxe ControllerAs et sert comme une alternative à  $scope.$watch() ou encore $scope.emit() et $scope.broadcast().

Ce patron est utilisé pour faire communiquer plusieurs contrôleurs utilisant le même modèle.

Voici un exemple démontrant comment attacher, notifier et détacher un événement grâce un Service Commun :

angular.module('app.controllers')
  .controller('ObserverExample', ObserverExample);

ObserverExample.$inject= ['ObserverService', '$timeout'];

function ObserverExample(ObserverService, $timeout) {
  var vm = this;
  var id = 'vm1';

  ObserverService.attach(callbackFunction, 'let_me_know', id)

  function callbackFunction(params){
    console.log('now i know');
    ObserverService.detachByEvent('let_me_know')
  }

  $timeout(function(){
    ObserverService.notify('let_me_know');
  }, 5000);
}

Une solution alternative…

angular.module('app.controllers')
  .controller('ObserverExample', ObserverExample);
ObserverExample.$inject= ['ObserverService', '$timeout', '$scope'];

function ObserverExample(ObserverService, $timeout, $scope) {
  var vm = this;
  var id = 'vm1';
  ObserverService.attach(callbackFunction, 'let_me_know', id)

  function callbackFunction(params){
    console.log('now i know');
  }

  $timeout(function(){
    ObserverService.notify('let_me_know');
  }, 5000);

  // Cleanup listeners when this controller is destroyed
  $scope.$on('$destroy', function handler() {
    ObserverService.detachByEvent('let_me_know')
  });
}

Voilà !

Nous arrivons à la fin de cette série d’articles consacrés à l’utilisation des patrons de conception dans des applications AngularJS.

Merci de votre lecture. N’hésitez pas à reparcourir cette série au besoin.

Standard