Service, factory et provider dans AngularJS


AngularJS est un framework difficile à prendre en main. Pas parce qu’il est particulièrement compliqué, mais parce que ses concepts sont vraiment différents de ceux qu’on a l’habitude de rencontrer dans les frameworks habituels. Le pire, c’est quand on vient de jQuery, car Angular est un peu l’anti-jQuery et il faut littéralement désapprendre ses habitudes.

Généralement, les gens s’en sortent avec les contrôleurs. Ils ne mettent pas le bon code dedans, ils ne savent pas comment rendre les bouts de code indépendants et réutilisables, mais ils arrivent à en faire quelque chose. Les directives, ils n’y touchent pas, mais ils peuvent s’en passer pendant un certain temps et juste réutiliser du code trouvé sur Github.

Par contre le côté service/factory/provider, ça c’est un gros problème. On ne peut pas faire sans, mais peu de gens savent faire avec. En codant All that counts, j’ai réalisé que c’était un bon tuto à faire. Donc en avant.

Article long = musique, évidement.

On m’a très justement demandé les prérequis pour suivre cet article : il faut avoir fait des tutos de base sur Angular et notamment comprendre l’injection de dépendance et le data binding.

Que font ces trucs là ?

Techniquement, un service, une factory ou un provider, dans AngularJS, ça sert à la même chose. Un service est juste une manière plus facile d’écrire une factory, qui est juste une manière plus simple d’écrire un provider.

Les 3 servent à créer un objet Javascript ordinaire, c’est tout.

Oui, oui, c’est vraiment tout. C’est le seul et unique but de cela. Vous allez me dire, mais alors pourquoi se faire chier à les utiliser alors qu’on peut écrire un objet à la main ? Tout simplement parce qu’ils permettent d’encapsuler cette création, l’isoler du reste du code, un problème toujours difficile en Javascript.

Ils sont là pour éviter de pourrir le namespace global, tout en n’utilisant pas la technique bien connu de “je met un gros conteneur avec tout dedans et tout le monde y accède”, qu’on a tendance à voir dans la plupart des projets JS. Le code est ainsi plus maintenable et testable.

Chaque service/factory/provider permet d’avoir un groupe de fonctionnalités séparé du reste. On peut utiliser celui-ci facilement partout ailleurs en utilisant l’injection de dépendance. Si vous ne savez pas ce que c’est, arrêtez-vous tout de suite, c’est le principe de fonctionnement de base d’Angular, il faut quitter cet article et prendre un tuto pour débutant. Revenez ensuite.

Dans un service/factory/provider, on va donc mettre du code métier et/ou des données de l’app, mais groupés par thème.

Par exemple, sur une app video, on peut en faire un qui va contenir le code pour gérer le profile utilisateur et un pour gérer la playlist.

On aura donc deux services/factories/providers, qui vont créer au final deux objets. Chaque objet va contenir des méthodes et des données. Pour le profile, on peut imaginer qu’il va contenir le nom d’utilisateur, son ID, et une méthode de login et de logout :

{
    "name": "Hodor",
    "Id": 7898,
    "login": function(){
        // faire un truc pour se loger
    },
    "logout": function(){
        // faire un truc pour se deco
    }
}

Pour la playlist, on peut imaginer que ça va contenir la liste des videos, la video en cours de lecture et de quoi passer à la suivante et précédente :

{
    "name" : "Super playlist"
    "videos": [
        {"title": "video 1", "url": "http://..."},
        {"title": "video 2", "url": "http://..."}
    ],
    "currentVideo": 1,
    "next": function(){
        // passer à la vid suivante
    },
    "previous": function(){
        // passer à la vid précédente
    }
 
}

Et c’est tout. Ce n’est ni plus ni moins que ça, des objets normaux dans lesquels ont va mettre nos données et nos méthodes. Ce qui est “special”, c’est comment ils sont créés et utilisés. Et c’est ça qu’on va voir tout de suite.

Créer un objet via un service/factory/provider

Voyons le plus compliqué d’abord : le provider. Les deux autres ne sont que des raccourcis pour écrire un provider, de toute façon.

Le rôle du provider, c’est de créer et mettre à disposition un objet. C’est tout. Il n’y a rien de plus. Je répète, ça ne sert qu’à ça. “provider”, ça veut dire “fournisseur” en anglais. Et c’est ce que ça fait : ça fournit un objet. Supposons que vous avez déjà créé une app “monApp”. Écrire un provider pour créer l’objet “profile” ressemble à ça :

// On appelle notre objet qui sera créé "profile". Ce qui signifie que l'on
// pourra ensuite utiliser le nom "profile" pour annoncer quand
// on souhaite utiliser l'objet issu de ce provider.
monApp.provider("profile", function(){
 
    // Un provider, au final, c'est juste une fonction anonyme qu'on relie
    // à un nom :)
 
    // Cette fonction DOIT avoir une méthode nommé "$get" attachée à son "this"
    this.$get = function() {
 
        // Et la méthode "$get" DOIT retourner l'objet qu'on veut créer.
        return {
            "name": "Anonymous",
            "Id": null,
            "login": function(){
                // faire un truc pour se loger
            },
            "logout": function(){
                // faire un truc pour se deco
            }
        }
    }
});

C’est tout. Je vous jure que c’est tout. C’est juste une boîte qu’Angular va prendre, et appeler la méthode $get pour obtenir un objet. En plus, un seul objet, car la méthode ne sera appelée qu’une fois : l’objet sera un singleton.

Plus tard dans le code, quand vous ferez un controller :

mymonAppApp.controller('UnControlleurCommeUnAutre', function($scope, profile) {
    $scope.profile = profile;
});

Vous utiliserez profile en paramètre, il va donc être injecté automatiquement. C’est tout le principe d’angular, des tas de boîtes qui déclarent quelles autres boîtes elles utilisent via l’injection de dépendance.

Ici, l’objet va être créé (si ce n’est pas déjà fait ailleurs), et c’est tout. Le boulot d’un controller, c’est essentiellement de mettre des objets issus des providers dans le scope pour que le HTML puisse les utiliser. Je vous promet, c’est 90% des use cases.

Votre objet sera le conteneur que l’on va manipuler pour changer le nom du profile, se logger, etc. C’est juste une boîte qui rend un service. D’ailleurs on appelle souvent les objets issus des providers des “services”.

Utiliser des providers évite de créer tout une logique de classes, de gérer des namespaces, des instanciations : on est obligé de faire de l’encapsulation et de la composition. Ca rend le code (javascript) plus propre et plus testable, bien décomposé et découplé. Avoir un code propre est le but principal des providers, ils ne fournissent rien comme fonctionnalités qu’on ne puisse faire autrement.

Maintenant voyons ce qu’est une factory. En fait, c’est juste un raccourci pour écrire un provider :

monApp.factory('profile', function() {
    return {
        "name": "Anonymous",
        "Id": null,
        "login": function(){
            // faire un truc pour se loger
        },
        "logout": function(){
            // faire un truc pour se deco
        }
    }
});

Ça fait exactement la même chose, et on l’utilise exactement pareil. C’est juste plus court. Pas de méthode $get à écrire, on retourne l’objet cash pistache.

Pourquoi on utilise un provider alors ? Parce que c’est plus pratique à configurer, comme on le verra plus bas. Mais la factory est suffisante la plupart du temps. Ce qui est marrant, c’est qu’en terme informatique, un provider est un design pattern “factory”, ce qui m’a vachement rendu confus au début…

Et le service alors ? C’est une syntaxe alternative :

monApp.service('profile', function() {
    this.name = "Anonymous";
    this.id = null;
    this.login = function(){
        // faire un truc pour se loger
    }
    this.logout = function(){
        // faire un truc pour se deco
    }
});

La fonction va être utilisée directement (avec new) pour créer l’objet, donc pas besoin de return : on attache tout à this. Mais le résultat est strictement le même. Le choix entre une factory et un service est vraiment une question de goût. Personnellement je vous conseille d’utiliser des factories car ça vous évite le casse-tête de la portée de this, grande cause d’erreurs en JS.

Dépendances et injections

Souvenez-vous qu’on peut faire ça :

mymonAppApp.controller('UnControlleurCommeUnAutre', function($scope, profile) {
    $scope.profile = profile;
});

Pour dire “mon controller dépend de profile, donc s’il te plait Angular, crée l’objet et passe le moi”.

Et bien ça marche aussi entre les providers/factories/services. Par exemple, si je fais un provider pour ma playlist, mais que la playlist dépend du profile :

monApp.provider("playlist", function(){
 
    // En utilisant le nom de l'autre provider ici, je demande à ce qu'il
    // soit injecté ici, et donc disponible ici. Je dis "ce provider
    // est dépendant de l'objet fournit par cet autre provider."
    this.$get = function(profile) {
 
        return {
            // Et du coup je peux accéder aux attributs de l'objet.
            "name": "Playlist de " + profile.name
            "videos": [],
            "currentVideo": 1,
            "next": function(){
                // passer à la vid suivante
            },
            "previous": function(){
                // passer à la vid précédente
            }
        }
    }
});

Si je le faisais avec une factory, se serait pareil, mais en plus simple :

// l'injection de dépendance se fait au niveau de la fonction anonyme
// car il n'y a pas de méthode "$get"
monApp.factory("playlist", function(profile){
    return {
        "name": "Playlist de " + profile.name
        "videos": [],
        "currentVideo": 1,
        "next": function(){
            // passer à la vid suivante
        },
        "previous": function(){
            // passer à la vid précédente
        }
    }
});

L’interêt des providers

L’interêt principal d’utiliser un provider plutôt qu’une factory, c’est qu’on peut le rendre configurable.

Par exemple, je veux que ma playlist ne soit jamais vide, alors si elle l’en, on met deux vidéos par défaut dedans :

monApp.factory("playlist", function(profile){
 
    // je ne retourne pas l'objet tout de suite, je vais le modifier
    var playlist = {
        "name": "Playlist de " + profile.name
        "videos": [],
        "currentVideo": 1,
        "next": function(){
            // passer à la vid suivante
        },
        "previous": function(){
            // passer à la vid précédente
        }
    };
 
    // hop, je m'assure que la playlist n'est jamais vide en modifiant
    // l'objet
    if (playlist.videos.length === 0){
        playlist.videos = [
            {"title": "video 1", "url": "http://..."},
            {"title": "video 2", "url": "http://..."}
        ]
    }
 
    // ne pas oublier de retourner l'objet à la fin quand même :)
    return playlist;
});

Tout ça sera largement suffisant dans la plupart des cas. Mais dans le rare cas où je veux faire une lib réutilisable, je veux que ces vidéos par défaut soient configurables. Comment alors permettre que l’utilisateur de ma lib les choisisse ?

En utilisant un provider :

monApp.provider("playlist", function(){
 
    // On déclare des données attachée sur le provider. PAS sur l'objet que
    // le provider va créer, attention. Sur le provider lui-même.
    // Le provider, je le rappelle, c'est cette fonction anonyme qui retourne
    // un objet via sa méthode "$get" (oui, les fonctions sont des objets
    // en JS, et peuvent avoir des méthodes. Ce langage est très clair.)
    this.defaultVideos = [
            {"title": "video 1", "url": "http://..."},
            {"title": "video 2", "url": "http://..."}
    ]
 
    // ensuite je fais ma méthode "$get" qui va retourner l'objet voulu,
    // comme d'hab
    this.$get = function(profile) {
 
        var playlist = {
            "name": "Playlist de " + profile.name
            "videos": [],
            "currentVideo": 1,
            "next": function(){
                // passer à la vid suivante
            },
            "previous": function(){
                // passer à la vid précédente
            }
        }
 
        // Au moment de la création de l'objet, j'utilise les données
        // attachées au provider pour fournir la valeur par défaut.
        if (playlist.videos.length === 0){
            playlist.videos = this.defaultVideos;
        }
 
        return playlist;
    }
});

Jusqu’ici, vous allez me dire, mais quelle est la différence avec le précédent ? Et bien il se trouve qu’Angular met à votre disposition automatiquement playlist, mais également playlistProvider, une référence sur le provider qui va créer l’objet playlist.

ATTENTION : quand vous déclarez ici “playlist”, “playlist” est le nom de l’objet créé par le provider. Angular, lui, va automagiquement attacher le nom “playlistProvider” au provider qui a créé l’objet. Vous n’avez rien à faire pour cela, il va automatiquement s’appeler “nomDeVotreObjetProvider” et sera mis à votre disposition.

Du coup :

// On peut utiliser "config" pour lancer du code avant la création des objets
// par les providers. Tout le code dans des blocs "config" est toujours lancé en premier,
// avant tous les providers/factories/services de cette app.
monApp.config(function(playlistProvider){
    // ici l'utilisateur de votre lib a accès au provider avant la création de
    // l'objet, et à donc le loisir de changer les valeurs par défaut
    playlistProvider.defaultVideos = [
            {"title": "Autre video", "url": "http://..."},
    ]
});

Ce n’est pas un cas courant, et la plupart du temps, utiliser une factory marchera très bien.

Et tout ça, je m’en sers pour quoi ?

On met dans les providers/services/factories, tout le code qui n’est pas lié à la navigation ou à la manipulation de DOM. Bref, tout le code de la logique de votre app. Le code qui n’est pas lié au Web. Un profile n’a rien à avoir avec le Web. Une playlist n’a rien à voir avec le Web. Mais il faut un code pour les gérer, et des variables vont devoir êtres mises quelque part.

Généralement, ces providers/services/factories sont utilisés :

  • Par un controller.
  • Par une directive.
  • Par d’autres providers/services/factories.

Mais aussi, directement dans le HTML.

En effet, quand vous allez faire ça :

// le boulot du controller, c'est essentiellement d'attacher les bons
// services au scope, je vous dis !
mymonAppApp.controller('VideoCtrl', function($scope, playlist) {
    $scope.playlist = playlist;
});

Vous allez ensuite pouvoir déclarer votre controller dans votre HTML. Dans un
bloc de HTML couvert par un controller, vous avez accès à toutes les attributs
du scope, donc à votre service “playlist”. Et du coup, paf, à tout son code :

<div ng-controller="VideoCtrl">
    ...
    <p>Playlist :</p>
    <ul>
       <li ng-repeat="vid for playlist.videos">{{ vid.title }}</li>
    </ul>
    <p>
    <button ng-click="playlist.next()">Next</button>
    </p>
</div>

Et c’est ça la beauté d’Angular : tout est bien séparé, et bien rangé. Votre code métier dans les providers, votre code d’interface dans le HTML, votre liaison entre le HTML et les providers à travers les controllers… Enfin du code JS qui ne ressemble pas à des spaghetti trop cuites par un enfant de 6 ans scatoman.

29 thoughts on “Service, factory et provider dans AngularJS

  • Dica

    sont ID” => “son ID”
    “mais groupés par theme” => “mais groupés par thème”
    (j’ai un doute sur “groupés”, j’enlèverais le ‘s’)
    “alors si elle l’en” => “alors si elle l’est”
    “accès à toutes les attributs” => “accès à tous les attributs”
    “Ca fait” => “Ça fait”

    à la fin de plusieurs blocs de code, il y a un }]); et je me demandais d’où venait le ]

    Merci pour l’article, j’aime pas le JS mais ça passe tout seul là

  • Teocali

    Dis donc, ca me donnerait presque enve de me mettre au JavaScript…

    Mais non, je viens de decouvrir Django et je suis amoureux. Une femme et une maitresse, pourquoi pas, mais deux maitresses, c’est au dessus de mes forces… On a plus 20 ans

  • Swizz

    Actuellement, justement entrain d’étudier AngularJS.
    Ton article soulève une question supplémentaire dans mon esprit.

    Est il possible de combiner l’utilisation d’un provider et d’une API REST ?

    En gros, actuellement j’ai plusieurs controller qui se contente de faire des gets. Mais à terme, il serait intéressant de pouvoir utiliser les providers pour des “edit” ou des “delete”.

    Je ne sais pas si je suis compréhensible, en faite.

  • Rémy

    Un grand merci pour cet article!
    J’étais justement en train de galérer à comprendre la doc d’Angular sur l’injection de dépendances (en grande partie à cause de la barrière de la langue). Heureusement que je consulte votre site chaque jour ^^.
    Merci encore.

  • Morgotth

    Ça serait bien de dire les prérequis nécessaire avant de se lancer dans le tuto. Je peux le lire sans avoir découvert Angular ou il vaut mieux le lire après un tuto découverte ?

  • Teocali

    @Morgoth :
    Perso, je dirais que les bases du JS et une connaissance minimale des principes de l’injection de dependance, genre un peu de bouteille avec Java Spring, sont suffisantes. En tout cas, c’etait le cas pour moi.

  • Badeu

    L’intro résume bien je trouve :

    Le pire, c’est quand on vient de jQuery, car Angular est un peu l’anti-jQuery et il faut littéralement désapprendre ses habitudes.

    Après c’est franchement pas violent Angular mais il faut juste accepter que sa fonctionne différemment de ce qu’on a l’habitude de croiser et se mettre dedans. Après une grosse journée à fond dessus on commence à maitriser les concepts de base.

  • Sam Post author

    @Swizz : pour ça il y a une feature spéciale appelée une ressource (https://docs.angularjs.org/api/ngResource/service/$resource).

    @Rémy: et encore, je me suis taté à faire un article dédié à l’injection de dépendance juste avant. Mais bon, fuck.

    @Morgotth: tu as raison, j’ai rajouté une ligne à ce propos. Il vaut mieux suivre l’article après un tuto découverte, car il faut comprendre quelques concepts de base. Et puis surtout, c’est après le tuto découverte qu’on se rend compte à quel point ces notions sont pas claires.

    @PunKeel: super projet, qui je pense va être intégré dans angular 2. Je tweet.

  • Walt

    Sam, si ce blog commence à parler régulièrement de Angular en plus de Django, c’est la grosse extase !
    Je m’explique :Comme l’a dit un article trolleux sur le JS, aujourd’hui on est absolument obligé de s’en servir pour faire du ouaib / mobile, même si parfois on a envie de tuer un chat avec les ‘})];’ qu’on peut retrouver.

    Django + AngularJS ensemble c’est un petit rêve. Juste avec ces deux technos aujourd’hui vous pouvez faire :
    – Django : un vrai site back-end robuste, une jolie API (Django)
    – Angular : créer des sites web et applications mobiles (et oui, même des “vraies” applis mobiles sur smartphone, avec des wrappers comme Cordova).

    Désolé pour le petit hors sujet, mais tout ça pour vous encourager à continuer sur cette voie de l’ouverture :) Il y a encore trop peu de contenu de qualité en français sur AngularJS (le blog FRangular est pas mal mais parfois le mec se perd un peu dans des détails techniques qui perdent tout le monde je trouve).

    Ciao !

  • walt

    Ha tant qu’on y est, une petite question à Sam que d’autres ont du se poser :
    Vous avez essayé de faire de l’angularJS en remplacant javascript par cofeescript ?

    Avantage évident : se débarrasser de la syntaxe horrible du Javascript.
    Inconvénient : 95% du code Angular sur internet est en JS, donc ça nécessite à chaque fois des efforts d’adaptation.

    De mon côté, j’essaierai quand j’aurai un niveau correct en Angular.

  • Sam Post author

    @walt: j’ai essayé coffeescript en long et en large et je n’aime pas du tout. Ca mériterait un article coup de gueule :) Mais en gros :

    – ça rajoute un niveau d’indirection en plus;
    – c’est chiant à setup;
    – ça réduit encore plus la pool de recrutement ou alors allonge la formation;
    – ça s’utilise que pour Javascript;
    – on se retrouve toujours avec un mélange js et coffeescript car comme tu l’as dit, tout le monde ne l’utilise pas;
    – la syntaxe n’a pas du tout été pensée pour diminuer la charge cognitive mais pour diminuer la frappe. Résultat, pas de délimiteurs là où il faut, et une lecture du code plus difficile qu’en JS, c’est un comble.

  • PoF

    Dans mon taf j’utilise exclusivement des factory pour créer de nouveau services, et je comprenais pas bien la diff entre service/factory/provider, merci !
    J’étais pas trop d’accord avec l’article à troll sur le JS, mais c’est peut être parce que je suis venu au javascript grâce a angular.

  • Sam Post author

    Angular est l’exemple typique qui prouve que Javascript est un langage de merde : il faut tout ce bordel pour faire du code propre. J’adore Angular, mais dans un bon langage, il n’existerait même pas, parce qu’on aurait des namespaces, des classes, des un framework de test inclu, etc. Angular est fantastique, dans le sens ou c’est “une béquille extraordinnaire pour notre jambe de bois de merde”.

  • Sam Post author

    Bizarrement, j’arrive pas à vendre une seule formation angular, alors que les formations jquery, ça fuzze. Et les gars semblent découvrir jquery comme si c’était une nouvelle techno. Ca fait peur l’innertie technique, j’vous jure.

  • G-rom

    La fonction va être utilisée directement (avec new) pour créer l’objet, donc pas besoin de return : on attache tout à this. Mais le résultat est strictement le même. Le choix entre une factory et un service est vraiment une question de goût. Personnellement je vous conseille d’utiliser des factories car ça vous évite le casse-tête de la portée de this, grande cause d’erreurs en JS.

    Mouais… c’est surtout qu’un service va te renvoyer une instance, là où la factory te retourne un simple objet / hash.

    Le résultat n’est pas le même et le premier te permet de jouer avec la chaine de prototypage et le pseudo héritage alors que l’autre non.

    Après rien n’empêche de renvoyer une instance depuis une factory au lieu d’un objet.

    Sinon, vrai question, qui a une méthode simple ou un exemple d’appli angularJS qui va plus loin que la single page app, avec plein de vues, une navigation, etc… J’ai du mal à voir comment on peut gérer facilement des régions / zones / vues.

  • Sam Post author

    Effectivement je n’avais pas pensé au proto. Je l’utilise tellement jamais. Avec angular, on fait de la composition partout, du coup le prototypage…

    Pour les zones / vues, les routes d’angular sont assez limitées. En général, pour une grosse app, on utilise ça : https://github.com/angular-ui/ui-router. D’ailleurs, ça sera intégré dans angular 2 je pense.

  • G-rom

    Mouais pas convaincu… je reste avec Backbone.Marionette pour les app plus grosses que la SPA

  • funkomatic

    Super ! Je n’ai pas lu d’article aussi drôle et intelligent depuis bien longtemps ! Merci beaucoup .

  • MLCissé

    Oh comme ton article m’a donné envie de continuer ce que je fais avec Angular. C’est un Framework tellement puissant que ça en fait peur. J’ai peur de ne plus pouvoir m’en passe :(

  • Vespira

    hello,

    merci super article bien expliqué ! si j’ai bien compris, chaque factory (ou service) doit correspondre a la création d’un ou plusieurs objets du même type. Admettons que j’ai deux requêtes HTTP GET à faire sur deux Web Services me renvoyant chacun un objet JS; j’imagine qu’il serait donc plus propre de faire une factory par type d’objet (donc 2) ? ou je peux me permettre de tout mettre dans une seule factory, merger mes objets et renvoyer le tout ? (ca paraît sale lol).

  • Sam Post author

    Tu fais une factory par API car ton objet représente ton client API, et un client finira forcément par faire bien plus qu’une requête GET :

    • encapsulation des URL et paramètres;
    • authentification;
    • gestion des erreurs;
    • casting des valeurs;
      etc
    • Vespira

      ok c’est bien ce qui me semblait, au tout début je faisais tout dans le controleur. c’est vite devenu l’enfer, d’où ma récente documentation et mon passage aux factories. merci, et bonne continuation !

  • tonio

    top ton article sérieux vrai de vrai j’ai tout compris et j’ai même tout kiffé

    le JS est notre futur, acceptez le

    • Sam Post author

      Ce JS en tout cas en sera pas ton future, car Angular 2 casse toute la compatibilité avec ce code qui ne marchera donc bientôt plus.

      Je ne sais pas si le JS est notre futur, mais l’instabilité est le passé et le présent du JS.

  • MedJS

    Merci beaucoup pour cet article. C’est bien expliqué et ça éclairci des concepts fondamentaux d’AngularJS.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.