Не-одиночные сервисы в AngularJS


90

AngularJS четко заявляет в своей документации, что службы являются одиночными:

AngularJS services are singletons

Как ни странно, module.factoryтакже возвращает экземпляр Singleton.

Учитывая, что существует множество вариантов использования для не-одноэлементных служб, как лучше всего реализовать фабричный метод для возврата экземпляров службы, чтобы каждый раз, когда ExampleServiceобъявляется зависимость, она удовлетворялась другим экземпляром ExampleService?


1
Если вы могли бы это сделать, не так ли? Другие разработчики Angular не ожидали бы, что фабрика с внедрением зависимостей будет постоянно возвращать новые экземпляры.
Марк Райкок

1
Думаю, это вопрос документации. Я думаю, что это позор, что это не было поддержано сразу, поскольку теперь ожидается, что все службы будут синглетонами, но я не вижу причин ограничивать их синглтонами.
Undistraction

Ответы:


44

Я не думаю, что у нас когда-либо должна быть фабрика, возвращающая newработоспособную функцию, поскольку это начинает разрушать внедрение зависимостей, и библиотека будет вести себя неуклюже, особенно для третьих сторон. Короче говоря, я не уверен, что существуют какие-либо законные варианты использования не-одноэлементных служб.

Лучший способ добиться того же - использовать фабрику в качестве API для возврата коллекции объектов с прикрепленными к ним методами получения и установки. Вот какой-то псевдокод, показывающий, как может работать такой сервис:

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

Это просто псевдокод для поиска виджета по идентификатору и возможности сохранения изменений, внесенных в запись.

Вот какой-то псевдокод сервиса:

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

Хотя эти виды гибких сервисов не включены в этот пример, они также могут легко управлять состоянием.


У меня сейчас нет времени, но если это будет полезно, я могу позже собрать простой Plunker для демонстрации.


Это действительно интересно. Пример был бы действительно полезен. Большое спасибо.
Undistraction

Это интересно. Похоже, что он будет работать аналогично angular $resource.
Джонатан Палумбо

@JonathanPalumbo Вы правы - очень похоже на ngResource. Фактически, мы с Педром начали это обсуждение косвенно, с другого вопроса, где я предложил использовать подход, аналогичный ngResource. Для такого простого примера нет никаких преимуществ делать это вручную - ngResource или Restangular будут работать без проблем . Но для случаев, не столь типичных, такой подход имеет смысл.
Джош Дэвид Миллер

4
@Pedr Простите, я забыл об этом. Вот супер-простая демонстрация: plnkr.co/edit/Xh6pzd4HDlLRqITWuz8X
Джош Дэвид Миллер,

15
@JoshDavidMiller, не могли бы вы указать, почему / что «нарушит внедрение зависимостей и [почему / что] библиотека будет вести себя неуклюже»?
окиган 08

77

Я не совсем уверен, какой вариант использования вы пытаетесь удовлетворить. Но возможно, чтобы фабрика возвращала экземпляры объекта. Вы должны иметь возможность изменить это в соответствии со своими потребностями.

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

Обновлено

Рассмотрим следующий запрос для не-одиночных служб . В котором Брайан Форд отмечает:

Идея о том, что все службы являются синглтонами, не мешает вам писать одноэлементные фабрики, которые могут создавать экземпляры новых объектов.

и его пример возврата экземпляров с фабрик:

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

Я бы также сказал, что его пример лучше, потому что вам не нужно использовать newключевое слово в вашем контроллере. Он инкапсулирован в getInstanceметоде службы.


Спасибо за пример. Таким образом, нет никакого способа, чтобы контейнер DI удовлетворял зависимости с экземпляром. Единственный способ - удовлетворить зависимость от поставщика, который затем можно использовать для создания экземпляра?
Undistraction

Спасибо. Я согласен с тем, что это лучше, чем использовать новое в сервисе, однако я думаю, что этого все еще недостаточно. Почему класс, зависящий от службы, должен знать или заботиться о том, является ли предоставляемая служба синглтоном? Оба этих решения не могут абстрагироваться от этого факта и подталкивают то, что, по моему мнению, должно быть внутренним для контейнера DI, в зависимое. Когда вы создаете Службу, я вижу вред, позволяющий создателю решать, хотят ли они, чтобы она предоставлялась как синглтон или как отдельные экземпляры.
Undistraction

+1 Очень помогло. Я использую этот подход со ngInfiniteScrollслужбой настраиваемого поиска, поэтому я могу отложить инициализацию до какого-либо события щелчка. JSFiddle 1-го ответа обновлен вторым решением: jsfiddle.net/gavinfoley/G5ku5
GFoley83

4
Почему использование оператора new - это плохо? Мне кажется, что если ваша цель - не синглтон, тогда использование newбудет декларативным, и сразу легко сказать, какие службы являются синглтонами, а какие нет. В зависимости от того, обновляется ли объект.
j_walker_dev

похоже, что это должен быть ответ, потому что он дает то, о чем задан вопрос, особенно приложение «Обновлено».
lukkea

20

Другой способ - скопировать служебный объект с помощью angular.extend().

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

а потом, например, в вашем контроллере

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

Вот такой кусок .


Действительно аккуратно! Вы знаете, какие опасности стоит за этим трюком? В конце концов, это просто angular.extend'ing объекта, так что я думаю, у нас все будет хорошо. Тем не менее, создание десятков копий сервиса звучит немного устрашающе.
vucalur

9

Я знаю, что на этот пост уже был дан ответ, но я все же думаю, что могут быть некоторые законные сценарии, в которых вам понадобится не-одиночная служба. Допустим, есть бизнес-логика многократного использования, которая может использоваться несколькими контроллерами. В этом сценарии лучшим местом для размещения логики будет сервис, но что, если нам нужно сохранить какое-то состояние в нашей повторно используемой логике? Затем нам нужна не-одноэлементная служба, чтобы ее можно было использовать на разных контроллерах в приложении. Вот как я бы реализовал эти услуги:

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

    }]);

Это очень похоже на ответ Джонатана Палумбо, за исключением того, что Джонатан инкапсулирует все в своем «Обновленном» приложении.
lukkea

1
Вы хотите сказать, что служба, отличная от Singleton, будет постоянной? И должно сохранять состояние, вроде как наоборот.
eran otzap

2

Вот мой пример службы, отличной от singleton, это из ORM, над которым я работаю. В этом примере я показываю базовую модель (ModelFactory), которую я хочу, чтобы службы («пользователи», «документы») наследовали и потенциально расширяли.

В моем ORM ModelFactory внедряет другие службы для обеспечения дополнительной функциональности (запрос, постоянство, отображение схемы), которая изолирована с помощью модульной системы.

В этом примере и пользователь, и служба документов имеют одинаковую функциональность, но имеют свои собственные независимые области действия.

/*
    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 предоставляет только одноэлементный сервис / заводскую опцию. один из способов - создать заводскую службу, которая создаст для вас новый экземпляр внутри вашего контроллера или других экземпляров-потребителей. единственное, что вводится, - это класс, создающий новые экземпляры. это хорошее место для внедрения других зависимостей или для инициализации вашего нового объекта в соответствии со спецификацией пользователя (добавление служб или конфигурации)

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

}

тогда в вашем потребительском экземпляре вам понадобится сервис фабрики и вызовите метод сборки на фабрике, чтобы получить новый экземпляр, когда он вам нужен

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

  }

вы можете видеть, что он предоставляет возможность внедрить некоторые определенные службы, которые недоступны на заводском этапе. вы всегда можете выполнить инъекцию в экземпляр фабрики, который будет использоваться всеми экземплярами модели.

Обратите внимание, мне пришлось вырезать часть кода, чтобы я мог сделать некоторые контекстные ошибки ... если вам нужен образец кода, который работает, дайте мне знать.

Я считаю, что NG2 будет иметь возможность внедрить новый экземпляр вашего сервиса в нужное место в вашей DOM, чтобы вам не нужно было создавать собственную реализацию фабрики. придется подождать и посмотреть :)


хороший подход - я хотел бы видеть этот $ serviceFactory как пакет npm. Если хотите, я могу создать его и добавить вас в качестве участника?
IamStalker

1

Я считаю, что есть веская причина для создания нового экземпляра объекта в службе. Мы также должны быть непредвзятыми, а не просто говорить, что мы никогда не должны делать такие вещи, но синглтон был создан таким по какой-то причине . Контроллеры часто создаются и уничтожаются в течение жизненного цикла приложения, но службы должны быть постоянными.

Я могу представить себе вариант использования, в котором у вас есть какой-то рабочий процесс, например, принятие платежа, и у вас есть несколько установленных свойств, но теперь необходимо изменить их тип платежа, потому что кредитная карта клиента не работает, и им нужно предоставить другую форму оплата. Конечно, это во многом связано с тем, как вы создаете свое приложение. Вы можете сбросить все свойства объекта платежа или создать новый экземпляр объекта внутри службы . Но вам не нужен новый экземпляр службы и вы не захотите обновлять страницу.

Я считаю, что решение предоставляет объект внутри службы, который вы можете создать и установить. Но для ясности: один экземпляр службы важен, потому что контроллер может создаваться и уничтожаться много раз, но службы нуждаются в постоянстве. То, что вы ищете, может быть не прямым методом в Angular, а шаблоном объекта, которым вы можете управлять внутри своей службы.

В качестве примера я сделал кнопку сброса . (Это не проверено, это просто краткое представление о сценарии использования для создания нового объекта в службе.

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

Вот еще один подход к проблеме, который меня вполне удовлетворил, особенно при использовании в сочетании с Closure Compiler с включенной расширенной оптимизацией:

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);
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.