Servizi non singleton in AngularJS


90

AngularJS afferma chiaramente nella sua documentazione che i servizi sono singoli:

AngularJS services are singletons

Controintuitivamente, module.factoryrestituisce anche un'istanza Singleton.

Dato che ci sono molti casi d'uso per servizi non singleton, qual è il modo migliore per implementare il metodo factory per restituire istanze di un servizio, in modo che ogni volta che ExampleServiceviene dichiarata una dipendenza, venga soddisfatta da un'istanza diversa di ExampleService?


1
Supponendo che tu possa farlo, dovresti? Altri sviluppatori Angular non si aspetterebbero che una factory iniettata di dipendenze restituisca sempre nuove istanze.
Mark Rajcok

1
Immagino sia una questione di documentazione. Penso che sia un peccato che questo non sia stato supportato fuori dal cancello poiché ora ci si aspetta che tutti i servizi saranno Singleton, ma non vedo motivo per limitarli a Singleton.
Undistraction

Risposte:


44

Non credo che dovremmo mai avere un ritorno in fabbrica a new funzione abile poiché questo inizia a interrompere l'iniezione di dipendenze e la libreria si comporterà in modo imbarazzante, specialmente per terze parti. In breve, non sono sicuro che ci siano casi d'uso legittimi per servizi non singleton.

Un modo migliore per ottenere la stessa cosa è utilizzare la factory come un'API per restituire una raccolta di oggetti con metodi getter e setter associati. Ecco uno pseudo-codice che mostra come potrebbe funzionare l'utilizzo di quel tipo di servizio:

.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();
  };
});

Questo è solo uno pseudo-codice per cercare un widget tramite ID e quindi poter salvare le modifiche apportate al record.

Ecco alcuni pseudo-codice per il servizio:

.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
    // ...
  };
});

Sebbene non inclusi in questo esempio, questi tipi di servizi flessibili potrebbero anche gestire facilmente lo stato.


Non ho tempo in questo momento, ma se sarà utile posso mettere insieme un semplice Plunker in seguito per dimostrarlo.


Questo è davvero interessante. Un esempio sarebbe davvero utile. Molte grazie.
Undistraction

Questo è interessante. Sembra che funzionerebbe in modo simile a un angolare $resource.
Jonathan Palumbo

@JonathanPalumbo Hai ragione - molto simile a ngResource. In effetti, Pedr e io abbiamo iniziato questa discussione tangenzialmente in un'altra domanda in cui ho suggerito di adottare un approccio simile a ngResource. Per un esempio così semplice come questo, non c'è alcun vantaggio nel farlo manualmente: ngResource o Restangular funzionerebbero a meraviglia . Ma per casi non così del tutto tipici, questo approccio ha senso.
Josh David Miller

4
@Pedr Scusa, me ne sono dimenticato. Ecco una demo semplicissima: plnkr.co/edit/Xh6pzd4HDlLRqITWuz8X
Josh David Miller,

15
@JoshDavidMiller potresti specificare perché / cosa "interromperà l'iniezione di dipendenze e [perché / cosa] la libreria si comporterà in modo imbarazzante"?
okigan

77

Non sono del tutto sicuro di quale caso d'uso stai cercando di soddisfare. Ma è possibile avere istanze di ritorno di fabbrica di un oggetto. Dovresti essere in grado di modificarlo in base alle tue esigenze.

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

Aggiornato

Considera la seguente richiesta per servizi non singleton . In cui Brian Ford osserva:

L'idea che tutti i servizi siano singleton non ti impedisce di scrivere factory singleton che possono istanziare nuovi oggetti.

e il suo esempio di restituzione di istanze dalle fabbriche:

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

Direi anche che il suo esempio è superiore perché non è necessario utilizzare la newparola chiave nel controller. È incapsulato nel getInstancemetodo del servizio.


Grazie per l'esempio. Quindi non è possibile fare in modo che il contenitore DI soddisfi la dipendenza con un'istanza. L'unico modo è far sì che soddisfi la dipendenza con un provider che può poi essere utilizzato per generare l'istanza?
Undistraction

Grazie. Concordo sul fatto che sia meglio che dover utilizzare nuovo in un servizio, tuttavia penso che sia ancora insufficiente. Perché la classe che dipende dal servizio dovrebbe sapere o preoccuparsi che il servizio con cui viene fornito è o non è un Singleton? Entrambe queste soluzioni non riescono ad astrarre questo fatto e stanno spingendo qualcosa che credo dovrebbe essere interno al contenitore DI nel dipendente. Quando crei un servizio, vedo che è pericoloso consentire al creatore di decidere se desidera o meno che venga fornito come singleton o come istanze separate.
Undistraction

+1 Molto aiuto. Sto utilizzando questo approccio con ngInfiniteScrolle un servizio di ricerca personalizzato in modo da poter ritardare l'inizializzazione fino a un evento di clic. JSFiddle della prima risposta aggiornato con la seconda soluzione: jsfiddle.net/gavinfoley/G5ku5
GFoley83

4
Perché l'utilizzo del nuovo operatore è dannoso? Mi sento come se il tuo obiettivo fosse un non singleton, l'uso newè dichiarativo ed è facile dire subito quali servizi sono singleton e cosa no. In base a se un oggetto è in fase di aggiornamento.
j_walker_dev

sembra che questa dovrebbe essere la risposta perché fornisce ciò che la domanda ha chiesto, specialmente l'appendice "Aggiornata".
lukkea

20

Un altro modo è copiare l'oggetto del servizio con angular.extend().

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

e poi, ad esempio, nel controller

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
});

Ecco un plunk .


Davvero pulito! Conosci pericoli dietro questo trucco? Dopotutto, è solo angolare, che estende un oggetto, quindi immagino che dovremmo andare bene. Tuttavia, fare dozzine di copie di un servizio sembra un po 'intimidatorio.
vucalur

9

So che questo post ha già ricevuto risposta, ma penso ancora che ci sarebbero alcuni scenari legittimi di cui hai bisogno per avere un servizio non singleton. Diciamo che ci sono alcune logiche aziendali riutilizzabili che possono essere condivise tra diversi controller. In questo scenario il posto migliore per inserire la logica sarebbe un servizio, ma cosa succede se abbiamo bisogno di mantenere uno stato nella nostra logica riutilizzabile? Quindi abbiamo bisogno di un servizio non singleton in modo che possa essere condiviso tra diversi controller nell'app. Ecco come implementerei questi servizi:

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));

    }]);

Questo è molto simile alla risposta di Jonathan Palumbo tranne che Jonathan incapsula tutto con la sua appendice "Aggiornata".
lukkea

1
Stai dicendo che un servizio non Singleton sarebbe persistente. E dovrebbe mantenere lo stato, sembra un po 'il contrario.
eran otzap

2

Ecco il mio esempio di un servizio non singleton, proviene da un ORM su cui sto lavorando. Nell'esempio mostro un modello base (ModelFactory) che desidero che i servizi ("utenti", "documenti") ereditino e che possano estendersi.

Nel mio ORM ModelFactory inietta altri servizi per fornire funzionalità extra (query, persistenza, mappatura dello schema) che è sandboxed utilizzando il sistema del modulo.

Nell'esempio, sia l'utente che il servizio documenti hanno le stesse funzionalità ma hanno i propri ambiti indipendenti.

/*
    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' 
            }
        }
     */
  }
]);

1

angular fornisce solo un'opzione di servizio / fabbrica singleton . un modo per aggirare il problema è disporre di un servizio di fabbrica che creerà una nuova istanza per te all'interno del controller o di altre istanze consumer. l'unica cosa che viene iniettata è la classe che crea nuove istanze. questo è un buon posto per iniettare altre dipendenze o per inizializzare il nuovo oggetto secondo le specifiche dell'utente (aggiungendo servizi o configurazione)

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);

}

quindi nella tua istanza consumer hai bisogno del servizio di fabbrica e chiama il metodo build sulla fabbrica per ottenere una nuova istanza quando ne hai bisogno

  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);
    }

  }

puoi vedere che fornisce l'opportunità di iniettare alcuni servizi specifici che non sono disponibili nel passaggio di fabbrica. puoi sempre fare in modo che l'iniezione avvenga sull'istanza di fabbrica per essere utilizzata da tutte le istanze del modello.

Nota ho dovuto rimuovere del codice in modo da poter commettere alcuni errori di contesto ... se hai bisogno di un esempio di codice che funzioni fammelo sapere.

Credo che NG2 avrà la possibilità di inserire una nuova istanza del tuo servizio nel posto giusto nel tuo DOM, quindi non avrai bisogno di costruire la tua implementazione di fabbrica. dovrò aspettare e vedere :)


buon approccio - mi piacerebbe vedere $ serviceFactory come un pacchetto npm. Se ti piace posso costruirlo e aggiungerti come collaboratore?
IamStalker

1

Credo che ci siano buone ragioni per creare una nuova istanza di un oggetto all'interno di un servizio. Dovremmo anche mantenere una mente aperta piuttosto che dire semplicemente che non dovremmo mai fare una cosa del genere, ma il singleton è stato creato in quel modo per una ragione . I controller vengono creati e distrutti spesso durante il ciclo di vita dell'app, ma i servizi devono essere persistenti.

Posso pensare a un caso d'uso in cui hai un flusso di lavoro di qualche tipo, come accettare un pagamento e hai più proprietà impostate, ma ora devo cambiare il loro tipo di pagamento perché la carta di credito del cliente non è riuscita e devono fornire una forma diversa di pagamento. Ovviamente questo ha molto a che fare con il modo in cui crei la tua app. È possibile reimpostare tutte le proprietà per l'oggetto di pagamento oppure creare una nuova istanza di un oggetto all'interno del servizio . Ma non vorresti una nuova istanza del servizio, né vorresti aggiornare la pagina.

Credo che una soluzione stia fornendo un oggetto all'interno del servizio di cui è possibile creare una nuova istanza e impostare. Ma, giusto per essere chiari, la singola istanza del servizio è importante perché un controller può essere creato e distrutto molte volte, ma i servizi hanno bisogno di persistenza. Quello che stai cercando potrebbe non essere un metodo diretto all'interno di Angular, ma un modello di oggetti che puoi gestire all'interno del tuo servizio.

Ad esempio, ho creato un pulsante di ripristino . (Questo non è testato, è solo una rapida idea di un caso d'uso per la creazione di un nuovo oggetto all'interno di un servizio.

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)
            }
        }
    }
}]);

0

Ecco un altro approccio al problema di cui sono rimasto abbastanza soddisfatto, in particolare se utilizzato in combinazione con Closure Compiler con ottimizzazioni avanzate abilitate:

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);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.