Services non-singleton dans AngularJS

90

AngularJS indique clairement dans sa documentation que les services sont des singletons:

AngularJS services are singletons

Contre-intuitivement, module.factoryretourne également une instance Singleton.

Étant donné qu'il existe de nombreux cas d'utilisation pour les services non singleton, quelle est la meilleure façon d'implémenter la méthode de fabrique pour renvoyer des instances d'un service, de sorte que chaque fois qu'une ExampleServicedépendance est déclarée, elle soit satisfaite par une instance différente de ExampleService?

Undistraction
la source
1
En supposant que vous puissiez le faire, n'est-ce pas? Les autres développeurs Angular ne s'attendraient pas à ce qu'une usine à injection de dépendances renvoie de nouvelles instances tout le temps.
Mark Rajcok
1
Je suppose que c'est une question de documentation. Je pense que c'est dommage que cela n'ait pas été pris en charge dès le départ, car on s'attend maintenant à ce que tous les Services soient des Singletons, mais je ne vois aucune raison de les limiter aux Singletons.
Undistraction

Réponses:

44

Je ne pense pas que nous devrions jamais avoir une newfonction capable de retourner une usine car cela commence à décomposer l'injection de dépendances et la bibliothèque se comportera de manière maladroite, en particulier pour les tiers. En bref, je ne suis pas sûr qu'il existe des cas d'utilisation légitimes pour les services non singleton.

Une meilleure façon d'accomplir la même chose est d'utiliser la fabrique comme API pour renvoyer une collection d'objets avec des méthodes getter et setter qui leur sont attachées. Voici un pseudo-code montrant comment l'utilisation de ce type de service peut fonctionner:

.controller( 'MainCtrl', function ( $scope, widgetService ) {
  $scope.onSearchFormSubmission = function () {
    widgetService.findById( $scope.searchById ).then(function ( widget ) {
      // this is a returned object, complete with all the getter/setters
      $scope.widget = widget;
    });
  };

  $scope.onWidgetSave = function () {
    // this method persists the widget object
    $scope.widget.$save();
  };
});

Il ne s'agit que d'un pseudo-code pour rechercher un widget par ID, puis pour pouvoir enregistrer les modifications apportées à l'enregistrement.

Voici un pseudo-code pour le service:

.factory( 'widgetService', function ( $http ) {

  function Widget( json ) {
    angular.extend( this, json );
  }

  Widget.prototype = {
    $save: function () {
      // TODO: strip irrelevant fields
      var scrubbedObject = //...
      return $http.put( '/widgets/'+this.id, scrubbedObject );
    }
  };

  function getWidgetById ( id ) {
    return $http( '/widgets/'+id ).then(function ( json ) {
      return new Widget( json );
    });
  }


  // the public widget API
  return {
    // ...
    findById: getWidgetById
    // ...
  };
});

Bien que non inclus dans cet exemple, ces types de services flexibles pourraient également facilement gérer l'état.


Je n'ai pas le temps pour le moment, mais si cela peut être utile, je peux préparer un simple Plunker plus tard pour en faire la démonstration.

Josh David Miller
la source
C'est vraiment intéressant. Un exemple serait vraiment utile. Merci beaucoup.
Undistraction
C'est intéressant. Il semble que cela fonctionnerait de manière similaire à un angulaire $resource.
Jonathan Palumbo
@JonathanPalumbo Vous avez raison - très similaire à ngResource. En fait, Pedr et moi avons commencé cette discussion de manière tangentielle dans une autre question où j'ai suggéré d'adopter une approche similaire à ngResource. Pour un exemple aussi simple que celui-ci, il n'y a aucun avantage à le faire manuellement - ngResource ou Restangular fonctionneraient à merveille . Mais pour les cas qui ne sont pas si typiques, cette approche a du sens.
Josh David Miller
4
@Pedr Désolé, j'ai oublié ça. Voici une démo super simple: plnkr.co/edit/Xh6pzd4HDlLRqITWuz8X
Josh David Miller
15
@JoshDavidMiller pourriez-vous spécifier pourquoi / qu'est-ce qui "décomposerait l'injection de dépendances et [pourquoi / quoi] la bibliothèque se comportera mal"?
okigan
77

Je ne suis pas tout à fait sûr du cas d'utilisation que vous essayez de satisfaire. Mais il est possible d'avoir une instance de retour d'usine d'un objet. Vous devriez pouvoir le modifier en fonction de vos besoins.

var ExampleApplication = angular.module('ExampleApplication', []);


ExampleApplication.factory('InstancedService', function(){

    function Instance(name, type){
        this.name = name;
        this.type = type;
    }

    return {
        Instance: Instance
    }

});


ExampleApplication.controller('InstanceController', function($scope, InstancedService){
       var instanceA = new InstancedService.Instance('A','string'),
           instanceB = new InstancedService.Instance('B','object');

           console.log(angular.equals(instanceA, instanceB));

});

JsFiddle

Actualisé

Considérez la demande suivante pour les services non singleton . Dans lequel Brian Ford note:

L'idée que tous les services sont des singletons ne vous empêche pas d'écrire des usines de singleton capables d'instancier de nouveaux objets.

et son exemple de retour des instances des usines:

myApp.factory('myService', function () {
  var MyThing = function () {};
  MyThing.prototype.foo = function () {};
  return {
    getInstance: function () {
      return new MyThing();
    }
  };
});

Je dirais également que son exemple est supérieur en raison du fait que vous n'avez pas à utiliser le newmot - clé dans votre contrôleur. Il est encapsulé dans la getInstanceméthode du service.

Jonathan Palumbo
la source
Merci pour l'exemple. Il n'y a donc aucun moyen de faire en sorte que le conteneur DI satisfasse la dépendance avec une instance. Le seul moyen est de le faire satisfaire la dépendance avec un fournisseur qui peut ensuite être utilisé pour générer l'instance?
Undistraction
Merci. Je conviens que c'est mieux que d'avoir à utiliser du nouveau dans un service, mais je pense que cela reste insuffisant. Pourquoi la classe qui dépend du service devrait-elle savoir ou se soucier que le service avec lequel elle est fournie est ou n'est pas un Singleton? Ces deux solutions ne parviennent pas à faire abstraction de ce fait et poussent quelque chose qui, à mon avis, devrait être interne au conteneur DI dans la personne à charge. Lorsque vous créez un service, je vois un mal connu en permettant au créateur de décider s'il souhaite ou non qu'il soit fourni en tant que singleton ou en tant qu'instances séparées.
Undistraction
+1 Très utile. J'utilise cette approche avec ngInfiniteScrollet un service de recherche personnalisé afin de pouvoir retarder l'initialisation jusqu'à un événement de clic. JSFiddle de la première réponse mis à jour avec la deuxième solution: jsfiddle.net/gavinfoley/G5ku5
GFoley83
4
Pourquoi l'utilisation du nouvel opérateur est-elle mauvaise? J'ai l'impression que si votre objectif n'est pas un singleton, alors l'utilisation newest déclarative et il est facile de dire tout de suite quels services sont des singletons et ce qui ne le sont pas. Basé sur si un objet est en cours de création.
j_walker_dev
semble que cela devrait être la réponse car elle fournit ce que la question a demandé - en particulier l'annexe «mise à jour».
lukkea
20

Une autre méthode consiste à copier l'objet de service avec angular.extend().

app.factory('Person', function(){
  return {
    greet: function() { return "Hello, I'm " + this.name; },
    copy: function(name) { return angular.extend({name: name}, this); }
  };
});

puis, par exemple, dans votre contrôleur

app.controller('MainCtrl', function ($scope, Person) {
  michael = Person.copy('Michael');
  peter = Person.copy('Peter');

  michael.greet(); // Hello I'm Michael
  peter.greet(); // Hello I'm Peter
});

Voici un plunk .

Evgenii
la source
Vraiment bien! Connaissez-vous des dangers derrière cette astuce? Après tout, c'est juste angular.extend'un objet, donc je suppose que nous devrions être bien. Néanmoins, faire des dizaines de copies d'un service semble un peu intimidant.
vucalur
9

Je sais que ce message a déjà reçu une réponse, mais je pense toujours qu'il y aurait des scénarios légitimes dont vous auriez besoin pour avoir un service non singleton. Disons qu'il existe une logique métier réutilisable qui peut être partagée entre plusieurs contrôleurs. Dans ce scénario, le meilleur endroit pour placer la logique serait un service, mais que se passe-t-il si nous devons conserver un état dans notre logique réutilisable? Ensuite, nous avons besoin d'un service non singleton afin de pouvoir être partagé entre différents contrôleurs dans l'application. Voici comment j'implémenterais ces services:

angular.module('app', [])
    .factory('nonSingletonService', function(){

        var instance = function (name, type){
            this.name = name;
            this.type = type;
            return this;
        }

        return instance;
    })
    .controller('myController', ['$scope', 'nonSingletonService', function($scope, nonSingletonService){
       var instanceA = new nonSingletonService('A','string');
       var instanceB = new nonSingletonService('B','object');

       console.log(angular.equals(instanceA, instanceB));

    }]);
msoltanie
la source
Ceci est très similaire à la réponse de Jonathan Palumbo, sauf que Jonathan encapsule tout avec son annexe "Mise à jour".
lukkea
1
Êtes-vous en train de dire qu'un service non Singleton serait persistant. Et devrait garder l'état,, semble un peu comme l'inverse.
eran otzap
2

Voici mon exemple de service non singleton, il provient d'un ORM sur lequel je travaille. Dans l'exemple, je montre un modèle de base (ModelFactory) dont je veux que les services ('utilisateurs', 'documents') héritent et étendent potentiellement.

Dans mon ORM, ModelFactory injecte d'autres services pour fournir des fonctionnalités supplémentaires (requête, persistance, mappage de schéma) qui sont mises en bac à sable à l'aide du système de modules.

Dans l'exemple, le service utilisateur et le service de documents ont les mêmes fonctionnalités mais ont leurs propres étendues indépendantes.

/*
    A class which which we want to have multiple instances of, 
    it has two attrs schema, and classname
 */
var ModelFactory;

ModelFactory = function($injector) {
  this.schema = {};
  this.className = "";
};

Model.prototype.klass = function() {
  return {
    className: this.className,
    schema: this.schema
  };
};

Model.prototype.register = function(className, schema) {
  this.className = className;
  this.schema = schema;
};

angular.module('model', []).factory('ModelFactory', [
  '$injector', function($injector) {
    return function() {
      return $injector.instantiate(ModelFactory);
    };
  }
]);


/*
    Creating multiple instances of ModelFactory
 */

angular.module('models', []).service('userService', [
  'ModelFactory', function(modelFactory) {
    var instance;
    instance = new modelFactory();
    instance.register("User", {
      name: 'String',
      username: 'String',
      password: 'String',
      email: 'String'
    });
    return instance;
  }
]).service('documentService', [
  'ModelFactory', function(modelFactory) {
    var instance;
    instance = new modelFactory();
    instance.register("Document", {
      name: 'String',
      format: 'String',
      fileSize: 'String'
    });
    return instance;
  }
]);


/*
    Example Usage
 */

angular.module('controllers', []).controller('exampleController', [
  '$scope', 'userService', 'documentService', function($scope, userService, documentService) {
    userService.klass();

    /*
        returns 
        {
            className: "User"
            schema: {
                name : 'String'
                username : 'String'
                password: 'String'
                email: 'String'     
            }
        }
     */
    return documentService.klass();

    /*
        returns 
        {
            className: "User"
            schema: {
                name : 'String'
                format : 'String'
                formatileSize: 'String' 
            }
        }
     */
  }
]);
Nath
la source
1

angular ne donne qu'une option de service / usine unique . une façon de contourner le problème est d'avoir un service d'usine qui créera une nouvelle instance pour vous à l'intérieur de votre contrôleur ou d'autres instances de consommateur. la seule chose qui est injectée est la classe qui crée de nouvelles instances. c'est un bon endroit pour injecter d'autres dépendances ou pour initialiser votre nouvel objet aux spécifications de l'utilisateur (ajout de services ou de configuration)

namespace admin.factories {
  'use strict';

  export interface IModelFactory {
    build($log: ng.ILogService, connection: string, collection: string, service: admin.services.ICollectionService): IModel;
  }

  class ModelFactory implements IModelFactory {
 // any injection of services can happen here on the factory constructor...
 // I didnt implement a constructor but you can have it contain a $log for example and save the injection from the build funtion.

    build($log: ng.ILogService, connection: string, collection: string, service: admin.services.ICollectionService): IModel {
      return new Model($log, connection, collection, service);
    }
  }

  export interface IModel {
    // query(connection: string, collection: string): ng.IPromise<any>;
  }

  class Model implements IModel {

    constructor(
      private $log: ng.ILogService,
      private connection: string,
      private collection: string,
      service: admin.services.ICollectionService) {
    };

  }

  angular.module('admin')
    .service('admin.services.ModelFactory', ModelFactory);

}

Ensuite, dans votre instance consommateur, vous avez besoin du service d'usine et appelez la méthode de construction sur l'usine pour obtenir une nouvelle instance lorsque vous en avez besoin

  class CollectionController  {
    public model: admin.factories.IModel;

    static $inject = ['$log', '$routeParams', 'admin.services.Collection', 'admin.services.ModelFactory'];
    constructor(
      private $log: ng.ILogService,
      $routeParams: ICollectionParams,
      private service: admin.services.ICollectionService,
      factory: admin.factories.IModelFactory) {

      this.connection = $routeParams.connection;
      this.collection = $routeParams.collection;

      this.model = factory.build(this.$log, this.connection, this.collection, this.service);
    }

  }

vous pouvez voir qu'il offre l'opportunité d'injecter des services spécifiques qui ne sont pas disponibles dans l'étape d'usine. vous pouvez toujours avoir une injection sur l'instance de fabrique à utiliser par toutes les instances de modèle.

Notez que j'ai dû supprimer du code pour que je puisse faire des erreurs de contexte ... si vous avez besoin d'un exemple de code qui fonctionne, faites-le moi savoir.

Je crois que NG2 aura la possibilité d'injecter une nouvelle instance de votre service au bon endroit dans votre DOM afin que vous n'ayez pas besoin de créer votre propre implémentation d'usine. devra attendre et voir :)

Gadi
la source
belle approche - j'aimerais voir que $ serviceFactory est un package npm. Si vous le souhaitez, je peux le construire et vous ajouter en tant que contributeur?
IamStalker
1

Je pense qu'il y a de bonnes raisons de créer une nouvelle instance d'un objet dans un service. Nous devrions également garder l'esprit ouvert plutôt que de simplement dire que nous ne devrions jamais faire une telle chose, mais le singleton a été conçu de cette façon pour une raison . Les contrôleurs sont souvent créés et détruits au cours du cycle de vie de l'application, mais les services doivent être persistants.

Je peux penser à un cas d'utilisation dans lequel vous avez un flux de travail quelconque, comme l'acceptation d'un paiement et vous avez plusieurs propriétés définies, mais vous devez maintenant changer leur type de paiement car la carte de crédit du client a échoué et il doit fournir une forme différente de Paiement. Bien sûr, cela a beaucoup à voir avec la façon dont vous créez votre application. Vous pouvez réinitialiser toutes les propriétés de l'objet de paiement ou créer une nouvelle instance d'un objet dans le service . Mais vous ne voudriez pas d'une nouvelle instance du service, ni ne voudriez actualiser la page.

Je pense qu'une solution fournit un objet dans le service que vous pouvez créer et définir une nouvelle instance. Mais, pour être clair, l'instance unique du service est importante car un contrôleur peut être créé et détruit plusieurs fois, mais les services ont besoin de persistance. Ce que vous recherchez n'est peut-être pas une méthode directe dans Angular, mais un modèle d'objet que vous pouvez gérer dans votre service.

À titre d'exemple, j'ai fait un bouton de réinitialisation . (Ce n'est pas testé, c'est vraiment juste une idée rapide d'un cas d'utilisation pour créer un nouvel objet dans un service.

app.controller("PaymentController", ['$scope','PaymentService',function($scope, PaymentService) {
    $scope.utility = {
        reset: PaymentService.payment.reset()
    };
}]);
app.factory("PaymentService", ['$http', function ($http) {
    var paymentURL = "https://www.paymentserviceprovider.com/servicename/token/"
    function PaymentObject(){
        // this.user = new User();
        /** Credit Card*/
        // this.paymentMethod = ""; 
        //...
    }
    var payment = {
        options: ["Cash", "Check", "Existing Credit Card", "New Credit Card"],
        paymentMethod: new PaymentObject(),
        getService: function(success, fail){
            var request = $http({
                    method: "get",
                    url: paymentURL
                }
            );
            return ( request.then(success, fail) );

        }
        //...
    }
    return {
        payment: {
            reset: function(){
                payment.paymentMethod = new PaymentObject();
            },
            request: function(success, fail){
                return payment.getService(success, fail)
            }
        }
    }
}]);
Esclave créatif
la source
0

Voici une autre approche du problème dont j'étais assez satisfait, en particulier lorsqu'elle est utilisée en combinaison avec Closure Compiler avec les optimisations avancées activées:

var MyFactory = function(arg1, arg2) {
    this.arg1 = arg1;
    this.arg2 = arg2;
};

MyFactory.prototype.foo = function() {
    console.log(this.arg1, this.arg2);

    // You have static access to other injected services/factories.
    console.log(MyFactory.OtherService1.foo());
    console.log(MyFactory.OtherService2.foo());
};

MyFactory.factory = function(OtherService1, OtherService2) {
    MyFactory.OtherService1_ = OtherService1;
    MyFactory.OtherService2_ = OtherService2;
    return MyFactory;
};

MyFactory.create = function(arg1, arg2) {
    return new MyFactory(arg1, arg2);
};

// Using MyFactory.
MyCtrl = function(MyFactory) {
    var instance = MyFactory.create('bar1', 'bar2');
    instance.foo();

    // Outputs "bar1", "bar2" to console, plus whatever static services do.
};

angular.module('app', [])
    .factory('MyFactory', MyFactory)
    .controller('MyCtrl', MyCtrl);
James Wilson
la source