AngularJs logicless templates : limitez la logique métier au sein de vos templates

Ceux qui ont utilisé un moteur de template logicless comme Mustache ou Handlebars savent immédiatement de quoi je parle. Un template doit contenir le minimum possible de logique métier. Et avec Mustachejs ou Handlebarsjs, le concept est poussé loin puisque c’est tout simplement impossible.

Logic-less templates – Semantic templates

J’ai découvert Handlebars sur mon premier projet EmberJs et son utilisation m’a paru bien déroutante au début. Voici un exemple de template Handlebars :

{{#if person.isAdmin}}
Welcome in admin area, <strong>{{person.firstName}} {{person.lastName}}</strong>!
{{/if}}

Jusque là, rien d’anormal ! Comment faire maintenant si je veux modifier un peu ma condition pour afficher ce bloc si la personne est Admin ou si elle est Super Admin ? L’extrait de code suivant semble tout indiqué :

{{#if person.isAdmin or person.isSuperAdmin}}
Welcome in admin area, <strong>{{person.firstName}} {{person.lastName}}</strong>!
{{/if}}

Pourtant c’est impossible ! Nous sommes dans un moteur de template logicless et nous n’avons pas la possibilité d’utiliser ce type de condition complexe.
Nous sommes dès lors obligés d’extraire la logique de notre template et d’injecter une condition simple, calculée en dehors. Exemple :

{{#if isAdminAreaAuthorized}}
Welcome in admin area, <strong>{{person.firstName}} {{person.lastName}}</strong>!
{{/if}}

Cela semble très contraignant au début mais on s’y fait finalement vite.
Ce type de template est en revanche parfaitement possible au sein d’une application AngularJs :

<p ng-show="person.isAdmin || person.isSuperAdmin">
    Welcome in admin area, <strong>{{person.firstName}} {{person.lastName}}</strong>!
</p>

Sur l’extrait de code précédent, la logique implémentée au sein du template reste assez concise. Mais je suis trop souvent tombé sur ce genre de templates sur des applications AngularJS :

<p ng-show="!isWorkInProgress && currentItem.price > 1000 && (user.id == currentItem.authorId || user.isAdmin || user.isSuperAdmin)">
  {{ currentItem.title }}
</p>

<p>Un paragraphe qui s'affiche toujours</p>

<p ng-show="!isWorkInProgress && currentItem.price > 1000  && (user.id == currentItem.authorId || user.isAdmin || user.isSuperAdmin)">
  {{ currentItem.description }}
</p>

Problèmes

Nous avons ici plusieurs problèmes :

  • Les conditions sont assez obscures et difficiles à comprendre
  • Les conditions sont répétées en plusieurs endroits au sein du même template
  • Mélange de logique métier et de logique d’affichage
  • La logique business est probablement dispatchée en différents endroits du code (des controllers, des services, des templates, …)

Difficile pour un intégrateur de travailler sur un template de ce type. Et quid du changement de condition pour l’affichage de nos blocs ? Nous sommes bons pour un chercher / remplacer douloureux.

Les moteurs de templates logic-less tels que que Mustache ou Handlebars sont loin de faire l’unanimité parmi les développeurs. En cause, leur aspect trop contraignant et l’obligation d’extraire la logique purement de présentation en même temps que la logique business. C’est vrai que finalement, la condition isWorkInProgress est liée uniquement à l’affichage et peut trouver légitimement sa place au sein du template.
Néanmoins, ces moteurs de templates logicless ont le mérite de forcer le développeur à extraire la logique business du template pour la centraliser au sein du controller ou de services dédiés.

Pour avoir le point de vue de personnes se positionnant contre les moteurs de templates logic-less, je vous conseille la lecture de The Case Against Logic-less Templates et de cet autre article dans la même veine : Cult of logicless templates.

AngularJS : extraction de la logique vers les controllers et services

En ce qui concerne vos applications AngularJS, de même qu’il est important de simplifier au maximum les controllers, il est important de limiter le plus possible la logique métier implémentée au sein des templates.

Dans les faits, le template précédent pourrait être repensé de la sorte :

<p ng-show="isItemViewableByUser(currentItem, user)">
  {{ currentItem.title }}
</p>

<p>Un paragraphe qui s'affiche toujours</p>

<p ng-show="isItemViewableByUser(currentItem, user)">
  {{ currentItem.description }}
</p>

Notre template est plus clair. Le nommage sémantique de la fonction qui conditionne l’affichage (isItemViewableByUser) rend le code compréhensible immédiatement. De plus, si la logique métier qui conditionne l’affichage change, le template n’a plus à changer.

Nous injectons cette logique dans le scope via le controller associé. Et pour ne pas faire l’erreur de remonter telle quelle une condition métier au sein du controller, n’oubliez pas d’extraire à son tour la logique métier de ce dernier vers un service dédié, rendant le tout plus facile à tester unitairement, plus facile à comprendre et donc plus facile à maintenir :

function MyCtrl($scope, UserService) {
    $scope.isWorkInProgress = false;
    // [...]
    $scope.isItemViewableByUser = function(item, user) {
        // Au choix, logique remontée au sein du Model
        // cf http://blog.overnetcity.com/2014/03/15/angularjs-models-donnees-model-data
        return !isWorkInProgress && user.canSeeItem(item);
        // ou au sein d'un service dédié
        return !isWorkInProgress && UserService.doesUserCanSeeItem(user, item);
    };
}

Pour aller plus loin

Quelques ressources pour aller plus loin :

EmberJs – De l’intérêt d’utiliser les Namespaces Ember ou comment connaître le nom de la classe d’une instance d’Ember.Object

Certaines parties de Ember/Ember-Data sont assez obscures pour les nouveaux venus. Parfois, les comportements du framework peuvent paraître relever de la magie aussi longtemps qu’on ne se plonge pas dans le code pour en isoler certains mécanismes.

C’était pour moi le cas concernant les requêtes émises par EmberData en fonction des modèles définit dans votre application.

Considérons le bout de code suivant :

MyApp.User = DS.Model.extend({
    firstname: DS.attr('string'),
    lastname: DS.attr('string')
    // ...
});

MyApp.User.find();

Lorsqu’on exécute un find sur ce modèle, EmberData est suffisamment intelligent pour lancer une requête GET sur /url/de/votre/api/user.

Mais comment ?

Quand on décortique l’adapter, on constate qu’au moment de construire la requête Ajax, ce dernier fait appel à la méthode rootForType du Serializer :

rootForType: function(type) {
    var typeString = type.toString();
    // [...]
    var parts = typeString.split(".");
    var name = parts[parts.length - 1];
    return name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1);
}

C’est en fait tout bête, le Serializer fait un toString sur notre classe du modèle.

MyApp.User.toString() // => MyApp.User

Et pour obtenir le même résultat depuis une instance, EmberData utilise tout simplement l’attribut constructor de l’objet :

MyApp.User.find(someId).then(function(someUser) {
    someUser.contructor.toString(); // => MyApp.User
});

On continue néanmoins à nager en pleine magie, comment Ember est-il capable d’afficher ce résultat sur un toString() d’un objet quelconque.
Ember redéfinie de manière intelligente la méthode toString de la manière suivante :

Ember.Mixin.prototype.toString = function() {
  if (!Ember.BOOTED && !this[NAME_KEY]) {
    processAllNamespaces();
  }

  var ret;
  if (this[NAME_KEY]) {
    ret = this[NAME_KEY];
  } else {
    var str = superClassString(this);
    if (str) {
      ret = "(subclass of " + str + ")";
    } else {
      ret = "(unknown mixin)";
    }
    this.toString = makeToString(ret);
  }

  return ret;
};

En lisant ce bout de code, on constate qu’un traitement particulier est fait à l’initialisation de l’application Ember pour parser tous les Namespaces.
Ember parcourt de manière récursive l’ensemble des Namespaces déclarés et surcharge pour chacune des classes appartenant à ces namespaces la méthode toString, de manière à renvoyer un path de la forme MyNameSpace.MySubNameSpace.MyAmazingClass.

Tout simplement !

Et je dois dire que ce comportement est bien pratique pour calculer certaines propriétés de manière générique, en se basant justement sur le nom de notre classe via la méthode toString.

Exemple :

// Au sein d'une classe héritant de Ember.Object et appartenant à un Namespace quelconque
myPropertyWhichReallyNeedMyClassname: function() {
    var parts = this.constructor.toString().split('.'),
        name = parts[parts.length - 1];

    return 'Something' + name;
}.property(),

Plus d’infos ici : http://www.emberist.com/2012/04/09/naming-conventions.html
Et dans le code évidemment : https://github.com/emberjs/ember.js/blob/master/packages/ember-runtime/lib/system/namespace.js

Ember Data Serialization Process – Hooks

Le Serializer de Ember Data s’occupe de transformer une instance d’un modèle Ember dans le format accepté par votre API.
Pour cela, Ember Data met à la disposition du développeur toute une série de Hooks permettant de s’adapter à chaque API, aussi complexe et peu consistante soit-elle.

Le code de Ember Data est extrêmement bien documenté mais comme on dit qu’une image vaut toujours mieux qu’un long discours, j’en ai extrait ce graphique me permettant de trouver plus rapidement où me brancher selon les cas :

Ember Data Serialization Process

Peut-être qu’il pourra être utile à certains.

Et pour plonger dans le code du Serializer : https://github.com/emberjs/data/blob/master/packages/ember-data/lib/system/serializer.js