Usługi niebędące singletonami w AngularJS

90

AngularJS jasno stwierdza w swojej dokumentacji, że usługi są singletonami:

AngularJS services are singletons

Wbrew intuicji module.factoryzwraca również instancję Singleton.

Biorąc pod uwagę, że istnieje wiele przypadków użycia usług innych niż pojedyncze, jaki jest najlepszy sposób na zaimplementowanie metody fabrycznej w celu zwrócenia wystąpień usługi, tak aby za każdym razem, gdy ExampleServicezadeklarowano zależność, spełniała ją inna instancja ExampleService?

Nieuwaga
źródło
1
Zakładając, że możesz to zrobić, prawda? Inni programiści Angular nie spodziewaliby się, że fabryka z wstrzyknięciem zależności będzie cały czas zwracać nowe instancje.
Mark Rajcok
1
Myślę, że to kwestia dokumentacji. Myślę, że to wstyd, że nie było to obsługiwane poza bramą, ponieważ obecnie oczekuje się, że wszystkie usługi będą singletonami, ale nie widzę powodu, aby ograniczać je do singletonów.
Undistraction

Odpowiedzi:

44

Nie sądzę, abyśmy kiedykolwiek przywracali fabryczną newfunkcję, ponieważ zaczyna to rozkładać wstrzykiwanie zależności, a biblioteka będzie zachowywać się niezręcznie, szczególnie dla osób trzecich. Krótko mówiąc, nie jestem pewien, czy istnieją jakiekolwiek uzasadnione przypadki użycia usług innych niż pojedyncze.

Lepszym sposobem osiągnięcia tego samego jest użycie fabryki jako interfejsu API do zwracania kolekcji obiektów z dołączonymi do nich metodami pobierającymi i ustawiającymi. Oto pseudokod pokazujący, jak może działać korzystanie z tego rodzaju usługi:

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

To tylko pseudokod do wyszukiwania widżetu według identyfikatora, a następnie umożliwiający zapisanie zmian wprowadzonych w rekordzie.

Oto pseudokod dla usługi:

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

Chociaż nie uwzględniono tego w tym przykładzie, tego rodzaju elastyczne usługi mogą również łatwo zarządzać stanem.


Nie mam teraz czasu, ale jeśli będzie to pomocne, mogę później złożyć prostego Plunkera, aby zademonstrować.

Josh David Miller
źródło
To jest naprawdę interesujące. Przykład byłby naprawdę pomocny. Wielkie dzięki.
Undistraction
To jest interesujące. Wygląda na to, że działałby podobnie do kątownika $resource.
Jonathan Palumbo
@JonathanPalumbo Masz rację - bardzo podobny do ngResource. W rzeczywistości Pedr i ja rozpoczęliśmy tę dyskusję stycznie od innego pytania, w którym zasugerowałem podejście podobne do ngResource. Na przykład tak prosty jak ten, nie ma żadnej korzyści z robienia tego ręcznie - ngResource lub Restangular działałyby płynnie . Ale w przypadkach nie do końca typowych takie podejście ma sens.
Josh David Miller
4
@Pedr Przepraszam, zapomniałem o tym. Oto bardzo proste demo: plnkr.co/edit/Xh6pzd4HDlLRqITWuz8X
Josh David Miller,
15
@JoshDavidMiller czy mógłbyś określić, dlaczego / co miałoby „rozbić wstrzykiwanie zależności i [dlaczego / co] biblioteka będzie zachowywać się niezręcznie”?
okigan
77

Nie jestem do końca pewien, jaki przypadek użycia próbujesz spełnić. Możliwe jest jednak przywrócenie instancji obiektu do fabryki. Powinieneś móc to zmodyfikować, aby dopasować je do swoich potrzeb.

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

Zaktualizowano

Rozważ następujące żądanie dotyczące usług innych niż pojedyncze . W którym Brian Ford zauważa:

Pomysł, że wszystkie usługi są pojedynczymi usługami, nie powstrzymuje Cię przed pisaniem pojedynczych fabryk, które mogą tworzyć instancje nowych obiektów.

i jego przykład powracających instancji z fabryk:

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

Twierdziłbym również, że jego przykład jest lepszy, ponieważ nie musisz używać newsłowa kluczowego w kontrolerze. Jest zamknięty w getInstancemetodzie usługi.

Jonathan Palumbo
źródło
Dzięki za przykład. Dlatego nie ma sposobu, aby kontener DI spełniał zależność z instancją. Jedynym sposobem jest spełnienie zależności od dostawcy, którego można następnie użyć do wygenerowania instancji?
Undistraction
Dzięki. Zgadzam się, że jest to lepsze niż używanie nowego w usłudze, jednak myślę, że nadal nie spełnia. Dlaczego klasa zależna od usługi miałaby wiedzieć lub dbać o to, że usługa, z którą jest ona świadczona, jest singletonem lub nim nie jest? Oba te rozwiązania nie potrafią wyabstrahować tego faktu i wypychają coś, co moim zdaniem powinno być wewnętrzne dla kontenera DI do zależnego. Kiedy tworzysz usługę, widzę szkodę w umożliwieniu twórcy decydowania, czy chciałby, aby była dostarczana jako pojedyncza lub jako oddzielne instancje.
Undistraction
+1 Bardzo pomoc. Używam tego podejścia w połączeniu z ngInfiniteScrollniestandardową usługą wyszukiwania, więc mogę opóźnić inicjalizację do momentu kliknięcia. JSFiddle of 1st answer zaktualizowane o drugie rozwiązanie: jsfiddle.net/gavinfoley/G5ku5
GFoley83
4
Dlaczego używanie nowego operatora jest złe? Wydaje mi się, że jeśli Twoim celem nie jest singleton, to użycie newjest deklaratywne i łatwo jest od razu powiedzieć, które usługi są singletonami, a które nie. Na podstawie tego, czy obiekt jest nowy.
j_walker_dev
wydaje się, że to powinna być odpowiedź, ponieważ zawiera to, o co pytano - zwłaszcza dodatek „Zaktualizowany”.
lukkea
20

Innym sposobem jest skopiowanie obiektu usługi z rozszerzeniem angular.extend().

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

a następnie, na przykład, w kontrolerze

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

Oto plunk .

Evgenii
źródło
Naprawdę fajnie! Czy znasz jakieś niebezpieczeństwa związane z tą sztuczką? W końcu jest to po prostu kątowe, rozciągające obiekt, więc myślę, że powinno być dobrze. Niemniej jednak tworzenie dziesiątek kopii usługi brzmi nieco onieśmielająco.
vucalur
9

Wiem, że na ten post już odpowiedziano, ale nadal uważam, że byłyby pewne uzasadnione scenariusze, w których trzeba mieć usługę inną niż singleton. Załóżmy, że istnieje logika biznesowa wielokrotnego użytku, którą można współdzielić między kilkoma kontrolerami. W tym scenariuszu najlepszym miejscem do umieszczenia logiki byłaby usługa, ale co zrobić, jeśli musimy zachować jakiś stan w naszej logice wielokrotnego użytku? Następnie potrzebujemy usługi innej niż singleton, aby można ją było udostępniać na różnych kontrolerach w aplikacji. Oto jak wdrożyłbym te usługi:

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
źródło
Jest to bardzo podobne do odpowiedzi Jonathana Palumbo, z wyjątkiem tego, że Jonathan podsumowuje wszystko w swoim „Zaktualizowanym” dodatku.
lukkea
1
Czy twierdzisz, że usługa inna niż Singleton byłaby trwała. I powinien zachować stan, wydaje się, że jest na odwrót.
eran otzap
2

Oto mój przykład usługi innej niż singleton, pochodzi z ORM, nad którym pracuję. W przykładzie pokazuję model podstawowy (ModelFactory), który chcę, aby usługi („użytkownicy”, „dokumenty”) dziedziczyły i potencjalnie rozszerzały.

W moim ORM ModelFactory wstrzykuje inne usługi, aby zapewnić dodatkową funkcjonalność (zapytania, trwałość, mapowanie schematu), która jest piaskownicą za pomocą systemu modułowego.

W tym przykładzie zarówno usługa użytkownika, jak i obsługa dokumentów mają tę samą funkcjonalność, ale mają własne, niezależne zakresy.

/*
    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
źródło
1

kątowy daje tylko pojedynczą usługę / opcję fabryczną. jednym ze sposobów jest posiadanie usługi fabrycznej, która utworzy dla Ciebie nową instancję wewnątrz kontrolera lub innych instancji konsumenckich. jedyną rzeczą, która jest wstrzykiwana, jest klasa, która tworzy nowe instancje. to jest dobre miejsce do wstrzyknięcia innych zależności lub zainicjowania nowego obiektu do specyfikacji użytkownika (dodanie usług lub konfiguracji)

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

}

następnie w instancji konsumenckiej potrzebujesz usługi fabrycznej i wywołaj metodę kompilacji w fabryce, aby uzyskać nową instancję, gdy jej potrzebujesz

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

  }

widać, że zapewnia możliwość wstrzyknięcia określonych usług, które nie są dostępne na etapie fabrycznym. zawsze można wykonać wstrzyknięcie w instancji fabryki, która będzie używana przez wszystkie instancje modelu.

Uwaga Musiałem usunąć część kodu, więc mogłem popełnić błędy kontekstu ... jeśli potrzebujesz próbki kodu, który działa, daj mi znać.

Wierzę, że NG2 będzie miało możliwość wstrzyknięcia nowej instancji Twojej usługi we właściwe miejsce w Twoim DOM, więc nie musisz budować własnej implementacji fabrycznej. będę musiał poczekać i zobaczyć :)

Gadi
źródło
fajne podejście - chciałbym zobaczyć ten $ serviceFactory jako pakiet npm. Jeśli chcesz, mogę go zbudować i dodać Cię jako współtwórcę?
IamStalker
1

Uważam, że istnieje dobry powód, aby utworzyć nową instancję obiektu w ramach usługi. Powinniśmy również zachować otwarty umysł, a nie tylko mówić, że nigdy nie powinniśmy robić czegoś takiego, ale singleton powstał w ten sposób z jakiegoś powodu . Kontrolery są tworzone i niszczone często w cyklu życia aplikacji, ale usługi muszą być trwałe.

Przychodzi mi do głowy przypadek użycia, w którym masz pewien przepływ pracy, taki jak akceptowanie płatności i masz ustawionych wiele właściwości, ale musisz teraz zmienić typ płatności, ponieważ karta kredytowa klienta zawiodła i musi podać inną formę Zapłata. Oczywiście ma to wiele wspólnego ze sposobem tworzenia aplikacji. Możesz zresetować wszystkie właściwości obiektu płatności lub możesz utworzyć nową instancję obiektu w usłudze . Ale nie chcesz nowej instancji usługi ani odświeżania strony.

Uważam, że rozwiązaniem jest udostępnienie obiektu w ramach usługi, którego można utworzyć i ustawić nową instancję. Ale żeby było jasne, pojedyncza instancja usługi jest ważna, ponieważ kontroler może być tworzony i niszczony wiele razy, ale usługi wymagają trwałości. To, czego szukasz, może nie być bezpośrednią metodą w Angular, ale wzorcem obiektu, którym możesz zarządzać w swojej usłudze.

Jako przykład zrobiłem przycisk resetowania . (To nie jest testowane, tak naprawdę to tylko krótki pomysł na przypadek użycia do tworzenia nowego obiektu w usłudze.

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)
            }
        }
    }
}]);
Creative Slave
źródło
0

Oto inne podejście do problemu, z którego byłem całkiem zadowolony, szczególnie w połączeniu z Closure Compiler z włączonymi zaawansowanymi optymalizacjami:

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
źródło