Serviços não singleton em AngularJS

90

O AngularJS afirma claramente em sua documentação que os Serviços são Singletons:

AngularJS services are singletons

Contra-intuitivamente, module.factorytambém retorna uma instância de Singleton.

Dado que há muitos casos de uso para serviços não singleton, qual é a melhor maneira de implementar o método de fábrica para retornar instâncias de um serviço, de modo que cada vez que uma ExampleServicedependência é declarada, ela é satisfeita por uma instância diferente de ExampleService?

Undistraction
fonte
1
Supondo que você pudesse fazer isso, não é? Outros desenvolvedores do Angular não esperariam que uma fábrica com injeção de dependência retornasse novas instâncias o tempo todo.
Mark Rajcok
1
Acho que é uma questão de documentação. Acho uma pena que isso não tenha sido suportado desde o início, pois agora há uma expectativa de que todos os Serviços serão Singletons, mas não vejo razão para limitá-los a Singletons.
Undistraction de

Respostas:

44

Eu não acho que deveríamos ter uma fábrica retornando uma newfunção capaz, pois isso começa a quebrar a injeção de dependência e a biblioteca se comportará de maneira estranha, especialmente para terceiros. Resumindo, não tenho certeza se há casos de uso legítimos para serviços não singleton.

A melhor maneira de realizar a mesma coisa é usar a fábrica como uma API para retornar uma coleção de objetos com métodos getter e setter anexados a eles. Aqui está um pseudo-código que mostra como usar esse tipo de serviço pode funcionar:

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

Este é apenas um pseudocódigo para procurar um widget por ID e, em seguida, ser capaz de salvar as alterações feitas no registro.

Aqui estão alguns pseudocódigos para o serviço:

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

Embora não estejam incluídos neste exemplo, esses tipos de serviços flexíveis também podem gerenciar facilmente o estado.


Não tenho tempo agora, mas se for útil, posso montar um Plunker simples mais tarde para demonstrar.

Josh David Miller
fonte
Isso é realmente interessante. Um exemplo seria muito útil. Muito obrigado.
Undistraction
Isto é interessante. Parece que funcionaria semelhante a um angular $resource.
Jonathan Palumbo
@JonathanPalumbo Você está certo - muito semelhante ao ngResource. Na verdade, Pedr e eu começamos essa discussão tangencialmente em outra questão, em que sugeri uma abordagem semelhante ao ngResource. Para um exemplo tão simples como este, não há vantagem em fazê-lo manualmente - ngResource ou Restangular funcionaria perfeitamente . Mas para casos não tão típicos, essa abordagem faz sentido.
Josh David Miller
4
@Pedr Desculpe, esqueci disso. Aqui está uma demonstração super simples: plnkr.co/edit/Xh6pzd4HDlLRqITWuz8X
Josh David Miller
15
@JoshDavidMiller você poderia especificar por que / o que "quebraria a injeção de dependência e [por que / o que] a biblioteca se comportará de maneira estranha"?
okigan
77

Não tenho certeza de qual caso de uso você está tentando satisfazer. Mas é possível que uma fábrica retorne instâncias de um objeto. Você deve ser capaz de modificar isso para atender às suas necessidades.

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

Atualizada

Considere a seguinte solicitação para serviços não singleton . Em que Brian Ford observa:

A ideia de que todos os serviços são singletons não o impede de escrever fábricas de singleton que podem instanciar novos objetos.

e seu exemplo de retorno de instâncias de fábricas:

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

Eu também diria que seu exemplo é superior devido ao fato de que você não precisa usar a newpalavra - chave em seu controlador. Ele é encapsulado no getInstancemétodo do serviço.

Jonathan Palumbo
fonte
Obrigado pelo exemplo. Portanto, não há como fazer com que o DI Container satisfaça a dependência de uma instância. A única maneira é satisfazer a dependência de um provedor que pode então ser usado para gerar a instância?
Undistraction
Obrigado. Concordo que é melhor do que ter que usar novo em um serviço, porém acho que ainda fica aquém. Por que a classe que depende do serviço deve saber ou se importar se o serviço que está sendo fornecido é ou não um Singleton? Ambas as soluções falham em abstrair esse fato e estão empurrando algo que acredito que deveria ser interno ao contêiner de DI para o dependente. Quando você cria um serviço, vejo maléfico em permitir que o criador decida se deseja ou não que ele seja fornecido como um singleton ou como instâncias separadas.
Undistraction
+1 muito ajuda. Estou usando essa abordagem com ngInfiniteScrollum serviço de pesquisa personalizado para que possa atrasar a inicialização até algum evento de clique. JSFiddle da 1ª resposta atualizado com a segunda solução: jsfiddle.net/gavinfoley/G5ku5
GFoley83
4
Por que usar a nova operadora é ruim? Eu sinto que se o seu objetivo não for um singleton, então o uso newé declarativo e é fácil dizer imediatamente quais serviços são singleton e quais não são. Com base em se um objeto está sendo atualizado.
j_walker_dev
parece que esta deveria ser a resposta porque fornece o que a pergunta pedida - especialmente o apêndice "Atualizado".
lukkea
20

Outra maneira é copiar o objeto de serviço com 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 então, por exemplo, em seu 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
});

Aqui está um golpe .

Evgenii
fonte
Muito legal! Você conhece os perigos por trás desse truque? Afinal, é apenas angular.estendendo um objeto, então acho que devemos ficar bem. No entanto, fazer dezenas de cópias de um serviço parece um pouco intimidador.
vucalur
9

Sei que esta postagem já foi respondida, mas ainda acho que haveria alguns cenários legítimos em que você precisa para ter um serviço não único. Digamos que haja alguma lógica de negócios reutilizável que pode ser compartilhada entre vários controladores. Nesse cenário, o melhor lugar para colocar a lógica seria um serviço, mas e se precisarmos manter algum estado em nossa lógica reutilizável? Então, precisamos de um serviço não único para que possa ser compartilhado entre diferentes controladores no aplicativo. É assim que eu implementaria esses serviços:

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
fonte
Isso é muito semelhante à resposta de Jonathan Palumbo, exceto que Jonathan encapsula tudo com seu apêndice "Atualizado".
lukkea
1
Você está dizendo que um serviço não Singleton seria persistente. E deve manter o estado, meio que parece o contrário.
eran otzap
2

Aqui está meu exemplo de um serviço não singleton, é de um ORM em que estou trabalhando. No exemplo, mostro um modelo básico (ModelFactory) que desejo que os serviços ('usuários', 'documentos') herdem e possam estender.

No meu ORM, o ModelFactory injeta outros serviços para fornecer funcionalidade extra (consulta, persistência, mapeamento de esquema) que é colocado em sandbox usando o sistema de módulo.

No exemplo, o usuário e o serviço de documentos têm a mesma funcionalidade, mas têm seus próprios escopos independentes.

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

angular só dá um singleton opção de serviço / fábrica. uma maneira de contornar isso é ter um serviço de fábrica que criará uma nova instância para você dentro de seu controlador ou outras instâncias de consumidor. a única coisa que é injetada é a classe que cria novas instâncias. este é um bom lugar para injetar outras dependências ou para inicializar seu novo objeto com a especificação do usuário (adicionando serviços ou configuração)

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

}

então, em sua instância de consumidor, você precisa do serviço de fábrica e chama o método de construção na fábrica para obter uma nova instância quando precisar

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

  }

você pode ver que ele fornece oportunidade para injetar alguns serviços específicos que não estão disponíveis na etapa de fábrica. você sempre pode fazer a injeção acontecer na instância de fábrica para ser usada por todas as instâncias de Model.

Note que tive que retirar algum código, então posso cometer alguns erros de contexto ... se você precisar de um exemplo de código que funcione, me avise.

Acredito que NG2 terá a opção de injetar uma nova instância de seu serviço no lugar certo em seu DOM, de forma que você não precise construir sua própria implementação de fábrica. Vai ter que esperar e ver :)

Gadi
fonte
boa abordagem - eu gostaria de ver essa $ serviceFactory como um pacote npm. Se quiser, posso criá-lo e adicioná-lo como colaborador?
IamStalker
1

Acredito que haja um bom motivo para criar uma nova instância de um objeto dentro de um serviço. Devemos manter a mente aberta também, em vez de apenas dizer que nunca devemos fazer tal coisa, mas o singleton foi feito assim por uma razão . Os controladores são criados e destruídos frequentemente durante o ciclo de vida do aplicativo, mas os serviços devem ser persistentes.

Posso pensar em um caso de uso em que você tem um fluxo de trabalho de algum tipo, como aceitar um pagamento e tem várias propriedades definidas, mas agora deve alterar o tipo de pagamento porque o cartão de crédito do cliente falhou e ele precisa fornecer uma forma diferente de Forma de pagamento. Claro, isso tem muito a ver com a maneira como você cria seu aplicativo. Você pode redefinir todas as propriedades do objeto de pagamento ou pode criar uma nova instância de um objeto dentro do serviço . Porém, você não deseja uma nova instância do serviço, nem deseja atualizar a página.

Acredito que uma solução é fornecer um objeto dentro do serviço que você pode criar uma nova instância e definir. Mas, só para ficar claro, a única instância do serviço é importante porque um controlador pode ser criado e destruído muitas vezes, mas os serviços precisam de persistência. O que você está procurando pode não ser um método direto no Angular, mas um padrão de objeto que você pode gerenciar dentro do seu serviço.

Por exemplo, fiz um botão de reset . (Isso não foi testado; na verdade, é apenas uma ideia rápida de um caso de uso para a criação de um novo objeto dentro de um serviço.

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
fonte
0

Aqui está outra abordagem para o problema com a qual fiquei bastante satisfeito, especificamente quando usado em combinação com o Closure Compiler com otimizações avançadas 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);
James Wilson
fonte