Skip to content

Modules & Bindings

Stéphane Nicolas edited this page Aug 17, 2019 · 23 revisions

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 bindings in a scope

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

Configuration lambda

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.

Injections defined via bindings only

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 of Foo created in the module above;
  • a.foo2 will be the same instance of Foo;
  • a.bar1 and a.bar2 will be both be instances of Bar.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.

Binding modes

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 of Foo.
  • case 2: Every @Inject IFoo will be assigned the same instance of Foo. The instance defined in the module.
  • case 3: Every @Inject IFoo will be assigned a new instance of Foo produced by a new instance of FooProvider.
  • case 4: Every @Inject IFoo will be assigned a new instance of Foo produced by the same instance of FooProvider. The instance defined in the module.
  • case 5 : Every @Inject Foo will be assigned a new instance of Foo.

Named/Qualified bindings

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"

Bindings, instance creations and injections

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).

Binding overrides in children scopes

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

Binding Fluent API

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

Links

Clone this wiki locally