-
Notifications
You must be signed in to change notification settings - Fork 113
Scoped & Unscoped Bindings
In toothpick there are 2 kinds of bindings :
- unscoped bindings
- scoped bindings
In the 2 following sections, we will use a common example class and see how scoped and unscoped bindings differ when injecting them :
class A {
@Inject IFoo foo1;
@Inject IFoo foo2;
}
class Foo {
@Inject Scope s;
}bind(IFoo.class).to(Foo.class) is a simple association IFoo --> Foo. An unscoped binding expresses no constraints on the creation of the Foo instances, as opposed to a scoped binding. An unscoped binding is said to belong to a given scope (in which it will be installed via a Module).
Basically, when a scope defines bind(IFoo.class).to(Foo.class), it means that in this scope and its children scopes : @Inject IFoo foo will return a Foo.
The scope tree used in this example will be :
Scope s0 : Scope --> S0
\
\
Scope S1 : Scope --> S1 & IFoo --> Foo
\
\
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): There is no binding to injectIFooas the binding is only defined in child scope S1. The injection ofnew A()inS0will fail with an appropriate error message. -
Toothpick.inject(new A(), S1): The injection ofnew A()inS1will result in the following : -
a.foo1will be an instance ofFoo; -
a.foo2will be an instance ofFoo, different froma.foo1; -
a.foo1.scope&a.foo2.scopewill beS1. -
Toothpick.inject(new A(), S2): The injection ofnew A()inS2will result in the following : -
a.foo1will be an instance ofFoo; -
a.foo2will be an instance ofFoo, different froma.foo1; -
a.foo1.scope&a.foo2.scopewill beS2.
A binding is scoped when we call one of its method xxxInScope():
- instancesInScope()
- singletonInScope()
- providesSingletonInScope()
All together these methods provide a feature rich API that lets developers have fine grained control over the way instances are created and recycled during injection.
First, let's highlight an important property of bindings that are not scoped:
bind(IFoo.class).to(Foo.class) is defined in a module that will be installed in a Scope S1. However, the instance Foo will not be created using that Scope S1, but the scope that is used to get the instance S2: s2.getInstance(IFoo.class).
On the opposite, the xxxInScope() methods will ensure that the scope used to create instances of IFoo will be the one where the binding is defined, and not the current scope.
Here is the exact meaning of these methods:
-
bind(IFoo.class).to(Foo.class).instancesInScope()express more than the simple associationIFoo --> Foo. Such a scoped bindings means :- there is an association
IFoo --> Foo, in the same way as an unscoped binding does; - AND the instance of
Foowill be created inside the scope that defines this binding. All the dependencies ofFoowill have to be found at runtime in the scope where the binding is scoped or in its parent scopes.
- there is an association
-
bind(IFoo.class).to(Foo.class).singletonInScope()express more than the simple associationIFoo --> Foo. Such a scoped bindings means :- same as
instancesInScope() - AND the same instance of
Foois recycled/reused for each injection ofIFoo
- same as
-
bind(IFoo.class).toProvider(FooProvider.class).providesSingletonInScope()express more than the simple associationIFoo --> Foo. Such a scoped bindings means :- same as
singletonInScope(), but applied to the object provided by the providers. The provider will provide only one instance ofIFoo, and it will be created inside the scope where the binding is defined.
- same as
A scoped binding is said to be scoped in a given scope. We note scoped bindings in the following ways:
-
instancesInScope()scoped bindings are notedIFoo --> (Foo)with the target of the binding in parenthesis; -
singletonInScope()scoped bindings are notedIFoo --> (new Foo); -
providesSingletonInScope()scoped bindings are notedIFoo --> (P<new Foo>).
The scope tree used in this example will be :
Scope s0 : Scope --> S0
\
\
Scope S1 : Scope --> S1 & IFoo --> (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): There is no binding to injectIFooas the binding is only defined in child scope S1. The injection ofnew A()inS0will fail with an appropriate error message. -
Toothpick.inject(new A(), S1): The injection ofnew A()inS1will result in the following : -
a.foo1will be an instance ofFoo; -
a.foo2will be the same instance ofFooasa.foo1; -
a.foo1.scope&a.foo2.scopewill beS1. -
Toothpick.inject(new A(), S2): The injection ofnew A()inS2will result in the following : -
a.foo1will be an instance ofFoo; -
a.foo2will be the same instance ofFooasa.foo1, and the same instance as whenAwas injected inS1. -
a.foo1.scope&a.foo2.scopewill beS1.
The last line a.foo1.scope = a.foo2.scope = S1 is the most important.
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 scopes OR the 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.
In a module, not all bindings can be scoped. Only the bindings that imply that Toothpick will need a scope to create instances can be scoped.
bind(IFoo.class).to(Foo.class); // can be scoped
bind(IFoo.class).to(new Foo()); // cannot be scoped (it is indeed scoped)
bind(IFoo.class).toProvider(FooProvider.class); // can be scoped, will scope both the provider and produced instances
bind(IFoo.class).toProvider(new FooProvider()); // can be scoped (the provider is always scoped, but the provided can also be scoped)
bind(Foo.class); // can be scoped