Не-одиночные сервисы в 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));

    }]);
MSoltany
источник
Это очень похоже на ответ Джонатана Палумбо, за исключением того, что Джонатан инкапсулирует все в своем «Обновленном» приложении.
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);
Джеймс Уилсон
источник