บริการที่ไม่ใช่ Singleton ใน AngularJS


90

AngularJS ระบุอย่างชัดเจนในเอกสารว่า Services เป็น Singletons:

AngularJS services are singletons

ในทางตรงกันข้ามmodule.factoryยังส่งคืนอินสแตนซ์ Singleton

ระบุว่ามีความอุดมสมบูรณ์ของกรณีการใช้งานสำหรับการให้บริการที่ไม่ใช่เดี่ยวสิ่งที่เป็นวิธีที่ดีที่สุดที่จะใช้วิธีการที่โรงงานจะกลับมากรณีของการบริการเพื่อให้แต่ละครั้งที่มีการExampleServiceพึ่งพาการประกาศก็เป็นที่พอใจโดยอินสแตนซ์ที่แตกต่างกันExampleService?


1
สมมติว่าคุณสามารถทำได้คุณควร? นักพัฒนา Angular คนอื่น ๆ คงไม่คาดหวังว่าโรงงานที่ฉีดพึ่งพาจะส่งคืนอินสแตนซ์ใหม่ตลอดเวลา
Mark Rajcok

1
ฉันเดาว่าเป็นเรื่องของเอกสาร ฉันคิดว่ามันน่าเสียดายที่ไม่ได้รับการสนับสนุนจากประตูเนื่องจากตอนนี้มีความคาดหวังว่าบริการทั้งหมดจะเป็น Singletons แต่ฉันไม่เห็นเหตุผลที่จะ จำกัด พวกเขาไว้ที่ Singletons
Undistraction

คำตอบ:


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

นี่เป็นเพียงรหัสหลอกสำหรับค้นหาวิดเจ็ตด้วย ID จากนั้นจึงสามารถบันทึกการเปลี่ยนแปลงที่เกิดขึ้นกับเรกคอร์ดได้

นี่คือรหัสหลอกสำหรับบริการ:

.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

เรื่องนี้น่าสนใจ $resourceดูเหมือนว่ามันจะทำงานคล้ายกับเชิงมุม
Jonathan Palumbo

@JonathanPalumbo คุณพูดถูก - คล้ายกับ ngResource มาก ในความเป็นจริง Pedr และฉันเริ่มต้นการสนทนานี้ด้วยกันในคำถามอื่นที่ฉันแนะนำให้ใช้แนวทางที่คล้ายกับ ngResource ยกตัวอย่างง่ายๆเช่นนี้ไม่มีประโยชน์ที่จะทำด้วยตนเอง - ngResource หรือRestangularจะทำงานว่ายน้ำได้ แต่สำหรับกรณีที่ไม่ปกติอย่างสมบูรณ์วิธีนี้ก็สมเหตุสมผล
Josh David Miller

4
@Pedr ขออภัยฉันลืมเรื่องนี้ไป นี่คือการสาธิตที่ง่ายสุด ๆ : plnkr.co/edit/Xh6pzd4HDlLRqITWuz8X
Josh David Miller

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

อัปเดตแล้ว

พิจารณาคำขอต่อไปนี้สำหรับบริการที่ไม่ใช่ซิงเกิลตันบริการที่ไม่ใช่เดี่ยวซึ่ง Brian Ford กล่าวว่า:

แนวคิดที่ว่าบริการทั้งหมดเป็นแบบซิงเกิลตันไม่ได้หยุดคุณจากการเขียนโรงงานซิงเกิลตันที่สามารถสร้างอินสแตนซ์อ็อบเจ็กต์ใหม่ได้

และตัวอย่างของเขาในการส่งคืนอินสแตนซ์จากโรงงาน:

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

ฉันจะโต้แย้งว่าตัวอย่างของเขาดีกว่าเนื่องจากคุณไม่จำเป็นต้องใช้newคำหลักในตัวควบคุมของคุณ มันถูกห่อหุ้มภายในgetInstanceวิธีการของบริการ


ขอบคุณสำหรับตัวอย่าง ดังนั้นจึงไม่มีวิธีใดที่จะทำให้ DI Container ตอบสนองการพึ่งพากับอินสแตนซ์ได้ วิธีเดียวคือทำให้มันเป็นไปตามการพึ่งพากับผู้ให้บริการซึ่งสามารถใช้เพื่อสร้างอินสแตนซ์ได้หรือไม่?
Undistraction

ขอบคุณ. ฉันยอมรับว่าดีกว่าต้องใช้บริการใหม่อย่างไรก็ตามฉันคิดว่ามันยังไม่ดี เหตุใดชั้นเรียนที่ขึ้นอยู่กับบริการจึงควรทราบหรือสนใจว่าบริการที่ให้บริการนั้นเป็นหรือไม่ใช่ Singleton โซลูชันทั้งสองนี้ล้มเหลวในการสรุปข้อเท็จจริงนี้และกำลังผลักดันบางสิ่งที่ฉันเชื่อว่าควรอยู่ภายในไปยังคอนเทนเนอร์ DI เข้าสู่การพึ่งพา เมื่อคุณสร้างบริการฉันเห็นว่าเป็นอันตรายในการอนุญาตให้ครีเอเตอร์ตัดสินใจว่าพวกเขาต้องการให้บริการเป็นแบบซิงเกิลหรือแบบแยกต่างหาก
Undistraction

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

นี่คือตีเสียงดังปัง


เรียบร้อยจริงๆ! คุณรู้หรือไม่ว่ามีอันตรายอยู่เบื้องหลังกลอุบายนี้หรือไม่? ท้ายที่สุดมันเป็นแค่เชิงมุมการขยายวัตถุดังนั้นฉันคิดว่าเราน่าจะสบายดี อย่างไรก็ตามการทำสำเนาบริการหลายสิบชุดฟังดูน่ากลัวเล็กน้อย
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));

    }]);

สิ่งนี้คล้ายกับคำตอบของ Jonathan Palumbo มากยกเว้นโจนาธานจะสรุปทุกอย่างด้วยภาคผนวก "อัปเดต" ของเขา
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

เชิงมุมให้ตัวเลือกบริการเดี่ยว / โรงงานเท่านั้น วิธีหนึ่งคือการมีบริการจากโรงงานที่จะสร้างอินสแตนซ์ใหม่สำหรับคุณภายในคอนโทรลเลอร์หรืออินสแตนซ์สำหรับผู้บริโภคอื่น ๆ สิ่งเดียวที่ถูกฉีดเข้าไปคือคลาสที่สร้างอินสแตนซ์ใหม่ นี่เป็นสถานที่ที่ดีในการแทรกการอ้างอิงอื่น ๆ หรือเพื่อเริ่มต้นอ็อบเจ็กต์ใหม่ของคุณตามข้อกำหนดของผู้ใช้ (เพิ่มบริการหรือกำหนดค่า)

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

  }

คุณจะเห็นว่ามันให้โอกาสในการฉีดบริการเฉพาะบางอย่างที่ไม่มีในขั้นตอนจากโรงงาน คุณสามารถมีการฉีดเกิดขึ้นบนอินสแตนซ์โรงงานเพื่อใช้กับอินสแตนซ์ Model ทั้งหมดได้เสมอ

โปรดทราบว่าฉันต้องตัดโค้ดบางส่วนออกดังนั้นฉันอาจทำผิดบริบทบางอย่าง ... หากคุณต้องการตัวอย่างโค้ดที่ใช้งานได้โปรดแจ้งให้เราทราบ

ฉันเชื่อว่า 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);
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.