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

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')?

From your API suggestions it looks like services will have to be singletons. I have written controllers I would consider services that need to be instantiated per host object. How does that mesh with your concept of services?

1 Like

Yeah, Tom, naming is intensely important. :slight_smile: For example, devs coming from other frameworks often assume they know what a “Route” is in Ember because it’s called a route and they’ve seen that before, so of course they should know all about what it means and how it behaves. As you know, totally not the case!

One of the worst things about programming is that problem of miscommunication because of differing context.

So, I reckon it doesn’t matter so much what we call these things so long as we define what they are… so we’re talking about services here: I notice you were careful about speaking about “pulling in additional functionality beyond what is provided out of the box” as your definition of a service originally. Maybe you’re being intentionally vague here so we can find out what we mean by this vague amorphously defined area that includes functionality not included out of the box :wink: :wink:

The question on my lips is… do we ask the community what a service is… or do we say “hey what’s a good name for something that fulfils this particular role? and we’re going to choose “service” for now as a discussion point. This will then go into the codebase as a beta feature and we’ll all use it, then inform our naming and definition later”. This seems to be what you’re saying… a community driven defintiion of services. Seems to be what happened with components.

The issue I encounter with that is that the definitions aren’t pinned down anywhere in a really hard way that explains examples very clearly. Point in case it took me a fair time to work out what a Route was and how it should be used… this could be solved with a much clearer architecture explanation… using non-trivial real-world examples.

A constant issue I have, for example, is I don’t know the Ember way to build a route that has access to sidechained models (for example, an edit screen that needs five lists for dropdowns which are model-driven). I’ve done this in a way in my app but I can’t shake the bad feeling that maybe it’s not “right”. I feel like this a lot when devving on Ember.

Having said all this, this services proposal sounds like a massively awesome idea, and reckon we should put it into swing and see where we end up. :smile: I reckon Ember has an amazing community… <3

1 Like

I agree, this feels like a junk drawer to me in the way it was described. This seems like something that could be implemented separately from Ember. Maybe that approach makes sense, where people who want it, pull it in and use it. The way people use it can then further inform whether it belongs in ember, what it should be called and how it should behave.

It seems that the “job to be done” for a service is not clear here. It might be helpful to think about the ways a service differs from a controller (and what they have in common).

Jobs common to Service & Controller

  • global
  • singleton (?)
  • managed by the container
  • Manages (unpersisted) application state

Jobs for service only

  • ?

Jobs for controller only

  • ?

I am not super experienced with Ember so I’m sure others can come up with a more complete list but currently it seems that controllers and services are very similar.

This might be (read: probably is) a daft idea but perhaps we should consider a way of blessing access to certain controllers from other parts of the app e.g.

App.BlahController = Ember.ObjectController.extend({
  exposeAsServiceTo: ['routes', 'controllers'], // routes|controllers|models|all|none etc.
  // ...
});

and then elsewhere

Ember.Object.extend({
  inject: ['blahService', 'fooService'], // append 'Service' to be more explicit
  // ... 
});

Throwing my hat into the ring with a generic dependency injection computed property: https://github.com/jayphelps/ember-computed-injection

var injection = Ember.computed.injection;

var MyController = Ember.Controller.extend({
  geolocation: injection('service:geolocation')
});

export default MyController;
7 Likes

Jay’s pitch is a clever way to declare things (which I will likely adopt in my own apps!) but I still don’t like moving forward without a solid understanding of what a service is and what a service isn’t. We don’t get all of the advantages of having common nomenclature and patterns with services if they’re just a thin veneer over a anything-goes container-loaded object. Luke’s post very neatly sums that up.

Regardless, as we’re heading down this path, I believe that “Services” is the kind of thing that lends itself well to being built as an addon. If the addon proves useful, matures well, and finds itself commonplace we can then move to include it in core.

1 Like

Yup, this would be my preference. Services could then just be a convention in ember-cli i.e. a services folder alongside controllers.

I’d like to see this approach replace ‘needs’ completely i.e.

var injection = Ember.computed.injection;

var MyController = Ember.Controller.extend({
  geolocation: injection('service:geolocation'),
  otherController: injection('controllers:otherController'),
});

export default MyController;
1 Like

Seems like the right place to bring up the discrepancy between Route-driven vs. non-Route-driven controllers. They seem like different objects to me. The former should manage state pertaining to a route, while the latter should manage state that wraps a model (eg. isSelected). The former should be singletons, while the latter should not. The former should have strict DI restrictions, while the latter should be instantiable wherever you want to wrap state around a model, similar to an ArrayProxy but with a little more itemController-flavor to it.

Looking forward to seeing @matchy’s work on model dependent state to clarify the roles of controllers.

Also, a backwards compatible, extensible API for services:

var MyController = Ember.Controller.extend({
  needs: {
    controllers: ['post', 'comments'],
    services: ['websocket', 'geolocation']
  }
});
4 Likes

Absolutely! Would be nice to have “one injection method to rule them all” (that isn’t type based, i.e. app.inject('controller', 'store', 'store:main')), getting rid of needs.