-
Notifications
You must be signed in to change notification settings - Fork 115
Scoped & Unscoped Bindings
In toothpick there are 2 kinds of bindings :
- unscoped bindings
- scoped 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).
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 inS0
, as well as all dependencies ofFoo
-
a.foo.scope
will beS0
.
-
-
Toothpick.inject(new A(), S1)
: -
a.foo
will be an instance ofFoo
, different from above; -
a.foo
will be created inS1
, as well as all dependencies ofFoo
. -
a.foo.scope
will beS1
. -
Toothpick.inject(new A(), S2)
: -
a.foo
will be an instance ofFoo
, different from above; -
a.foo
will be created inS2
, as well as all dependencies ofFoo
. -
a.foo.scope
will beS2
.
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.
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.
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.
- A cannot be instantiated because its dependency
-
Toothpick.inject(new A(), S1)
: -
a.foo
will be an instance ofFoo
; -
a.foo
will be created inS1
, as well as all dependencies ofFoo
. -
a.foo.scope
will beS1
. -
Toothpick.inject(new A(), S2)
: -
a.foo
will be an instance ofFoo
, same instance as above; -
a.foo
will be created inS1
, as well as all dependencies ofFoo
. -
a.foo.scope
will beS1
.
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.
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]]