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

Many Ember apps need to pull in additional functionality beyond what’s provided out of the box. Often times, these additional dependencies don’t always fit neatly into the existing roles provided in the programming model.

Some examples of things I’ve needed to implement in my own apps:

  • WebSocket initialization
  • Geolocation
  • Global config (for things like feature flags, etc.)

Most people end up putting this extra functionality in a controller, which is great, and works in many cases. For example, let’s say you need geolocation in controllers/nearby-tweets:

export default Ember.ArrayController.extend({
  needs: ['geolocation'],

  fetchNearbyTweets: function() {
    var geolocation = this.get('controllers.geolocation');
    geolocation.getCurrentPosition().then(function() {
       // ...
    });
  }
});

Because controllers in Ember have access to other controllers via needs, this works great.

But what about if you need the functionality somewhere other than a controller? For example, you may want to make a shared WebSocket available to all of the models in your app. Or, you may have a {{#feature-flag}} component, and you’d like to provide it with configuration information so it knows which flags are on or off.

Services

The idea behind services is to make conventional and declarative what many people are doing now imperatively via initializers and injections. I’ll provide API sketches using the ember-cli-style resolver, but they should translate to globals just as well.

Defining a Service

Services are automatically detected and registered by placing them in the services directory:

app/
  services/
    geolocation.js
    web-socket.js

To implement a service, just export a subclass of Ember.Service:

export default Ember.Service.extend();

Specifying Availability

While similar in idea to Angular’s services/providers/factories, we should try to avoid the same API mistakes that lead to the spaghetti code seen in most larger apps. In particular, many new developers use services to punch through architectural layering and share information between, say, the route and the template layer.

To help nudge users in the right direction, services should be opt-in rather than opt-out. Service authors explicitly say where their service is available:

// app/services/web-socket.js
export default Ember.Service.extend({
  availableIn: 'models'
});
// app/services/geolocation.js
export default Ember.Service.extend({
  availableIn: ['controllers', 'routes']
});

Using Services

Services are singletons, and the singleton is injected into every instantiated object that matches the service’s availableIn criteria. The name of the service is used to determine which property to set on the new instance.

For example, here’s the nearby-tweets controller, assuming a services/geolocation.js service was available to controllers:

// app/controllers/nearby-tweets.js
export default Ember.ArrayController.extend({
  fetchNearbyTweets: function() {
    this.geolocation.getCurrentPosition().then(function() {
       // ...
    });
  }
});

As you can see, the geolocation property has been set to the geolocation service singleton.

Similarly, the services/web-socket.js service would be injected as webSocket.

You can customize the property used to inject the service by setting its name property:

// app/services/web-socket.js
export default Ember.Service.extend({
  name: 'socket',
  availableIn: 'models'
});

In the above example, every model would have its socket property set to the service.

It’s important to note that, because the services API uses the existing container injection API, testing an object that relies on services is as simple as mocking or stubbing a property on that object. This avoids the complication of DI systems like Angular’s, where users have to learn DI incantations just to test inter-object dependencies.

Next Steps

I’ve implemented this pattern in several of the apps I’ve worked on and believe it should be straightforward given the existing primitives. The most important part is making sure that the API is as intuitive as possible, without giving new developers just enough rope to hang themselves.

Does the above proposed API solve your use cases? Do you think having this would help you clean up your apps? Do the names make sense? Let the bikeshedding ensue!

46 Likes

The title is ridiculous because Discourse says titles have to be over 15 characters, by the way. Originally it was just “Services”. :blush:

3 Likes

LGTM :+1:

We should start playing with this behind a feature flag in canary.

2 Likes

Whilst this would only save a few lines of code, I think it repeatably and consistently saves a few lines of code for many (most?) non-trivial apps

1 Like

The value is having the pattern defined so it’s not just a “pro-user” thing.

3 Likes

I think the benefit it really just being explicit about “Here’s the Ember way to create a service”.

4 Likes

Love it, and really hope to see this in release someday soon!

Can any kind soul point me to a guide or example of how to do this currently? (that is, with initializers and injections etc.)

@tomdale FYI whoever has the needed access for your discourse install can change the minimum title length.

/admin/site_settings/category/posting (5th field down)

1 Like

For some services, making them available to all of type makes sense.

eg. some concepts like analytics or instrumentation

But for example injecting your geolocation service on all controllers, even controllers which really don’t need it, seems like a big DI anti pattern.

1 Like

It’s actually surprisingly simple:

// inject the session into every controller
App.inject('controller', 'session', 'service:session');

// inject the geolocation service onto a specific controller
App.inject('controller:articles', 'geolocation', 'service:geolocation');

In global mode, you’d make an App.SessionService, and in modules mode, you’d make a appkit/services/session module.

Bada bing.

7 Likes

That’s a great point. Maybe we need an API for specifying specific instances, instead of just by type?

my concern with that is, now your services must be aware of the entities that depend on them. Which is clearly a DI anti-pattern.

Likely the entity should declare it’s intent to be “Serviced by” rather then the service being aware of each entity it should “service”

3 Likes

Let’s keep it safe for work, Stef.

7 Likes

i knew your would enjoy that one :stuck_out_tongue:

Feel free to tell me why this is stupid, but here was my thought:

Why not mimic the way controllers reference each other?

in a controller:

needsServices: ['geolocation'],

Then reference by

this.services.geolocation.foo();
2 Likes

I know this concept has been thrown down a few times. But the Ember.inject('service:geo') satisfies the problem, without us having to add needsFoo for every Foo we want to add.

1 Like

The issue with this, as we see in Angular apps, is that you have people who don’t understand or care about layering declare that two totally unrelated objects have access to the same service, and use it to do an end-run around the layering boundaries we’ve put in place.

After thinking about it a bit, it seems like if something should only be available on one or two specific objects, a “service” isn’t the right thing. We should perhaps define a service as something available to an entire category of object.

1 Like

Could we specify these in a services property instead? Then whoever looks at the controller is aware of its dependencies, which is important in testing (e.g. they’d know they’d need to stub out a geolocation service).

export default Ember.ArrayController.extend({
  needs: ['someController'],
  services: ['geolocation', 'websockets'],
  ...

availableIn could still be used to prevent developers from injecting anything they want.

1 Like

remember though, that lots of our objects (like object and array controllers are proxies) the less reserved properties names we use the better. It unfortunately seems possible for someone today to have company.services and the addition of services as a reserved word to all proxies becomes a breaking change.

It would be great if we could keep these properties on a meta or base class, and not on the instances…

6 Likes

I am a big fan of this concept, a few questions and thoughts…

  1. Isn’t the code still declarative & imperative in the needs + this.get code above? If you have to leverage the imperative this.get(...) is that not enough information to resolve the requested dependency? Why do I need the needs call? I suppose this gets resolved if needs attaches a reference to the injected target like in the this[token] example which removes the this.get call.

  2. I am a bit concerned about the the service author prescribing where a given service can be used, while I understand that people can use this construct to punch through architectural boundaries I think locking it down is a hefty price to pay because now a given service author has to be completely aware of his consumers when in a lot of cases he simply can’t be.

  3. I am also concerned about services as singletons, the benefit/detriment of having it as a singleton is having state which can be referenced across injections. One of the excellent pieces of Ember is that state management is clearly defined in the router, the singleton service concept might encourage less router state and push more of it into services as a crux.

  4. I think a way to inject an arbitrary Ember.Object subclass is the right way to go, perhaps I am missing the value of a Ember.Service abstraction.

If none of the above makes sense feel free to ignore it, trying to get more of a handle on the Ember ecosystem as it is today.

1 Like