AngularJS Services, Factories et Providers

Front-end 03/04/2026 22:00:00 Mezgani said
Angularjs Services Factories Providers Injection
AngularJS Services, Factories et Providers

Différences entre Service, Factory et Provider AngularJS 1.x : injection de dépendances, singletons, configuration et cas d'usage réels.

L'injection de dépendances en AngularJS

Le système d'injection de dépendances (DI) est l'un des piliers d'AngularJS. Il permet de déclarer des services réutilisables et de les injecter automatiquement là où on en a besoin, sans instanciation manuelle. Comprendre les différences entre service, factory et provider est fondamental pour écrire du code AngularJS maintenable.

Les trois types partagent une propriété essentielle : ce sont tous des singletons. AngularJS crée une seule instance de chaque service pendant toute la durée de vie de l'application. La différence réside dans la façon dont cette instance est créée et dans la possibilité de la configurer avant le démarrage.

Règle d'or : Tous les recipes AngularJS (service, factory, provider, value, constant) créent des singletons. Une fois instancié, le même objet est partagé partout dans l'application.

Service : le singleton avec new

Quand vous déclarez un .service(), AngularJS appelle new sur la fonction que vous fournissez. C'est l'équivalent d'une classe — vous attachez vos méthodes et propriétés à this.

Syntaxe de base

// notification.service.js
angular.module('monApp')
  .service('NotificationService', function($timeout) {
    // 'this' représente l'instance créée par 'new'
    this.messages = [];

    // Ajoute une notification et la supprime après 3 secondes
    this.add = function(message, type) {
      var notif = { message: message, type: type || 'info', id: Date.now() };
      this.messages.push(notif);

      $timeout(function() {
        this.remove(notif.id);
      }.bind(this), 3000);
    };

    // Supprime une notification par son ID
    this.remove = function(id) {
      this.messages = this.messages.filter(function(m) {
        return m.id !== id;
      });
    };

    // Vide toutes les notifications
    this.clear = function() {
      this.messages = [];
    };
  });

Utilisation dans un controller

// dashboard.controller.js
angular.module('monApp')
  .controller('DashboardController', function($scope, NotificationService) {
    // Accès direct à l'instance singleton
    $scope.notifications = NotificationService.messages;

    $scope.notify = function(msg) {
      NotificationService.add(msg, 'success');
    };
  });
Quand utiliser .service() : Quand votre logique ressemble naturellement à une classe avec des méthodes et un état interne. Préférez-le quand vous venez d'un background orienté objet.

Factory : le singleton avec return

Avec .factory(), AngularJS appelle votre fonction et utilise la valeur retournée comme singleton. C'est plus flexible que .service() : vous contrôlez exactement ce qui est exposé (pattern module révélateur).

Syntaxe avec pattern module révélateur

// auth.factory.js
angular.module('monApp')
  .factory('AuthFactory', function($http, $window) {
    // Variables privées — non exposées à l'extérieur
    var TOKEN_KEY = 'auth_token';
    var currentUser = null;

    // Fonctions privées (non retournées)
    function saveToken(token) {
      $window.localStorage.setItem(TOKEN_KEY, token);
    }

    function getToken() {
      return $window.localStorage.getItem(TOKEN_KEY);
    }

    // Connexion : POST /api/login et sauvegarde du token
    function login(credentials) {
      return $http.post('/api/login', credentials)
        .then(function(response) {
          saveToken(response.data.token);
          currentUser = response.data.user;
          return currentUser;
        });
    }

    // Déconnexion : supprime le token local
    function logout() {
      $window.localStorage.removeItem(TOKEN_KEY);
      currentUser = null;
    }

    // Vérifie si l'utilisateur est connecté
    function isAuthenticated() {
      return !!getToken();
    }

    // On retourne uniquement l'API publique (encapsulation)
    return {
      login: login,
      logout: logout,
      isAuthenticated: isAuthenticated,
      getToken: getToken,
      getUser: function() { return currentUser; },
    };
  });

Factory qui retourne une classe instanciable

// model.factory.js — retourne un constructeur, pas une instance
angular.module('monApp')
  .factory('UserModel', function() {
    // On retourne une classe, pas un objet
    function User(data) {
      this.id    = data.id;
      this.name  = data.name;
      this.email = data.email;
    }

    // Méthode prototype — partagée par toutes les instances
    User.prototype.getFullLabel = function() {
      return this.name + ' <' + this.email + '>';
    };

    User.prototype.isAdmin = function() {
      return this.role === 'admin';
    };

    return User; // on retourne le constructeur
  });

// Utilisation dans un controller
angular.module('monApp')
  .controller('UserCtrl', function($scope, UserModel) {
    // On crée des instances avec 'new UserModel(...)'
    $scope.user = new UserModel({ id: 1, name: 'Alice', email: 'alice@test.com' });
    console.log($scope.user.getFullLabel()); // "Alice <alice@test.com>"
  });
Quand utiliser .factory() : Quand vous avez besoin d'encapsulation (variables privées), quand vous voulez retourner une valeur primitive ou un constructeur, ou quand votre logique de création est complexe.

Provider : le singleton configurable

.provider() est le recipe le plus puissant. Il permet de configurer le service avant que l'application démarre, dans la phase config(). C'est celui qu'AngularJS utilise en interne pour $routeProvider, $httpProvider, etc.

Un provider a deux parties : une phase de configuration (accessible dans module.config()) et une méthode $get qui retourne l'instance finale du service.

Provider configurable : exemple API client

// api.provider.js
angular.module('monApp')
  .provider('ApiClient', function() {

    // Configuration par défaut — modifiable dans module.config()
    var config = {
      baseUrl: 'http://localhost:3000',
      timeout: 5000,
      headers: {},
    };

    // Méthodes de configuration (accessibles dans la phase config)
    this.setBaseUrl = function(url) {
      config.baseUrl = url;
      return this; // chainable
    };

    this.setTimeout = function(ms) {
      config.timeout = ms;
      return this;
    };

    this.setHeader = function(key, value) {
      config.headers[key] = value;
      return this;
    };

    // $get retourne l'instance de service (phase run)
    // C'est ici que les dépendances run-time sont injectées
    this.$get = function($http) {
      return {
        get: function(path) {
          return $http({
            method: 'GET',
            url: config.baseUrl + path,
            timeout: config.timeout,
            headers: config.headers,
          });
        },
        post: function(path, data) {
          return $http({
            method: 'POST',
            url: config.baseUrl + path,
            data: data,
            timeout: config.timeout,
            headers: config.headers,
          });
        },
      };
    };
  });

Configuration dans module.config()

// app.config.js — phase de configuration (avant le démarrage)
angular.module('monApp')
  .config(function(ApiClientProvider) {
    // Note : on injecte 'ApiClientProvider' (pas 'ApiClient')
    // La phase config n'a accès qu'aux providers, pas aux instances

    ApiClientProvider
      .setBaseUrl('https://api.monsite.com/v2')
      .setTimeout(10000)
      .setHeader('Authorization', 'Bearer ' + getStoredToken())
      .setHeader('X-App-Version', '2.1.0');
  });
Convention de nommage : Le provider est toujours accessible sous le nom NomDuServiceProvider (avec le suffixe Provider) dans la phase config(). Dans la phase run() et dans les controllers, on injecte simplement NomDuService.

Constant et Value

AngularJS propose deux recipes supplémentaires pour les valeurs simples : .constant() et .value(). La différence est subtile mais importante.

Constant — disponible partout, même en config()

// app.constants.js
angular.module('monApp')
  .constant('APP_CONFIG', {
    apiUrl: 'https://api.monsite.com',
    version: '2.1.0',
    maxRetries: 3,
    supportEmail: 'support@monsite.com',
  })
  // Constant primitive
  .constant('MAX_ITEMS_PER_PAGE', 25);

// Utilisation dans la phase config (possible avec constant, pas avec value)
angular.module('monApp')
  .config(function(APP_CONFIG, SomeProvider) {
    SomeProvider.setUrl(APP_CONFIG.apiUrl); // ✅ fonctionne
  });

Value — disponible en run() et controllers, mais pas en config()

// app.values.js
angular.module('monApp')
  // Value peut être remplacée par un decorator (constant ne peut pas)
  .value('defaultUser', {
    role: 'guest',
    theme: 'light',
    language: 'fr',
  });

// NE FONCTIONNE PAS — value n'est pas accessible en config()
angular.module('monApp')
  .config(function(defaultUser) {
    // ❌ Error: Unknown provider 'defaultUser'
  });

Decorator : modifier un service existant

Le $provide.decorator() permet d'intercepter et d'augmenter n'importe quel service existant, y compris les services built-in d'AngularJS comme $log ou $http. C'est une forme d'AOP (programmation orientée aspect).

// logging.decorator.js — enrichit $log avec un préfixe horodaté
angular.module('monApp')
  .config(function($provide) {
    $provide.decorator('$log', function($delegate) {
      // $delegate = instance originale de $log

      var originalLog = $delegate.log.bind($delegate);
      var originalError = $delegate.error.bind($delegate);

      // On remplace $log.log par une version avec timestamp
      $delegate.log = function() {
        var args = Array.prototype.slice.call(arguments);
        var timestamp = '[' + new Date().toISOString() + ']';
        originalLog.apply(null, [timestamp].concat(args));
      };

      // On remplace $log.error pour aussi envoyer à un service de monitoring
      $delegate.error = function() {
        var args = Array.prototype.slice.call(arguments);
        originalError.apply(null, args);
        // Envoyer l'erreur à un service externe (ex: Sentry)
        if (window.Sentry) {
          window.Sentry.captureMessage(args.join(' '));
        }
      };

      return $delegate; // on retourne le $log modifié
    });
  });

Tableau comparatif complet

Recipe Création Accessible en config() Variables privées Configurable avant run
service()new Fn()❌ (tout sur this)
factory()Fn() retourne valeur✅ (closures)
provider()new Fn().$get()✅ (via Provider)
constant()Valeur directeN/AN/A
value()Valeur directeN/A

Équivalents en Angular moderne

La migration vers Angular simplifie considérablement la DI. Tous les recipes AngularJS convergent vers un seul pattern : la classe décorée @Injectable().

// Angular 21 — un seul pattern pour tous les cas d'usage

// Équivalent de service() et factory()
@Injectable({ providedIn: 'root' }) // singleton global
export class NotificationService {
  messages = signal<Notification[]>([]);

  add(message: string, type = 'info') {
    const notif = { message, type, id: Date.now() };
    this.messages.update(list => [...list, notif]);
  }
}

// Équivalent de constant() — utiliser InjectionToken
import { InjectionToken } from '@angular/core';

export const APP_CONFIG = new InjectionToken('APP_CONFIG', {
  providedIn: 'root',
  factory: () => ({
    apiUrl: 'https://api.monsite.com',
    version: '2.1.0',
  }),
});

// Équivalent de provider() — APP_INITIALIZER ou injection conditionnelle
import { APP_INITIALIZER } from '@angular/core';

export function configureApi(config: AppConfig) {
  return () => fetch('/api/config').then(r => r.json())
    .then(data => Object.assign(config, data));
}
Conclusion : En Angular moderne, @Injectable({ providedIn: 'root' }) remplace service() et factory(). Les InjectionToken remplacent constant() et value(). La phase config() n'existe plus — son équivalent est APP_INITIALIZER ou la configuration au démarrage.

Partager