Some Improvement ideas for our DI subsystem

be warned this is mostly a stef.braindump that was re-awakened by a discussion on Reddit

This document will also likely change and improve as the ideas iterate and mature.

Currently our dependency injection framework provides us with scope level injection rules, that typically run at app initialization.

App-wide Rules

(local rule-sets are possible, but not thoroughly hashed-out yet)

// assume app-wide scope
App.inject(thingToInjectOn, property, thingToInject);
App.register(thingToInject, factory);

App.register('store:main', Store);
// example form ED. Injection on all contorllers
App.inject('controller', 'store', 'store:main');

// injection on a specific controller
App.inject('controller:person', 'facebook', 'service:facebook');

Specifying Inter-controller dependencies:

For this we have lazy property style injections, but with at instantiation time verification. This means we donā€™t inject at construction time, but if you instantiate the controller, and its cannot be satisfied, we throw an exception. Also the ā€œneededā€ things themselves are instantiated on demand.

B = Ember.Controller.extend({});
A = Ember.Controller.extend({
  needs: ['b']
}};

The Concern

I feel the global rules are powerful, and enable many use cases, but I am concerned the locality of some of the global rules, specifically for injections related to a single object.

For example, when specifying the following rule, in an app initializer, their may be no mention of FacebookService within the PersonController.

App.inject('controller:person', 'facebook', 'service:facebook');

So, often to prevent this situation, my team and I, annotate our definitions.

PersonController = Ember.Controller.extend({
  facebook: null // set by injection
})

Part of me thinks that this annotation feels as it should be the location of the rule itself.

An idea for the future

allow for at-design-time class level injection rules to be specified.

As the entity is resolved, we load additional rules that it provides. These rules should be limited to rules describing injections on itself.

If at instantiation time, or at factory lookup time these rules cannot be satisfied, we should error.

Some random API ideas


B = Ember.Controller.extend({
  needs: ["service:facebook"] // by convention would become a property: services.facebook

  injections: // some DSL for injections

  // or maybe a more direct
  facebook: Ember.inject("service:facebook"),
  facebook: Ember.needs("service:facebook")
});

notes:

Now some may point-out that today a simple computed property could solve this.

facebook: function() {
  return this.container.lookup('service:facebook')
}.property()

This is not entirely true, as this isnā€™t at design time, and isnā€™t verified possible until runtime. Which means, if the injection was forgotten, or something went wrong. You wont know until this code path is exercised.

2 Likes

Included in needs seems like the most natural place for it. Even if I didnā€™t know a thing about DI, and you told me I can stick that stuff in needs, I donā€™t think Iā€™d be confused, because the word ā€œneedsā€ is clear.

I really donā€™t like not having breadcrumbs I can follow up the tree to see where properties are coming from. Without knowing about Emberā€™s injection system (not covered by any of the guides), Iā€™d just have no idea where to look.

Really good proposal Stefan.

Defining the injections separately from the class that they are injected into has also been the cause for a few issues for me and my team.

I really like the API you proposed:

App.MyController = Ember.Controller.extend({
  facebook: Ember.needs("service:facebook")
});

The dependency is obvious to the reader. And itā€™s less code than:

App.initializer({
  name: 'myInjection',
  initialize: function(container) {
    container.inject('controller:my', 'facebook', 'service:facebook');
  }
});

needs sounds better and more declarative than inject IMO. Itā€™s more like observes.

Great ideas, Stef.

Iā€™ve been mulling this over and can think of use cases for both declarative and imperative injections, so it would be great if we could support both.

By default, I think injections should continue to be available according to their type and name (i.e. controllers.flash and services.facebook). However, I can also see the benefit of providing aliases as part of declarations (i.e. flash and facebook in addition to controllers.flash and services.facebook).

Supporting the following options would give us both declarative and imperative injections, with or without aliases:

App.MyController = Ember.Controller.extend({
  // declarative injection
  // -> available at `this.get('services.facebook')`
  needs: ['service:facebook'],

  // declarative injection with alias
  // -> available at `this.get('services.facebook')` and `this.get('facebook')`
  facebook: Ember.needs('service:facebook'),

  init: function() {
    this._super();

    // imperative injection
    // -> available at `this.get('services.facebook')`
    this.inject('service:facebook');

    // imperative injection with alias
    // -> available at `this.get('services.facebook')` and `this.get('facebook')`
    this.inject('facebook', 'service:facebook');
  }
});

Iā€™m a big fan of this. Iā€™ve never really liked needs: [..]

1 Like

This is always an option too, fwiw

App.SomeThing = Ember.Object.extend({
  facebook: "service:facebook".inject()
});

I feel like Ember could use some more string prototyping in general. Sucks to have to write Ember.computed.alias et al

2 Likes

Iā€™m assuming that most of these injections on a specific object first require you to specify what you want to inject in App? Otherwise, how can ember know what 'service:facebook' is?

How would you define 'service:facebook' without injecting it first?

yes, first registering service:facebook is required, and done so at the scope (likely App). This is how it works today, and likely doesnā€™t need to change.

That would certainly be nice, although it kind of goes outside the scope of the String object, and would only pollute that namespace. Iā€™m all for writing less, though :smile:

@knownasilya same with Function prototype extensions, e.g. .property() and .observes(). Discretion is needed, as well as fallbacks like Ember.inject or Ember.needs

I have always been a big fan of the declarative macro-style injection: facebook: Ember.inject('service:facebook').

@stefan can you say more about ā€œthis isnā€™t at design time, and isnā€™t verified possible until runtimeā€? Is there a way to keep things lazy and also have these properties?

Not a fan of "facebook:service".inject(). Seems like an odd prototype extension.

Ya.

the rules are not lazy. So ā€œcan X rule be satisfiedā€ checks occurs early. But the actual injection and instantiation occurs lazily.