Services: A Rumination on Introducing a New Role into the Ember Programming Model

The App.inject method of doing this seems pretty straightforward, I’m confused as to why we’d specifically need another layer?

4 Likes

For most cases (because I only inject a couple services) I think injecting by type makes sense. What if that would be the default but requiring a service would be an optional part of needs?

// app/services/session.js
export default Ember.Service.extend({
  name: 'session',
  // inject into all of this type
  injectIn: 'routes',
  // available to "needs" if required
  availableIn: 'controllers'
});

// app/controllers/user.js
export default Ember.Controller.extend({
  needs: ['foo', 'service:session']
});

I’d rather see a more consistent DI pattern, something like

export default Ember.ArrayController.extend({
  dependencies: ['controllers:organisation', services:geolocation],
  
  someProperty: function(){
      this.get("dependencies.controllers.organisation");
      this.get("dependencies.services.geolocation");
  }.property()
4 Likes

Having services tied down to specific instances seems like it would make reusing/sharing them pretty annoying.

@tomdale maybe making services read only(disabling a set on them from the outside world) would be enough of a burden/hint to prevent people from doing bad things like UserService or currentUrlService. Basically similarly to how we say that data flows from router to a controller to a view, while events bubble the other other way, we can say that services provide data to controllers/models/etc. but that data shouldn’t flow the other way. Would this cover most of the cases where you would want to use a service?

Yeah, I’m a fan of this one:

// assumes a controller unless you provide a qualified name
export default Ember.Controller.extend({
  needs: ['service:geolocation', 'posts']
});

The fact that this is so easily accomplished using register and inject makes it harder for me to get on board with creating a whole new abstraction for this behavior. We’re increasing the API surface area (and the learning cliff) almost unnecessarily. By the time you’ve reached the point where you need behavior that crosses the pre-defined Ember boundaries you’re typically experienced enough to know about Ember’s dependency injection patterns.

The only way that I might actually like this is if we do a full inversion-of-control pattern where injections are declared by those that need them, similar to how we do needs, but that will probably result in unknowingly bad development decisions from people new to Ember.

The current (unreleased, I know) version of ember-flows registers a new container type, flow, which injects the current flow into every single route. The existing DI pattern meets those needs beautifully. I would argue that any time you need a singleton available in multiple places in your app you’re only a slight UI change away from needing it everywhere, so injecting on type doesn’t seem that wasteful. Even going with my preferred full IOC style to declare what you need it is also annoying in that it increases boilerplate–not really something that Ember is known for.

One thing I want to be careful of is that I’m no longer approaching this as a novice which might be unfairly skewing my opinions.

2 Likes

Hi,

Like some others, I think this is a completely unnecessary abstraction. We have been using the service layer as an additional abstraction for some years now in the PHP world, so I immediately started to use this in my first EmberJS app as well. In server-side (and from my small experience in my EmberJS app), services have the exact same goal (moving logic from controller to services so it can be reused).

As a consequence, the “availableIn” and the possibility to inject services in models really feel wrong. Models should never be aware of services, or there is likely a design flaw. Neither services should be aware of controllers. Only dependencies to a service should be other services, or eventually the store to fetch additional records. I tried to think a bit more about it and I really think this logic still apply in the context of a JS framework.

Regarding the additional base classes for service, I don’t get it. For instance in my application, I have an analytics service, whose task is to query my analytics back-end and return results. It is a simple instance that extends Ember.Object:

module.exports = App.AnalyticsService = Ember.Object.extend({
  /**
   * @param {App.Project} project
   * @param {string}      queryType
   * @param {Object}      hash
   */
  doRequest: function(project, queryType, hash) {
    // Do things
  },
}

Then, I simply isolated which objects needed to use this service. It turns out three of my components (mainly visualization tools) needed this service, so I simply added an initializer and ONLY injected the service into the components that needed it:

module.export = Ember.Application.initializer({
  name: 'analytics',

  initialize: function(container, application) {
    // Register the various analytics services into analytics components
    application.register('analytics:service', App.AnalyticsService, {singleton: true});

    application.inject('component:analytics-serie', 'analytics:service', 'analytics:service');
    application.inject('component:subscription-distribution', 'analytics:service', 'analytics:service');
    application.inject('component:plan-details', 'analytics:service', 'analytics:service');
  }
});

This works really well, so I don’t see the whole point of a base class and which additional feature it could bring, considering services are simple objects.

2 Likes

It occurs to me that there’s another way of injecting whatever you want directly into a property:

export default Ember.ArrayController.extend({
  geolocation: App.__container__.lookup('services:geolocation')
})

to make it a bit prettier, you could inject a resolver e.g.

export default Ember.ArrayController.extend({
  geolocation: resolver.lookup('services:geolocation')
})

by limiting where the resolver is injected (i.e. only routes and controllers) you’ve got injection where you want it without beginners doing crazy things like injecting into models.

The other benefit of this is that using the inject service/controller is much cleaner i.e.

export default Ember.ArrayController.extend({
  geolocation: resolver.lookup('services:geolocation'),

  fetchNearbyTweets: function() {
    this.get("geolocation").getCurrentPosition().then(function() {
       // ...
    });
  }
})

This would also make unit testing easy, you can just set the injected properties directly with your stubs/mocks.

1 Like

There’s lots of great discussion here, so thank you everyone.

Before replying to specific technical proposals, I’d like to address some of the responses that I think are caused by a fundamental misunderstanding of what Ember is after. Specifically:

  • Why even have an API for this? The underlying primitives make it easy to write my own services however I want.
  • Why create an Ember.Service base class? Just use Ember.Object.

To me and Yehuda, the notion of defining roles for objects is paramount in the Ember programming model.

If you’re pushing back against this feature, don’t ask yourself, “How hard is it to implement this simple feature with existing primitives?” Instead, ask yourself, “What can we, as a community, build over the next few years with the shared understanding of what a ‘service’ is?”

The point of creating new objects with new roles isn’t to add syntactic sugar on top of what we have today (though that is a nice benefit). The point is to get everyone developing Ember apps to use these objects to solve similar problems. That way, when someone has an insight into new ways that services can interact with the rest of the system, we have a foundation upon which we can reason about that interaction.

As you can see in this discussion, people have many different ideas about how services should work. If Ember shrugs its shoulders and says, “Just inject whatever you want,” our poor future API designer will be left with a mess on his or her hands.

A historical exemplar here is components. Take a look at @wycats’ first implementation of Ember.Component (then called Ember.Control): https://github.com/emberjs/ember.js/blob/ad4d1ef5fdda67c503c24346e097ddf06f77787f/packages/ember-views/lib/views/control.js.

The implementation is an embarrassingly small delta on top of the Ember.View primitive available on the time. Literally, components were just views that set their controller to themselves, and registered a Handlebars helper with the same name. That was it.

I remember at the time people pushing back, because it seemed like a small, trivial addition that would just cause confusion. “If you want to do this, you can do it in a few lines of code.”

But by giving a name—a role—to a new kind of object, as a community we were able to iterate on and expand the scope of that object, to the point where components are so much more than just “views whose controllers are themselves.” In fact, the implementation hasn’t changed that much. Instead, having components has given us a shared vocabulary to discuss the problem.

The router is another example of where we’re able to do awesome things because of the shared understanding of the roles of objects in the system. Things like query params are only possible because these roles are well-defined.

Services are the same. Don’t think about services in terms of “how hard would it be to implement this today?” Don’t think of it in terms of “I will lose the flexibility I need.” The container isn’t going away, and if services don’t do what you need, you can keep doing what you’re doing today.

Instead, think about how creating a vocabulary around the roles that services play in the system will allow us to have a discussion—and a shared implementation—that we can continue iterating on, as a community, for the years to come.

24 Likes

I’m assuming you’d still want the eager-validation of dependencies a la needs if we take the API in this direction, yes?

EDIT: another thing is that binding/aliasing to controllers.whatever and then further binding to properties on that dependency is really verbose and annoying; the Ember.inject approach would alleviate that greatly. Seems like a major win if we could preserve a lot of the same eager dependency validation.

Yeah I like that too, so long as we don’t make it too easy to inject any ol thing. Also, part of the goal of Tom’s proposal is to make stuff injectable to more things than just controllers; are you proposing expanding the needs API to non-controller objects?

yes validation on instantiation, but Ember.inject would return a cp, which enables lazy instantiation.

I’m a bit concern about a couple of things.

  • I don’t think the service should dictate where it should be used. I think it’s up to the consumer to specify its dependencies (as other’s mentioned by using something similar to needs instead of availableIn)
  • Lifetime management isn’t specified. I think that Singleton is an obvious choice for stateless services or App Level services. However, it might make sense to tie the lifetime of the object to the instance or maybe route.
2 Likes

The discussion seems to be leaning towards using injection defined outside of the controllers/routers (Ember.inject). In my experience DI containers that start off with this approach usually end up adding mechanisms for injecting directly into either constructors or properties (admittedly we’re going back to my java days here, but also note angular’s use of constructor injection).

The following API would be simple to work with for the reasons stated above, easy access within dependents( this.get(“geolocation”) ), easy unit testing and the ability to restrict which types of object could do injection.

export default Ember.ArrayController.extend({
  geolocation: inject('services:geolocation'),

  fetchNearbyTweets: function() {
    this.get("geolocation").getCurrentPosition().then(function() {
       // ...
    });
  }
})

edit: have to think about how you could actually make the inject method available in the right places…

1 Like

I think the difference between this proposal and the Ember.Control/Ember.Component evolution is that “service” seems to me to be a general bucket for everything that doesn’t fit Ember’s currently defined roles. Without a philosophy about what a service’s role is, it is hard to say that labeling it will drive simplicity or better architectures, as opposed to just creating a name for a dumping ground.

Maybe if we can define what isn’t a service that is also not a controller, route, view, template or model, we can start to hone in on something with more utility.

Or is the idea that there is value in having an “everything else” bucket?

10 Likes

Why would a service be a singleton?

Server side, I instantiate service instances with references to the objects to be operated on/with, and capture the results and state as properties. When done, I dispose of the service instance. Is there a reason this wouldn’t be done in an ember app?

2 Likes

It seems like a lot of “services” will have some singleton-like behavior. In general, though, I don’t like to use public singletons. In my experience, they can lead to brittle code, especially if the consumers of that singleton know that they’re using a singleton. Instead, I prefer pairing a private singleton with a public-facing class whose instances provide access to it.

Declaring services as classes seems like a good idea because it discourages people from designing them as singletons (or does it?). Also, it could allow the framework to intelligently create/destroy instances as the service as it goes in and out of scope.

Maybe the Service base class could go even further, providing hooks that indicate when service instances are created/destroyed. Or maybe just when the first/last instance is created/destroyed. That could allow the service author to proactively “shut down” a resource-intensive service when it is no longer in use.

At the very least, it might discourage people from using a service as a “dumping ground for quasi-global state”, since there would be no guarantee that you would get the same service object in two different places. In fact, the Ember layering could be enforced by creating different service instances for each layer.

I like this Services proposal. Right now our app creates several singleton objects (mostly subclassed from Ember.ObjectController) and injects them into routes and/or controllers. As they are globals, I imagine some are also being used in models and templates too. I don’t think all of these are implemented in the best possible way, but–at least for some–I think the approach makes sense and I think it would be improved by the explicit Service role.

Also, I remember it took us a long time to learn about and utilize the initializer/register/inject pattern. I know Ember and its docs and tutorials have come a long way since last year, but I still bet this Service role would be easier to understand than initializer/register/inject and you could get up and running with it faster and earlier.

2 Likes

What’s wrong with require('some-module')?