Dịch vụ Non-Singleton trong AngularJS


90

AngularJS tuyên bố rõ ràng trong tài liệu của mình rằng Dịch vụ là Singleton:

AngularJS services are singletons

Ngược lại, module.factorycũng trả về một cá thể Singleton.

Cho rằng có rất nhiều trường hợp sử dụng cho các dịch vụ không phải singleton, cách tốt nhất để triển khai phương thức gốc để trả về các trường hợp của một Dịch vụ, để mỗi khi một ExampleServicephụ thuộc được khai báo, nó được thỏa mãn bởi một trường hợp khác ExampleService?


1
Giả sử bạn có thể làm điều này, bạn có nên không? Các nhà phát triển Angular khác sẽ không mong đợi một nhà máy có sự phụ thuộc vào sẽ luôn trả lại các phiên bản mới.
Mark Rajcok

1
Tôi đoán đó là một vấn đề cho tài liệu. Tôi nghĩ thật tiếc khi điều này không được hỗ trợ ngay từ đầu vì hiện tại có kỳ vọng rằng tất cả Dịch vụ sẽ là Singleton, nhưng tôi không có lý do gì để giới hạn chúng ở Singleton.
Undistraction

Câu trả lời:


44

Tôi không nghĩ rằng chúng ta nên có một nhà máy trả về một newchức năng có thể vì điều này bắt đầu phá vỡ việc tiêm phụ thuộc và thư viện sẽ hoạt động một cách khó xử, đặc biệt là đối với các bên thứ ba. Tóm lại, tôi không chắc có bất kỳ trường hợp sử dụng hợp pháp nào cho các thiết bị không phải singleton.

Một cách tốt hơn để hoàn thành điều tương tự là sử dụng factory như một API để trả về một tập hợp các đối tượng có các phương thức getter và setter được đính kèm với chúng. Dưới đây là một số mã giả cho thấy cách sử dụng loại dịch vụ đó có thể hoạt động:

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

Đây chỉ là mã giả để tìm kiếm tiện ích theo ID và sau đó có thể lưu các thay đổi được thực hiện vào bản ghi.

Đây là một số mã giả cho dịch vụ:

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

Mặc dù không được bao gồm trong ví dụ này, các loại dịch vụ linh hoạt này cũng có thể dễ dàng quản lý trạng thái.


Tôi không có thời gian ngay bây giờ, nhưng nếu nó hữu ích, tôi có thể tập hợp một Plunker đơn giản sau để chứng minh.


Điều này thực sự thú vị. Một ví dụ sẽ thực sự hữu ích. Cảm ơn rất nhiều.
Undistraction

Hay đấy. Có vẻ như nó sẽ hoạt động tương tự như một góc $resource.
Jonathan Palumbo

@JonathanPalumbo Bạn nói đúng - rất giống với ngResource. Trên thực tế, Pedr và tôi bắt đầu cuộc thảo luận này theo cách tiếp theo trong một câu hỏi khác, nơi tôi đề xuất thực hiện một cách tiếp cận tương tự như ngResource. Đối với một ví dụ quá đơn giản như thế này, không có lợi thế khi làm thủ công - ngResource hoặc Restangular sẽ hoạt động một cách tự nhiên. Nhưng đối với những trường hợp không hoàn toàn điển hình, cách tiếp cận này có ý nghĩa.
Josh David Miller

4
@Pedr Xin lỗi, tôi đã quên điều này. Đây là một bản demo siêu đơn giản: plnkr.co/edit/Xh6pzd4HDlLRqITWuz8X
Josh David Miller

15
@JoshDavidMiller, bạn có thể chỉ định lý do tại sao / điều gì sẽ "phá vỡ việc tiêm phụ thuộc và [tại sao / cái gì] thư viện sẽ hoạt động một cách khó xử"?
okigan

77

Tôi không hoàn toàn chắc chắn bạn đang cố gắng đáp ứng trường hợp sử dụng nào. Nhưng có thể có các trường hợp trả về ban đầu của một đối tượng. Bạn sẽ có thể sửa đổi điều này cho phù hợp với nhu cầu của mình.

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

Đã cập nhật

Hãy xem xét yêu cầu sau đối với các dịch vụ không phải singleton . Trong đó Brian Ford lưu ý:

Ý tưởng rằng tất cả các dịch vụ đều là đơn lẻ không ngăn bạn viết các nhà máy đơn lẻ có thể khởi tạo các đối tượng mới.

và ví dụ của anh ấy về việc trả lại các bản sao từ các nhà máy:

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

Tôi cũng cho rằng ví dụ của anh ấy là tốt hơn vì thực tế là bạn không phải sử dụng newtừ khóa trong bộ điều khiển của mình. Nó được đóng gói trong getInstancephương thức của dịch vụ.


Cảm ơn vì ví dụ. Vì vậy, không có cách nào để DI Container thỏa mãn sự phụ thuộc với một thể hiện. Cách duy nhất là làm cho nó thỏa mãn sự phụ thuộc với một trình cung cấp mà sau đó có thể được sử dụng để tạo phiên bản?
Undistraction

Cảm ơn. Tôi đồng ý rằng tốt hơn là phải sử dụng mới trong một dịch vụ, tuy nhiên tôi nghĩ rằng nó vẫn còn thiếu sót. Tại sao lớp phụ thuộc vào dịch vụ phải biết hoặc quan tâm rằng dịch vụ mà nó đang được cung cấp có phải là Singleton hay không? Cả hai giải pháp này đều không trừu tượng hóa thực tế này và đang đẩy một cái gì đó mà tôi tin rằng nên nằm bên trong vùng chứa DI vào phần phụ thuộc. Khi bạn tạo Dịch vụ, tôi thấy có hại trong việc cho phép người tạo quyết định xem họ muốn cung cấp dịch vụ đó dưới dạng đơn lẻ hay dưới dạng các bản sao riêng biệt.
Undistraction

+1 Rất hữu ích. Tôi đang sử dụng cách tiếp cận này với ngInfiniteScrollvà một dịch vụ tìm kiếm tùy chỉnh để tôi có thể trì hoãn việc khởi chạy cho đến khi có một số sự kiện nhấp chuột. JSFiddle của câu trả lời đầu tiên được cập nhật với giải pháp thứ hai: jsfiddle.net/gavinfoley/G5ku5
GFoley83

4
Tại sao sử dụng toán tử mới không tốt? Tôi cảm thấy như nếu mục tiêu của bạn là không phải là singleton thì việc sử dụng newlà khai báo và rất dễ dàng để biết ngay dịch vụ nào là singleton và đâu là không. Dựa trên việc một đối tượng đang được tạo mới.
j_walker_dev

Có vẻ như đây phải là câu trả lời vì nó cung cấp những gì câu hỏi yêu cầu - đặc biệt là phụ lục "Cập nhật".
lukkea

20

Một cách khác là sao chép đối tượng dịch vụ với angular.extend().

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

và sau đó, ví dụ, trong bộ điều khiển của bạn

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

Đây là một plunk .


Thực sự gọn gàng! Bạn có biết bất kỳ nguy hiểm nào đằng sau trò lừa này? Rốt cuộc, nó chỉ là một đối tượng có góc cạnh, tạo ra một đối tượng, vì vậy tôi đoán chúng ta sẽ ổn thôi. Tuy nhiên, việc tạo ra hàng chục bản sao của một dịch vụ nghe có vẻ hơi đáng sợ.
vucalur

9

Tôi biết bài đăng này đã được trả lời nhưng tôi vẫn nghĩ rằng sẽ có một số trường hợp hợp pháp mà bạn cần phải có dịch vụ không phải singleton. Giả sử có một số logic nghiệp vụ có thể tái sử dụng có thể được chia sẻ giữa một số bộ điều khiển. Trong trường hợp này, nơi tốt nhất để đặt logic sẽ là một dịch vụ, nhưng nếu chúng ta cần giữ một số trạng thái trong logic có thể tái sử dụng của mình thì sao? Sau đó, chúng tôi cần dịch vụ non-singleton để có thể được chia sẻ trên các bộ điều khiển khác nhau trong ứng dụng. Đây là cách tôi sẽ triển khai các dịch vụ này:

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

    }]);

Điều này rất giống với câu trả lời của Jonathan Palumbo ngoại trừ việc Jonathan gói gọn mọi thứ bằng phụ lục "Đã cập nhật" của mình.
lukkea

1
Bạn có nói rằng một dịch vụ không phải của Singleton sẽ tồn tại lâu dài. Và nên giữ trạng thái, có vẻ như ngược lại.
eran otzap

2

Đây là ví dụ của tôi về một dịch vụ non-singleton, Nó từ một ORM tôi đang làm việc. Trong ví dụ, tôi hiển thị một Mô hình cơ sở (ModelFactory) mà tôi muốn các dịch vụ ('người dùng', 'tài liệu') kế thừa và mở rộng tiềm năng.

Trong ORM ModelFactory của tôi chèn các dịch vụ khác để cung cấp chức năng bổ sung (truy vấn, tính liên tục, ánh xạ lược đồ) được đóng hộp cát bằng cách sử dụng hệ thống mô-đun.

Trong ví dụ này, cả người dùng và dịch vụ tài liệu đều có chức năng giống nhau nhưng có phạm vi độc lập của riêng họ.

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

góc chỉ cung cấp cho một singleton lựa chọn dịch vụ / nhà máy. một cách giải quyết là có một dịch vụ nhà máy sẽ xây dựng một phiên bản mới cho bạn bên trong bộ điều khiển của bạn hoặc các phiên bản tiêu dùng khác. thứ duy nhất được đưa vào là lớp tạo ra các thể hiện mới. đây là một nơi tốt để đưa các phụ thuộc khác vào hoặc khởi tạo đối tượng mới của bạn vào đặc tả của người dùng (thêm dịch vụ hoặc cấu hình)

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

}

thì trong trường hợp người tiêu dùng của bạn, bạn cần dịch vụ nhà máy và gọi phương thức xây dựng trên nhà máy để nhận bản sao mới khi bạn cần

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

  }

bạn có thể thấy nó cung cấp cơ hội để đưa một số dịch vụ cụ thể không có sẵn trong bước xuất xưởng. bạn luôn có thể có quá trình tiêm xảy ra trên phiên bản gốc để tất cả các phiên bản Model sử dụng.

Lưu ý rằng tôi đã phải loại bỏ một số mã vì vậy tôi có thể mắc một số lỗi ngữ cảnh ... nếu bạn cần một mẫu mã hoạt động, hãy cho tôi biết.

Tôi tin rằng NG2 sẽ có tùy chọn để đưa một phiên bản mới của dịch vụ của bạn vào đúng vị trí trong DOM của bạn, do đó bạn không cần phải xây dựng triển khai nhà máy của riêng mình. sẽ phải chờ xem :)


cách tiếp cận tốt - Tôi muốn xem $ serviceFactory đó là một gói npm. Nếu bạn thích tôi có thể xây dựng nó và thêm bạn làm cộng tác viên?
IamStalker

1

Tôi tin rằng có lý do chính đáng để tạo một phiên bản mới của một đối tượng trong một dịch vụ. Chúng ta cũng nên giữ một tâm trí cởi mở hơn là chỉ nói rằng chúng ta không bao giờ nên làm một điều như vậy, nhưng singleton được tạo ra như vậy là có lý do . Bộ điều khiển được tạo và hủy thường xuyên trong vòng đời của ứng dụng, nhưng các dịch vụ phải hoạt động bền bỉ.

Tôi có thể nghĩ đến một trường hợp sử dụng trong đó bạn có một số loại quy trình công việc, chẳng hạn như chấp nhận một khoản thanh toán và bạn có nhiều thuộc tính được đặt, nhưng bây giờ phải thay đổi hình thức thanh toán của họ vì thẻ tín dụng của khách hàng bị lỗi và họ cần cung cấp một hình thức khác thanh toán. Tất nhiên, điều này liên quan nhiều đến cách bạn tạo ứng dụng của mình. Bạn có thể đặt lại tất cả các thuộc tính cho đối tượng thanh toán hoặc bạn có thể tạo một phiên bản mới của một đối tượng trong dịch vụ . Tuy nhiên, bạn sẽ không muốn có phiên bản mới của dịch vụ, cũng như không muốn làm mới trang.

Tôi tin rằng một giải pháp là cung cấp một đối tượng trong dịch vụ mà bạn có thể tạo một phiên bản và thiết lập mới. Tuy nhiên, cần phải nói rõ rằng, một phiên bản duy nhất của dịch vụ rất quan trọng vì một bộ điều khiển có thể được tạo và hủy nhiều lần, nhưng các dịch vụ này cần phải bền bỉ. Những gì bạn đang tìm kiếm có thể không phải là một phương thức trực tiếp trong Angular, mà là một mẫu đối tượng mà bạn có thể quản lý bên trong dịch vụ của mình.

Ví dụ, tôi có một nút đặt lại được tạo . (Đây không phải là thử nghiệm, nó thực sự chỉ là một ý tưởng nhanh về một ca sử dụng để tạo một đối tượng mới trong một dịch vụ.

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

Đây là một cách tiếp cận khác cho vấn đề mà tôi khá hài lòng, cụ thể là khi được sử dụng kết hợp với Trình biên dịch đóng cửa có bật tính năng tối ưu hóa nâng cao:

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);
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.