Nicht-Singleton-Dienste in AngularJS

90

AngularJS stellt in seiner Dokumentation klar fest, dass Services Singletons sind:

AngularJS services are singletons

Gibt intuitiv module.factoryauch eine Singleton-Instanz zurück.

Was ist der beste Weg, um die Factory-Methode zum Zurückgeben von Instanzen eines Dienstes zu implementieren, damit jedes Mal, wenn eine ExampleServiceAbhängigkeit deklariert wird, eine andere Instanz von erfüllt wird , da es viele Anwendungsfälle für Nicht-Singleton-Dienste gibt ExampleService?

Ablenkung
quelle
1
Angenommen, Sie könnten dies tun, sollten Sie? Andere Angular-Entwickler würden nicht erwarten, dass eine von Abhängigkeiten injizierte Factory ständig neue Instanzen zurückgibt.
Mark Rajcok
1
Ich denke, das ist eine Frage der Dokumentation. Ich finde es schade, dass dies nicht von Anfang an unterstützt wurde, da jetzt erwartet wird, dass alle Dienste Singletons sein werden, aber ich sehe keinen Grund, sie auf Singletons zu beschränken.
Undistraction

Antworten:

44

Ich denke nicht, dass wir jemals eine Fabrik haben sollten, die eine newfähige Funktion zurückgibt, da dies die Abhängigkeitsinjektion zusammenbricht und sich die Bibliothek ungeschickt verhält, insbesondere für Dritte. Kurz gesagt, ich bin mir nicht sicher, ob es legitime Anwendungsfälle für Nicht-Singleton-Dienste gibt.

Eine bessere Möglichkeit, dasselbe zu erreichen, besteht darin, die Factory als API zu verwenden, um eine Sammlung von Objekten mit den damit verbundenen Getter- und Setter-Methoden zurückzugeben. Hier ist ein Pseudocode, der zeigt, wie die Verwendung dieser Art von Dienst funktionieren könnte:

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

Dies ist nur ein Pseudocode, um ein Widget nach ID zu suchen und dann die am Datensatz vorgenommenen Änderungen zu speichern.

Hier ist ein Pseudocode für den Dienst:

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

Obwohl in diesem Beispiel nicht enthalten, können diese Arten von flexiblen Diensten auch den Status problemlos verwalten.


Ich habe momentan keine Zeit, aber wenn es hilfreich ist, kann ich später einen einfachen Plunker zusammenstellen, um dies zu demonstrieren.

Josh David Miller
quelle
Das ist wirklich interessant. Ein Beispiel wäre wirklich hilfreich. Vielen Dank.
Undistraction
Das ist interessant. Es scheint, als würde es ähnlich wie ein Winkel funktionieren $resource.
Jonathan Palumbo
@ JonathanPalumbo Du hast Recht - sehr ähnlich zu ngResource. Tatsächlich haben Pedr und ich diese Diskussion tangential in einer anderen Frage begonnen, in der ich einen ähnlichen Ansatz wie ngResource vorgeschlagen habe. Für ein so einfaches Beispiel gibt es keinen Vorteil, wenn Sie es manuell ausführen - ngResource oder Restangular würden schwimmend funktionieren. Für Fälle, die nicht ganz so typisch sind, ist dieser Ansatz jedoch sinnvoll.
Josh David Miller
4
@Pedr Sorry, das habe ich vergessen. Hier ist eine supereinfache Demo: plnkr.co/edit/Xh6pzd4HDlLRqITWuz8X
Josh David Miller
15
@JoshDavidMiller Könnten Sie angeben, warum / was "die Abhängigkeitsinjektion auflösen würde und [warum / was] sich die Bibliothek unbeholfen verhält"?
Okigan
77

Ich bin mir nicht ganz sicher, welchen Anwendungsfall Sie zu befriedigen versuchen. Es ist jedoch möglich, dass eine Factory Instanzen eines Objekts zurückgibt. Sie sollten dies an Ihre Bedürfnisse anpassen können.

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

Aktualisiert

Betrachten Sie die folgende Anforderung für Nicht-Singleton-Dienste . In dem Brian Ford bemerkt:

Die Idee, dass alle Dienste Singletons sind, hindert Sie nicht daran, Singleton-Fabriken zu schreiben, die neue Objekte instanziieren können.

und sein Beispiel für die Rückgabe von Instanzen aus Fabriken:

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

Ich würde auch argumentieren, dass sein Beispiel überlegen ist, da Sie das newSchlüsselwort nicht in Ihrem Controller verwenden müssen. Es ist in der getInstanceMethode des Dienstes gekapselt .

Jonathan Palumbo
quelle
Danke für das Beispiel. Es gibt also keine Möglichkeit, dass der DI-Container die Abhängigkeit mit einer Instanz erfüllt. Die einzige Möglichkeit besteht darin, die Abhängigkeit von einem Anbieter zu erfüllen, der dann zum Generieren der Instanz verwendet werden kann.
Undistraction
Vielen Dank. Ich bin damit einverstanden, dass es besser ist, als in einem Dienst neu zu verwenden, aber ich denke, dass es immer noch zu kurz kommt. Warum sollte die Klasse, die vom Dienst abhängig ist, wissen oder sich darum kümmern, dass der Dienst, mit dem sie bereitgestellt wird, ein Singleton ist oder nicht? Beide Lösungen können diese Tatsache nicht abstrahieren und drängen etwas, von dem ich glaube, dass es innerhalb des DI-Containers liegen sollte, in die Abhängigkeit. Wenn Sie einen Service erstellen, kann ich feststellen, dass der Ersteller entscheiden kann, ob er als Singleton oder als separate Instanzen bereitgestellt werden soll.
Undistraction
+1 Sehr hilfreich. Ich verwende diesen Ansatz mit ngInfiniteScrollund einem benutzerdefinierten Suchdienst, damit ich die Initialisierung bis zu einem Klickereignis verzögern kann. JSFiddle der ersten Antwort mit der zweiten Lösung aktualisiert: jsfiddle.net/gavinfoley/G5ku5
GFoley83
4
Warum ist die Verwendung des neuen Operators schlecht? Ich habe das Gefühl, wenn Ihr Ziel ein Nicht-Singleton ist, newist die Verwendung deklarativ und es ist einfach, sofort zu erkennen, welche Dienste Singletons sind und welche nicht. Basierend darauf, ob ein Objekt neu erstellt wird.
j_walker_dev
Dies scheint die Antwort zu sein, da es das liefert, wonach die Frage gestellt wurde - insbesondere den Anhang "Aktualisiert".
Lukkea
20

Eine andere Möglichkeit besteht darin, das Serviceobjekt mit zu kopieren angular.extend().

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

und dann zum Beispiel in Ihrem 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
});

Hier ist ein Plunk .

Evgenii
quelle
Wirklich ordentlich! Kennen Sie irgendwelche Gefahren hinter diesem Trick? Immerhin ist es nur eckig. Es erweitert ein Objekt, also denke ich, dass es uns gut gehen sollte. Trotzdem klingt es ein wenig einschüchternd, Dutzende Kopien eines Dienstes zu erstellen.
Vucalur
9

Ich weiß, dass dieser Beitrag bereits beantwortet wurde, aber ich denke immer noch, dass es einige legitime Szenarien gibt, für die Sie einen Nicht-Singleton-Dienst benötigen. Angenommen, es gibt eine wiederverwendbare Geschäftslogik, die von mehreren Controllern gemeinsam genutzt werden kann. In diesem Szenario wäre der beste Ort, um die Logik zu platzieren, ein Dienst, aber was ist, wenn wir einen Zustand in unserer wiederverwendbaren Logik beibehalten müssen? Dann benötigen wir einen Nicht-Singleton-Dienst, der in der App von verschiedenen Controllern gemeinsam genutzt werden kann. So würde ich diese Dienste implementieren:

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

    }]);
msoltany
quelle
Dies ist Jonathan Palumbos Antwort sehr ähnlich, außer dass Jonathan alles in seinem "Aktualisierten" Anhang zusammenfasst.
Lukkea
1
Wollen Sie damit sagen, dass ein Nicht-Singleton-Dienst dauerhaft ist? Und sollte den Zustand behalten, scheint irgendwie umgekehrt.
eran otzap
2

Hier ist mein Beispiel für einen Nicht-Singleton-Dienst. Er stammt von einem ORM, an dem ich arbeite. Im Beispiel zeige ich ein Basismodell (ModelFactory), das Dienste ('Benutzer', 'Dokumente') erben und potenziell erweitern sollen.

In meinem ORM fügt ModelFactory andere Dienste ein, um zusätzliche Funktionen (Abfrage, Persistenz, Schemazuordnung) bereitzustellen, die mithilfe des Modulsystems in einer Sandbox gespeichert werden.

Im Beispiel haben sowohl der Benutzer als auch der Dokumentendienst dieselbe Funktionalität, jedoch ihre eigenen unabhängigen Bereiche.

/*
    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
quelle
1

Angular bietet nur eine Singleton- Service- / Factory-Option. Eine Möglichkeit besteht darin, einen Factory-Service zu haben, der eine neue Instanz für Sie in Ihrem Controller oder anderen Consumer-Instanzen erstellt. Das einzige, was injiziert wird, ist die Klasse, die neue Instanzen erstellt. Dies ist ein guter Ort, um andere Abhängigkeiten einzufügen oder Ihr neues Objekt gemäß der Spezifikation des Benutzers zu initialisieren (Hinzufügen von Diensten oder Konfiguration).

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

}

In Ihrer Consumer-Instanz benötigen Sie dann den Factory-Service und rufen die Build-Methode in der Factory auf, um bei Bedarf eine neue Instanz abzurufen

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

  }

Sie können sehen, dass es die Möglichkeit bietet, bestimmte Dienste zu injizieren, die im Werksschritt nicht verfügbar sind. Auf der Factory-Instanz kann immer eine Injektion erfolgen, die von allen Model-Instanzen verwendet wird.

Hinweis: Ich musste Code entfernen, damit ich möglicherweise Kontextfehler machte. Wenn Sie ein Codebeispiel benötigen, das funktioniert, lassen Sie es mich wissen.

Ich glaube, dass NG2 die Option haben wird, eine neue Instanz Ihres Dienstes an der richtigen Stelle in Ihrem DOM einzufügen, sodass Sie keine eigene Factory-Implementierung erstellen müssen. muss abwarten und sehen :)

Gadi
quelle
netter Ansatz - Ich würde diese $ serviceFactory gerne als npm-Paket sehen. Wenn du magst, kann ich es bauen und dich als Mitwirkenden hinzufügen?
IamStalker
1

Ich glaube, es gibt gute Gründe, eine neue Instanz eines Objekts innerhalb eines Dienstes zu erstellen. Wir sollten auch offen bleiben, anstatt nur zu sagen, wir sollten so etwas niemals tun, aber der Singleton wurde aus einem bestimmten Grund so hergestellt . Controller werden häufig innerhalb des Lebenszyklus der App erstellt und zerstört, die Dienste müssen jedoch dauerhaft sein.

Ich kann mir einen Anwendungsfall vorstellen, in dem Sie einen Workflow haben, z. B. das Akzeptieren einer Zahlung, und Sie mehrere Eigenschaften festgelegt haben, aber jetzt die Zahlungsart ändern müssen, weil die Kreditkarte des Kunden fehlgeschlagen ist und eine andere Form von bereitgestellt werden muss Zahlung. Dies hat natürlich viel mit der Art und Weise zu tun, wie Sie Ihre App erstellen. Sie können alle Eigenschaften für das Zahlungsobjekt zurücksetzen oder eine neue Instanz eines Objekts innerhalb des Dienstes erstellen . Sie möchten jedoch weder eine neue Instanz des Dienstes noch die Seite aktualisieren.

Ich glaube, eine Lösung besteht darin, ein Objekt innerhalb des Dienstes bereitzustellen, von dem Sie eine neue Instanz erstellen und festlegen können. Um jedoch klar zu sein, ist die einzelne Instanz des Dienstes wichtig, da ein Controller möglicherweise viele Male erstellt und zerstört wird, die Dienste jedoch persistent sein müssen. Was Sie suchen, ist möglicherweise keine direkte Methode in Angular, sondern ein Objektmuster, das Sie in Ihrem Service verwalten können.

Als Beispiel habe ich eine Reset- Taste gemacht. (Dies wird nicht getestet, sondern ist nur eine kurze Vorstellung von einem Anwendungsfall zum Erstellen eines neuen Objekts innerhalb eines Dienstes.

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)
            }
        }
    }
}]);
Kreativer Sklave
quelle
0

Hier ist ein weiterer Ansatz für das Problem, mit dem ich sehr zufrieden war, insbesondere in Kombination mit Closure Compiler mit aktivierten erweiterten Optimierungen:

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
quelle