angular / angular.js

AngularJS - HTML enhanced for web apps!
https://angularjs.org
MIT License
58.81k stars 27.49k forks source link

services / factories, what is going on? #14948

Closed MrOutput closed 8 years ago

MrOutput commented 8 years ago

I am not sure if the angular team had mixed ideas about services and factories. But the code and ideas mentioned in the services section don't add up, not even on angular's site. If service registration takes a constructor function as the second argument, and its suppose to be invoked as a constructor would, with the new keyword, then why can't I treat it as a true constructor passing in parameters?

Service

(function () {
    angular
        .module("App")
        .service("Person", Person);

    //angular wants my parameters to be injectables.
    function Person(name, age) {
        this.name = name;
        this.age = age;
    }
})();

Controller

(function () {
    angular
        .module("App")
        .controller("DemoController", DemoController);

    DemoController.$inject = ["Person"];

    function DemoController(Person) {
        var a = new Person("rafael", 22);// ERROR

        var Demo = this;

        Demo.title = "Demo";
    }
})();

Factories

As for factories, your documentation states that, as well as other books, that factories should return service / class instances. However, this code contradicts. No instances were created and returned by the factory.

 var batchModule = angular.module('batchModule', []);

/**
 * The `batchLog` service allows for messages to be queued in memory and flushed
 * to the console.log every 50 seconds.
 *
 * @param {*} message Message to be logged.
 */
batchModule.factory('batchLog', ['$interval', '$log', function($interval, $log) {
  var messageQueue = [];

  function log() {
    if (messageQueue.length) {
      $log.log('batchLog messages: ', messageQueue);
      messageQueue = [];
    }
  }

  // start periodic checking
  $interval(log, 50000);

  return function(message) {
    messageQueue.push(message);
  }
}]);
gkalpak commented 8 years ago

I am not sure what part of the docs doesn't add up (although admittedly the multitude of ways to create Angular services (hereafter "services") has been a common source of confusion).

There are numerous resources on that subject, but in brief (quoting from the docs):

Angular services are substitutable objects that are wired together using dependency injection (DI). You can use services to organize and share code across your app.

So, services are "things" that you can inject into other parts/components, such as directives, controllers, filters, other services etc. Angular's Inversion Of Control container is in charge of creating the service instances (one per injector), supplying the necessary "ingredients" (dependent values) as arguments and handing them to whoever requests them (through Dependency Injection).

The "recipe" for how a service will de created is defined using one of the available methods: .provider(), .service(), .factory(), .value(), .constant()

Basically, .provider() is the most flexible but verbose, so Agular provides the other methods as syntactic sugar (more or less) for some of the most common usecases.

The service factory function generates the single object or function that represents the service to the rest of the application.

That is exactly what it does. Using .factory(), you can specify a (factory) function, that will be invoked by the injector (when it needs an instance of the service), and it is expected to return the service instance (i.e. the "thing" that will be passed to whoever requests the corresponding service).

value: Register a value service with the $injector, such as a string, a number, an array, an object or a function.

For you usecase, you might want to just use the .value() shortcut. E.g. .value('Person', Person) says: "Each time someone requests for the service identified by the string 'Person', give them the Person (which happens to be a constructor function, but Agular doesn't know and doesn't care) and let them use it as they see fit (e.g. call it with new and a bunch of arguments).

Regarding the following:

[...] factories should return service / class instances. However, this code contradicts. No instances were created and returned by the factory.

Note that instance doesn't necessarily mean "something that was the result of calling the new operator on a constructor function / class". The new operator is one of the many possible ways of creating an instance of something. The example code returns a new function, which is an instance of Function (which is an instance of Object).

But the main point is that whatever the factory function returns, is the "instance of the service" in the sense that it is the "concrete thing" that will be passed to anyone that requests it (through DI). And because services as (per injector) singletons, the exact same "thing" will be passed everytime.


Afaict, everything works (and adds up) properly. BUT we always welcome suggestions (or better yet pull requests) with docs improvements, so by all means do come forward if you have ideas on how we can do a better job explaining these concepts.

I am going to close this, since it is not actionable in its current form, but feel free continue the discussion below.

aluanhaddad commented 8 years ago

@gkalpak I have long felt that the key point, of confusion around the service / factory issue, which the documentation completely fails to address, is the difference in their use cases, or rather the lack thereof. .service and .factory essentially have the exact same capabilities but cater to different programming styles. If the documentation would simply remark that .service should be used when you want to write in a classical style, and .factory should be used otherwise, things would be a lot clearer.

aluanhaddad commented 8 years ago

Additionally, I have seen some highly skilled angular developers write code which reflects a misunderstanding of arbitrarity of this distinction. For example, I have seen this:

(function () {
    Function MyService ($http) {
        this.$http = $http;
    }
    MyService.prototype.getById = function (id) {
        return this.$http('/whatever?id=' + id);
    };

    angular.module('app', [])
        .factory('MyService', ['$http', function($http) {
        return new MyService($http);
    }]);
}());

Why didn't they write this? I believe it was because the documentation failed to adequitely explain the similarity and redundancy of the concepts.

(function () {
    Function MyService ($http) {
        this.$http = $http;
    }
    MyService.prototype.getById = function (id) {
        return this.$http('/whatever?id=' + id);
    };

    angular.module('app', [])
        .service('MyService', ['$http', MyService]);
}());
gkalpak commented 8 years ago

Maybe they know the difference and they intentionally use it this way. Note that @johnpapa's styleguide suggests using .factory even where .service would be enough, for the sake of consistency/fewer concepts. (Some agree, some don't - there is no right or wrong.)

.service and .factory essentially have the exact same capabilities

This is not 100% true, unless you use a constructor function as a factory, havig a return value (which is neither a good idea, nor necessary). .service has a more limited scope but requires less boilerplate. .factory is a little more flexible, but is more verbose.

Like I said, if you have an idea of how the docs could be improved, please submit a PR and we'll be more than happy to discuss :wink:

MrOutput commented 8 years ago

the docs say that .service takes a constructor function and will be invoked by the new keyword, which I supplied and have attempted. But I can't use it like a constructor, it thinks the parameters are dependencies for the service. Instead, to get around this, I have to return the REAL constructor function in the service function / wrapper. That is where the docs are not adding up, or at least are confusing for me.

I am well aware of the ideologies behind factories and that they have some method that returns an instance of something depending on the parameters passed to them.

working code

(function () {
    angular
        .module("App")
        .service("Form", FormService);

    function FormService() {// SERVICE FUNCTION WRAPPER
        function Form() {// REAL CONSTRUCTOR FUNCTION
            this.roles = [];
            this.name = "";
        }

        Form.prototype.addRole = addRole;

        function addRole() {
            this.roles.push("");
        }

        return Form;// RETURN REAL CONSTRUCTOR
    }
})();

In your factory example, you are returning a function which is indeed an instance of Function. But you are returning a new function just to push messages to the same message queue. Plus, the docs say that a factory returns an instance of a service, which isn't what was done in that example. A Function was returned. Why not just expose a single method to push onto a queue to begin with?

gkalpak commented 8 years ago

The whole point of .service() is that you are supplying a constructor function that Angular's $injector will instantiate (with new) when it needs to create an instance of the service. It is not a way to "store" constructor functions that will be passed to the user to instantiate. And yes, arguments of constructor functions passed to .service() (and used to create service instances) are resolved through DI - that is the whole point of having a IoC container.

Again, for your usecase (where you don't need anything from DI), you could (and should) use .value():

(function () {

angular.
  module('App').
  value('Form', Form);

function Form() {
  this.roles = [];
  this.name = '';
}
Form.prototype.addRole = function addRole() {
  this.roles.push('');
}

})();
MrOutput commented 8 years ago

Using .value() could work but the docs say that it should take a service instance object as the second parameter. A constructor function is not a service instance object or is it to angular? The reason for the large confusion in the community is the numerous ways of using these methods and the lack of a concrete definition.

gkalpak commented 8 years ago

the docs say that it should take a service instance object as the second parameter

This means that it should take the "thing" (whatever that is), that will be returned from the $injector whenever someone requests the service (in this case 'Form').

A constructor function is not a service instance object or is it to angular?

In JavaScript there are no "services", so when we are talking about "services" in this context, we are referring to Angular services" (a.k.a. "substitutable objects that are wired together using dependency injection (DI)"). So, a constructor function (which is essentially just a function, which is essentially an Object) can be used as a service instance (i.e. as the "thing" that will be returned when someone requests for the service instance).

I think much of the confusion comes from the fact that in JavaScript, functions are first class citizens and are objects themselves (everything is an object :grin:).

This is (more or less) how I like to think about it:

// Assuming:
$provide.
  service('FOO', FooService).
  factory('BAR', barFactory).
  value('BAZ', bazValue).
  provider('QUX', quxProvider);

...I imagine the following "dialogs" taking place inside the app:

someone: Hey, $injector, I need my 'FOO'. $injector: Sure thing. $injector: (To itself: They need 'FOO'. I don't have a 'FOO' yet, let's create one. How do I create 'FOO'? Aha, I just var foo = new FooService(/* any arguments are resolved through DI */).) $injector: There is your 'FOO', sir. Have a nice day! $injector: (Passing foo to someone.)

sometwo: Hey, Mr. $injector, could I get some 'BAR' here? $injector: Sure thing. $injector: (To itself: They need 'BAR'. I don't have a 'BAR' yet, let's create one. How do I create 'BAR'? Aha, I just var bar = barFactory(/* any arguments are resolved through DI */).) $injector: There is your 'BAR', sir. Have a nice day! $injector: (Passing bar to sometwo.)

somethree: Yo, $injector dude, gimme some 'BAZ'? $injector: Sure thing. $injector: (To itself: They need 'BAZ'. I don't have a 'BAZ' yet, let's create one. How do I create 'BAZ'? Aha, I just var baz = bazValue.) $injector: There is your 'BAZ', sir. Have a nice day! $injector: (Passing baz to somethree.)

somefour: Could I get some QUX, please? $injector: Sure thing. $injector: (To itself: They need 'QUX'. I don't have a 'QUX' yet, let's create one. How do I create 'QUX'? Aha, I just var qux = /* some stuff that are outside the scope of this comment */.) $injector: There is your 'QUX', sir. Have a nice day! $injector: (Passing qux to somefour.)

somefive: So, $injector, my old friend, here's the thing: I need some 'FOO', some 'BAR', some 'BAZ' and some 'QUX'. $injector: Sure thing. $injector: (To itself: They need 'FOO', 'BAR', 'BAZ' and 'QUX'. Hey, I already have all of them - how convenient.) $injector: There is your 'FOO', your 'BAR', your 'BAZ' and your 'QUX', sir. Have a nice day! $injector: (Passing foo, bar, baz and qux to somefive.)

(Yes, Angular components do speak to each other like this :grin:)