-
Notifications
You must be signed in to change notification settings - Fork 115
Modules & Bindings
Modules define bindings. In TP 3, bindings can be expressed both programmatically via bindings or via annotations (see Scope Annotations). Both are equivalent.
Setting up the bindings in a scope is performed via installation of modules in a scope. A module defines a set of bindings.
Toothpick.openScope(obj)
.installModules(new Module() {{
bind(IFoo.class).toInstance(new Foo());
bind(IBar.class).to(Bar.class);
}});
This will setup the 2 bindings in the scope s
.
Graphically, we represent this situation by:
Scope s : IFoo --> new Foo & IBar --> Bar
Alternatively, TP 3 offers a new method to open a scope and passing a lambda:
Scope s = Toothpick.openScope(obj, scope ->
scope.installModules(new Module() {{
bind(IFoo.class).toInstance(new Foo());
bind(IBar.class).to(Bar.class);
}}));
This lambda is executed only when/if the scope is created. It will not be executed if the scope was already created prior to the openScope
call.
Now, let's define:
class A {
@Inject IFoo foo1;
@Inject IFoo foo2;
@Inject IBar bar1;
@Inject IBar bar2;
}
and we inject it in scope s
defined above :
A a = new A();
ToothPick.inject(a, s);
then :
-
a.foo1
will be the instance ofFoo
created in the module above; -
a.foo2
will be the same instance ofFoo
; -
a.bar1
anda.bar2
will be both be instances ofBar.class
, and will be different from each other.
This is true for any scope that is a children of s
. Except if the child scope of s
overrides any binding of s
.
The class toothpick.config.Module
defines a small DSL to conveniently express bindings, and bindings can be expressed in various ways, which are called 'Binding modes' :
class SimpleModule extends Module {
SimpleModule() {
bind(IFoo.class).to(Foo.class); // case 1
bind(IFoo.class).toInstance(new Foo()); // case 2
bind(IFoo.class).toProvider(FooProvider.class); // case 3
bind(IFoo.class).toProviderInstance(new FooProvider()); // case 4
bind(Foo.class); // case 5
}
}
The various binding modes are :
- case 1: Every
@Inject IFoo
will be assigned a new instance ofFoo
. - case 2: Every
@Inject IFoo
will be assigned the same instance ofFoo
. The instance defined in the module. - case 3: Every
@Inject IFoo
will be assigned a new instance ofFoo
produced by a new instance ofFooProvider
. - case 4: Every
@Inject IFoo
will be assigned a new instance ofFoo
produced by the same instance ofFooProvider
. The instance defined in the module. - case 5 : Every
@Inject Foo
will be assigned a new instance ofFoo
.
It is possible to name injections & bindings in Toothpick to distinguish 2 bindings that bind the same class. The name used to qualify an injection or a binding can be either:
- a string
- an annotation class
Using an annotation class as a qualifier is the exact same thing as using a string with the value of the canonical name (~Fully Qualified Name) of the annotation class.
Qualified injections can be used when injecting :
- fields
- methods or constructor parameters
- programmatically, via
scope.getInstance(Foo.class, "name")
Example :
bind(IFoo.class).toInstance(new Foo()) // unnamed binding
bind(IFoo.class).withName("name").toInstance(new Foo()) // named binding, with name "name"
bind(IFoo.class).withName(my.MyAnnotation.class).toInstance(new Foo()) // named binding, with name "@my.MyAnnotation"
bind(IFoo.class).withName("my.MyAnnotation").toInstance(new Foo()) // named binding, with name "my.MyAnnotation"
//the 2 last bindings are equivalent and require the annotation :
package my;
@Qualifier //this annotation is needed to define a qualifier
@interface MyAnnotation {}
to use these bindings, one would use
@Inject IFoo foo; //use the unnamed binding
@Inject @Named("name) IFoo foo; //use the unnamed binding
scope.getInstance(IFoo.class, "name") // named binding, with name "name"
@Inject @my.MyAnnotation IFoo foo; // named binding, with name "my.MyAnnotation"
scope.getInstance(IFoo.class, "my.MyAnnotation") // named binding, with name "my.MyAnnotation"
Note that in kotlin, names and scope annotations must use the kotlin field
qualifier to state that a property's field is annotated:
@Inject lateinit var IFoo foo; //use the unnamed binding
@Inject @field:Named("name) lateinit var IFoo foo; //use the unnamed binding
scope.getInstance(IFoo::class.java, "name") // named binding, with name "name"
@Inject @field:my.MyAnnotation IFoo foo; // named binding, with name "@my.MyAnnotation"
scope.getInstance(IFoo::class.java, "my.MyAnnotation") // named binding, with name "@my.MyAnnotation"
In the example above, in the cases 1, 3 and 5, Toothpick is responsible for creating the instances during injection. Whereas in cases 2 and 4, the developer herself is creating the instances of Foo, either directly or via a provider that she defines.
A good rule to remember is :
As soon as Toothpick creates an object, its dependencies will be injected.
which also implies that :
As soon as a developer creates an object with Toothpick, she has to take care of injecting this object's dependencies.
This means that if we define the class :
class Foo {
@Inject Bar b;
}
In the cases 1, 3 and 5, all instances of Foo
that are created during injection by Toothpick will themselves be injected. All injected methods and fields will be called/assigned and their annotated constructor or the default constructor will be used to create the instances of Foo
.
In the case 2 and 4, the developer will have to assign the field bar
to an instance of Bar
, the developer can do it manually or by asking Toothpick to do it : Toothpick.inject(foo, scope)
.
By default, scopes use the bindings defined in parent scopes. But they are allowed to override them. Such an override will impact the scope itself and its own children, hiding the binding defined in its parent.
Here is an example :
Scope S0 : IFoo.class --> Foo.class
\
\
Scope S1 : IFoo.class --> Foo2.class
\
\
Scope S2
then if we have a class
class A {
@Inject IFoo foo;
}
Then
Toothpick.inject(a, s0); // => a.foo is an instance of Foo
Toothpick.inject(a, s1); // => a.foo is an instance of Foo2
Toothpick.inject(a, s2); // => a.foo is an instance of Foo2
Example As an example, in all scopes s
of Toothpick, there is always a binding of the toothpick.Scope
class to s
(as a singleton of scope s
). This binding is overridden by all scopes.
Scope S0 : Scope.class --> S0
\
\
Scope S1 : Scope.class --> S1
In TP3, the binding definition language has been largely enhanced. It is now a fluent API and gives a developer as much granularity as using scope annotations. Bindings can:
- create new instances every time, this is the default behavior.
- create singletons using
.singleton()
- create releasable singletons using
.singleton().releasable()
- create singletons from a provider using
.ProvidesSingleton()
- create releasable singletons from a provider using
.providesSingleton().providesReleasable()
- a provider itself can also be a singleton or a releasable singleton by using
.singleton()
or.singleton().releasable
on a provider binding.
Releasable singletons will be discussed in section Releasable Singletons, while scope annotations are discussed in section Scope Annotations