Servicios que no son Singleton en AngularJS


90

AngularJS establece claramente en su documentación que los servicios son Singletons:

AngularJS services are singletons

Contrariamente a la intuición, module.factorytambién devuelve una instancia de Singleton.

Dado que hay muchos casos de uso para servicios que no son singleton, ¿cuál es la mejor manera de implementar el método de fábrica para devolver instancias de un servicio, de modo que cada vez que ExampleServicese declare una dependencia, se satisfaga con una instancia diferente de ExampleService?


1
Suponiendo que puedas hacer esto, ¿deberías? Otros desarrolladores de Angular no esperarían que una fábrica inyectada en dependencia devuelva nuevas instancias todo el tiempo.
Mark Rajcok

1
Supongo que es cuestión de documentación. Creo que es una lástima que esto no fuera compatible desde el principio, ya que ahora existe la expectativa de que todos los servicios serán Singleton, pero no veo ninguna razón para limitarlos a Singletons.
distracciones

Respuestas:


44

No creo que nunca deberíamos tener una fábrica que devuelva una newfunción capaz, ya que esto comienza a romper la inyección de dependencia y la biblioteca se comportará de manera incómoda, especialmente para terceros. En resumen, no estoy seguro de que existan casos de uso legítimos para servicios que no sean singleton.

Una mejor manera de lograr lo mismo es usar la fábrica como API para devolver una colección de objetos con métodos getter y setter adjuntos. Aquí hay un pseudocódigo que muestra cómo podría funcionar el uso de ese tipo de servicio:

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

Esto es solo un pseudocódigo para buscar un widget por ID y luego poder guardar los cambios realizados en el registro.

Aquí hay un pseudocódigo para el servicio:

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

Aunque no se incluyen en este ejemplo, estos tipos de servicios flexibles también podrían administrar fácilmente el estado.


No tengo tiempo en este momento, pero si es útil, puedo armar un Plunker simple más tarde para demostrarlo.


Esto es realmente interesante. Un ejemplo sería de gran ayuda. Muchas gracias.
distracciones

Esto es interesante. Parece que funcionaría de manera similar a un angular $resource.
Jonathan Palumbo

@JonathanPalumbo Tienes razón, muy similar a ngResource. De hecho, Pedr y yo comenzamos esta discusión tangencialmente en otra pregunta en la que sugerí adoptar un enfoque similar a ngResource. Para un ejemplo tan simple como este, no hay ninguna ventaja en hacerlo manualmente: ngResource o Restangular funcionarían a la perfección . Pero para casos no tan típicos, este enfoque tiene sentido.
Josh David Miller

4
@Pedr Lo siento, me olvidé de esto. Aquí hay una demostración súper simple: plnkr.co/edit/Xh6pzd4HDlLRqITWuz8X
Josh David Miller

15
@JoshDavidMiller ¿podría especificar por qué / qué "rompería la inyección de dependencia y [por qué / qué] la biblioteca se comportará de manera incómoda"?
okigan

77

No estoy completamente seguro de qué caso de uso está tratando de satisfacer. Pero es posible tener instancias de retorno de fábrica de un objeto. Debería poder modificar esto para adaptarlo a sus necesidades.

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

Actualizado

Considere la siguiente solicitud para servicios que no son singleton . En el que Brian Ford señala:

La idea de que todos los servicios son singleton no le impide escribir fábricas de singleton que puedan crear instancias de nuevos objetos.

y su ejemplo de casos que regresan de las fábricas:

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

También diría que su ejemplo es superior debido al hecho de que no tiene que usar la newpalabra clave en su controlador. Está encapsulado dentro del getInstancemétodo del servicio.


Gracias por el ejemplo. Por lo tanto, no hay forma de que DI Container satisfaga la dependencia con una instancia. ¿La única forma es que satisfaga la dependencia con un proveedor que luego pueda usarse para generar la instancia?
distracciones

Gracias. Estoy de acuerdo en que es mejor que tener que usar nuevo en un servicio, sin embargo, creo que aún se queda corto. ¿Por qué la clase que depende del servicio debe saber o preocuparse de que el servicio que se le brinda sea o no sea Singleton? Ambas soluciones no logran abstraer este hecho y están impulsando algo que creo que debería ser interno al contenedor DI en el dependiente. Cuando crea un Servicio, veo que es perjudicial permitir que el creador decida si desea que se suministre como un singleton o como instancias separadas.
distracciones

+1 Muy ayuda. Estoy usando este enfoque con ngInfiniteScrollun servicio de búsqueda personalizado para poder retrasar la inicialización hasta que algún evento de clic. JSFiddle of 1st answer actualizado con la segunda solución: jsfiddle.net/gavinfoley/G5ku5
GFoley83

4
¿Por qué es malo utilizar el nuevo operador? Siento que si su objetivo no es un singleton, entonces el uso newes declarativo y es fácil saber de inmediato qué servicios son singleton y cuáles no. Según si un objeto se está renovando.
j_walker_dev

Parece que esta debería ser la respuesta porque ofrece lo que se pidió en la pregunta, especialmente el apéndice "Actualizado".
lukkea

20

Otra forma es copiar el objeto de servicio 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); }
  };
});

y luego, por ejemplo, en su controlador

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

Aquí hay un golpe .


¡Realmente ordenado! ¿Conoces algún peligro detrás de este truco? Después de todo, es solo angular. Extender un objeto, así que supongo que deberíamos estar bien. Sin embargo, hacer decenas de copias de un servicio suena un poco intimidante.
vucalur

9

Sé que esta publicación ya ha sido respondida, pero sigo pensando que habría algunos escenarios legítimos en los que necesita tener un servicio que no sea singleton. Digamos que hay alguna lógica empresarial reutilizable que se puede compartir entre varios controladores. En este escenario, el mejor lugar para poner la lógica sería un servicio, pero ¿qué pasa si necesitamos mantener algún estado en nuestra lógica reutilizable? Luego, necesitamos un servicio que no sea singleton, por lo que se puede compartir entre diferentes controladores en la aplicación. Así es como implementaría estos servicios:

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

    }]);

Esto es muy similar a la respuesta de Jonathan Palumbo, excepto que Jonathan encapsula todo con su apéndice "Actualizado".
lukkea

1
¿Está diciendo que un servicio que no sea Singleton sería persistente? Y debería mantener el estado, parece un poco al revés.
eran otzap

2

Aquí está mi ejemplo de un servicio no singleton, es de un ORM en el que estoy trabajando. En el ejemplo, muestro un modelo base (ModelFactory) que quiero que los servicios ('usuarios', 'documentos') hereden y puedan extender.

En mi ORM, ModelFactory inyecta otros servicios para proporcionar funcionalidad adicional (consulta, persistencia, mapeo de esquemas) que se coloca en un espacio aislado utilizando el sistema de módulos.

En el ejemplo, tanto el usuario como el servicio de documentos tienen la misma funcionalidad pero tienen sus propios ámbitos independientes.

/*
    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 solo ofrece una opción de fábrica / servicio singleton . una forma de evitarlo es tener un servicio de fábrica que creará una nueva instancia para usted dentro de su controlador u otras instancias de consumo. lo único que se inyecta es la clase que crea nuevas instancias. este es un buen lugar para inyectar otras dependencias o para inicializar su nuevo objeto a la especificación del usuario (agregando servicios o configuración)

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

}

luego, en su instancia de consumidor, necesita el servicio de fábrica y llama al método de compilación en la fábrica para obtener una nueva instancia cuando la necesite

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

  }

puede ver que brinda la oportunidad de inyectar algunos servicios específicos que no están disponibles en el paso de fábrica. siempre puede hacer que la inyección ocurra en la instancia de fábrica para que la utilicen todas las instancias del modelo.

Tenga en cuenta que tuve que quitar algo de código para que pudiera cometer algunos errores de contexto ... si necesita una muestra de código que funcione, hágamelo saber.

Creo que NG2 tendrá la opción de inyectar una nueva instancia de su servicio en el lugar correcto en su DOM, por lo que no necesita crear su propia implementación de fábrica. Tendré que esperar y ver :)


buen enfoque: me gustaría ver ese $ serviceFactory como un paquete npm. Si lo desea, puedo construirlo y agregarlo como colaborador.
IamStalker

1

Creo que hay una buena razón para crear una nueva instancia de un objeto dentro de un servicio. También deberíamos mantener la mente abierta en lugar de simplemente decir que nunca deberíamos hacer tal cosa, pero el singleton se hizo de esa manera por una razón . Los controladores se crean y destruyen a menudo durante el ciclo de vida de la aplicación, pero los servicios deben ser persistentes.

Puedo pensar en un caso de uso en el que tiene un flujo de trabajo de algún tipo, como aceptar un pago y tiene múltiples propiedades configuradas, pero ahora debe cambiar su tipo de pago porque la tarjeta de crédito del cliente falló y necesita proporcionar una forma diferente de pago. Por supuesto, esto tiene mucho que ver con la forma en que crea su aplicación. Puede restablecer todas las propiedades del objeto de pago o puede crear una nueva instancia de un objeto dentro del servicio . Pero no querrá una nueva instancia del servicio, ni querrá actualizar la página.

Creo que una solución es proporcionar un objeto dentro del servicio del que puede crear una nueva instancia y configurar. Pero, para que quede claro, la instancia única del servicio es importante porque un controlador puede crearse y destruirse muchas veces, pero los servicios necesitan persistencia. Es posible que lo que está buscando no sea un método directo dentro de Angular, sino un patrón de objeto que puede administrar dentro de su servicio.

Como ejemplo, hice un botón de reinicio . (Esto no está probado, en realidad es solo una idea rápida de un caso de uso para crear un nuevo objeto dentro de un servicio.

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

Aquí hay otro enfoque al problema con el que estaba bastante satisfecho, específicamente cuando se usa en combinación con Closure Compiler con optimizaciones avanzadas habilitadas:

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);
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.