Послуги, що не стосуються одного, у AngularJS


90

AngularJS чітко зазначає у своїй документації, що Служби є одиночними:

AngularJS services are singletons

Контрінтуїтивно, module.factoryтакож повертає екземпляр Singleton.

Враховуючи те, що існує безліч варіантів використання неслужбових служб, який найкращий спосіб реалізувати заводський метод для повернення екземплярів Служби, щоб кожен раз, коли ExampleServiceоголошується залежність, вона задовольнялася іншим екземпляром ExampleService?


1
Припускаючи, що ти міг би це зробити, чи не так? Інші розробники Angular не очікували, що фабрика з інжекцією залежностей постійно повертатиме нові екземпляри.
Марк Райчок

1
Я думаю, це питання документації. Я думаю, що це ганьба, що цього не підтримали з-за меж, оскільки зараз очікується, що всі Служби будуть одноосібниками, але я не бачу підстав обмежувати їх одноосібниками.
Невідволікання уваги

Відповіді:


44

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

Кращий спосіб зробити те саме - використовувати фабрику як API для повернення колекції об’єктів із приєднаними до них методами getter та setter. Ось деякий псевдокод, який показує, як може працювати таке обслуговування:

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

Хоча ці види гнучких послуг не включені в цей приклад, вони також можуть легко керувати станом.


Зараз у мене немає часу, але якщо це буде корисно, я можу зібрати простий Планкер пізніше для демонстрації.


Це справді цікаво. Приклад може бути дуже корисним. Дуже дякую.
Невідволікання уваги

Це цікаво. Здається, він би функціонував подібно до кутового $resource.
Джонатан Палумбо

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

4
@Pedr Вибачте, я забув про це. Ось надзвичайно проста демонстрація: plnkr.co/edit/Xh6pzd4HDlLRqITWuz8X
Джош Девід Міллер

15
@JoshDavidMiller, чи можете ви вказати, чому / що "розбиватиме введення залежностей і [чому / що] бібліотека буде поводитися незручно"?
okigan

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 задовольнив залежність за допомогою екземпляра. Єдиний спосіб - це задовольнити залежність від постачальника, який потім може бути використаний для генерації екземпляра?
Невідволікання уваги

Дякую. Я погоджуюсь з тим, що це краще, ніж використовувати нове в службі, однак я думаю, що це все одно не вистачає. Чому клас, який залежить від послуги, повинен знати чи піклуватися про те, що послуга, з якою вона постачається, є Singleton чи ні? Обидва ці рішення не можуть абстрагувати цей факт і проштовхують те, що, на мою думку, повинно бути внутрішнім для контейнера DI в залежне. Коли ви створюєте Службу, я бачу шкоду, дозволяючи творцеві вирішувати, чи хотіли б вони, щоб вона була надана як однотонна або як окрема копія.
Невідволікання уваги

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

4
Чому використання нового оператора погано? Я відчуваю, що якщо ваша мета - не синглтон, тоді використання 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

Ось мій приклад несінглтон-сервісу, це від 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

Ось ще один підхід до проблеми, який мене цілком задовольнив, зокрема, коли він використовується у поєднанні з компілятором закриття з увімкненою розширеною оптимізацією:

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.