Skip to content

Scoped & Unscoped Bindings

Stéphane Nicolas edited this page Jul 17, 2019 · 27 revisions

In toothpick there are 2 kinds of bindings :

  • unscoped bindings
  • scoped bindings

Unscoped Bindings

If we don't define any binding for ``Foo, it is still possible to inject it (@Inject Foo foo`). This is a simple binding `Foo --> Foo`, which is not scoped. An unscoped binding expresses no constraints on the creation of the `Foo` instances, as opposed to a scoped binding (see below). An unscoped binding is said to belong to all scopes. It is only possible to create unscoped bindings via annotations, more precisely via both the absence of any scope annotation and the absence of programmatic bindings (installed via modules).

A unscoped binding is the same as defining this binding in every single scope of your app.

In this case, any scope can use @Inject Foo, you can also use scope.getInstance(Foo.class) with any scope. The current scope will always be used to produce a Foo instance, and its dependencies (unless they are scoped, which is quite a smell that Foo itself should be scoped).

Examples of unscoped bindings:

Let's consider the 2 classes:

class A {
  @Inject Foo foo;
}

class Foo {
  @Inject Scope s;
}

The scope tree used in this example will be :

Scope s0 : Scope --> S0
  \
   \
  Scope S1 : Scope --> S1 
    \
     \
    Scope S2 : Scope --> S2

And the unscoped binding: Foo --> Foo

(Remember that the binding of class Scope is always overridden by all scopes.)

Then using the classes A & Foo & the scope tree defined above, we would have :

  • Toothpick.inject(new A(), S0) :
    • a.foo will be created in S0, as well as all dependencies of Foo
    • a.foo.scope will be S0.
  • Toothpick.inject(new A(), S1) :
  • a.foo will be an instance of Foo, different from above;
  • a.foo will be created in S1, as well as all dependencies of Foo.
  • a.foo.scope will be S1.
  • Toothpick.inject(new A(), S2) :
  • a.foo will be an instance of Foo, different from above;
  • a.foo will be created in S2, as well as all dependencies of Foo.
  • a.foo.scope will be S2.

Note on TP implementation: unscoped bindings are implemented using a static binding map in TP, this allows to reuse their factories and avoids to pay the price of creating these factories. But this has no implication at a conceptual level: an unscoped binding belong to every single scope.

Scoped Bindings

A binding is scoped when we define programmatically a binding and install via a module OR when we use scope annotations:

  • bind(Foo.class)
  • bind(IFoo.class).to(Foo.class)...
  • bind(IFoo.class).toProviderInstance(new FooProvider())...
  • bind(IFoo.class).toProvider(FooProvider.class)...

All of these bindings, when installed in a module are scoped and belong to the scope where the module is installed. These methods provide a feature rich API that lets developers have fine grained control over the way instances are created and recycled during injection.

Note that the fluent binding API offers the exact same granularity and expressivity as using annotations.

Examples of scoped bindings:

Let's consider the 2 classes:

class A {
  @Inject IFoo foo;
}

class Foo {
  @Inject Scope s;
}

The scope tree used in this example will be :

Scope s0 : Scope --> S0
  \
   \
  Scope S1 : Scope --> S1 & IFoo --> S(new Foo)  // <-- scoped singleton binding
    \
     \
    Scope S2 : Scope --> S2

(Remember that the binding of class Scope is always overridden by all scopes.)

Then using the classes A & Foo & the scope tree defined above, we would have :

  • Toothpick.inject(new A(), S0) :
    • A cannot be instantiated because its dependency IFoo has no binding in S0 or a parent of S0.
  • Toothpick.inject(new A(), S1) :
  • a.foo will be an instance of Foo;
  • a.foo will be created in S1, as well as all dependencies of Foo.
  • a.foo.scope will be S1.
  • Toothpick.inject(new A(), S2) :
  • a.foo will be an instance of Foo, same instance as above;
  • a.foo will be created in S1, as well as all dependencies of Foo.
  • a.foo.scope will be S1.

The instance of A itself is created in the current scope. But Foo is always created in S1 because it's the only scope that defines a binding for it. And because it is a singleton, the same instance is always returned.

Defining a scoped binding IFoo --> (Foo) means that Foo, the target of the injection, must fulfill all its dependencies in the scope where it is scoped or the parents of this scope OR these dependencies should be unscoped.

//space of creation of Foo instances in the case of a scoped binding in S1.
+--------------------------------------------------------------------+
|    Scope s0 : Scope --> S0                                         |
|         \                                                          |
|          \                                                         |
|   Scope S1 : Scope --> S1 & IFoo --> (Foo)  // <-- scoped binding  |
|          \                                                         |
+-----------\--------------------------------------------------------+
             \
        Scope S2 : Scope --> S2

All the bindings defined in children scopes of S1 are not taken into account : Foo instances must exist, as well as all their transitive dependencies, in S0 & S1. And the instances of Foo created will be recycled in S1 and the scope below S1.

Toothpick will enforce this constraint and will check that a scoped binding doesn't require any dependency that is scoped in a children scope.

Final note

Use scoping to create specific memory spaces that can be garbage collected (by closing the scope). Use them to enforce design constraint: scope dependencies to clearly mark the scope to which they belong.

Use unscoped bindings when the above doesn't apply, for instance for util classes used everywhere throughout the app. Note that unscoped bindings cannot create singletons, hence they cannot leak their instances.


## Links
* [Modules & Bindings](https://github.com/stephanenicolas/toothpick/wiki/Modules-&-Bindings)
* [[Scope annotations]]
* [[Scope resolution]]