Modules & inheritance

Modules

Definition

Kodein-DI allows you to export your bindings in modules. It is very useful to have separate modules defining their own bindings instead of having only one central binding definition. A module is an object that you can construct the exact same way as you construct a DI instance.

Example: a simple module
val apiModule = DI.Module(name = "API") {
    bind<API> { singleton { APIImpl() } }
    /* other bindings */
}

Then, in your DI binding block:

Example: imports the module
val di = DI {
    import(apiModule)
    /* other bindings */
}
Modules are definitions, they will re-declare their bindings in each DI instance you use. If you create a module that defines a singleton and import that module into two different DI instances, then the singleton object will exist twice: once in each DI instance.

Name uniqueness

Each module name should only be imported once.

If a second module with the name of an already imported module is imported, then Kodein-DI will fail.

However, you cannot always ensure that every module name is unique: you may need to import modules that are defined outside of your code. Kodein-DI offers two ways to mitigate that:

  1. Rename a module:
    Use when you are importing a module whose name already exists.

    Example: imports a renamed module
    val di = DI {
        import(apiModule.copy(name = "otherAPI"))
    }
  2. Add a prefix to modules imported by a module:
    Use when a module imported by another module uses a names which already exists.

    Example: imports a module with a prefix for sub-modules
    val di = DI {
        import(apiModule.copy(prefix = "otherAPI-"))
    }

Import once

You may define a module which you know depends on another module, so it would be great to import that dependency inside the module that has the dependency. However, each module can only be imported once, so if every module that depends on another module imports it, Kodein-DI will fail at the second module that imports it.

To support this, Kodein-DI offers importOnce: it imports the module if no module with that name was previously imported.

Example: importing a module only once
val appModule = DI.Module {
    importOnce(apiModule)
}

Extension (composition)

Kodein-DI allows you to create a new DI instance by extending an existing one.

Example: extends an already existing DI instance
val subDI = DI {
    extend(appDI)
    /* other bindings */
}
This preserves bindings, meaning that a singleton in the parent DI will continue to exist only once. Both parent and child DI objects will give the same instance.

Overriding

By default, overriding a binding is not allowed in Kodein-DI. That is because accidentally binding twice the same (class,tag) to different instances/providers/factories can cause real headaches to debug.

However, when intended, it can be really interesting to override a binding, especially when creating a testing environment. You can override an existing binding by specifying explicitly that it is an override.

Example: binds twice the same type, the second time explitly specifying an override
val di = DI {
    bind<API> { singleton { APIImpl() } }
    /* ... */
    bind<API>(overrides = true) { singleton { OtherAPIImpl() } }
}

By default, modules are not allowed to override, even explicitly. You can allow a module to override some of your bindings when you import it (the same goes for extension):

Example: imports a module and giving it the right to override existing bindings.
val di = DI {
    /* ... */
    import(testEnvModule, allowOverride = true)
}
The bindings in the module still need to specify explicitly the overrides.

Sometimes, you just want to define bindings without knowing if you are actually overriding a previous binding or defining a new. Those cases should be rare and you should know what you are doing.

Example: declaring a module in which each binding may or may not override existing bindings.
val testModule = DI.Module(name = "test", allowSilentOverride = true) {
    bind<EmailClient>() { singleton { MockEmailClient() } } (1)
}
1 Maybe adding a new binding, maybe overriding an existing one, who knows?

If you want to access an instance retrieved by the overridden binding, you can use overriddenInstance. This is useful if you want to "enhance" a binding (for example, using the decorator pattern).

Example: declaring a module in which each binding may or may not override existing bindings.
val testModule = DI.Module(name = "test") {
    bind<Logger>(overrides = true) { singleton { FileLoggerWrapper("path/to/file", overriddenInstance()) } } (1)
}
1 overriddenInstance() will return the Logger instance retrieved by the overridden binding.

Overridden access from parent

Let’s consider the following code :

Example: Mixing overriding & extension
val parent = DI {
    bind<Foo>() { provider { Foo1() } }
    bind<Bar>() { singleton { Bar(foo = instance<Foo>()) } }
}

val child = DI {
    extend(parent)
    bind<Foo>(overrides = true) { provider { Foo2() } }
}

val foo = child.instance<Bar>().foo

In this example, the foo variable will be of type Foo1. Because the Bar binding is a singleton and is declared in the parent Kodein-DI, it does not have access to bindings declared in child. In this example, both parent.instance<Bar>().foo and child.instance<Bar>().foo will yield a Foo1 object.

This is because Bar is bound to a singleton, the first access would define the container used (parent or child). If the singleton were initialized by child, then a subsequent access from parent would yeild a Bar with a reference to a Foo2, which is not supposed to exist in parent.
By default, all bindings that do not cache instances (basically all bindings but singleton and multiton) are copied by default into the new container, and therefore have access to the bindings & overrides of this new container.

If you want the Bar singleton to have access to the overridden Foo binding, you need to copy it into the child container.

Example: Copying the bar binding into the child container
val child = DI {
    extend(parent, copy = Copy {
        copy the binding<Bar>() (1)
    })
    bind<Foo>(overrides = true) { provider { Foo2() } }
}
Copying a binding means that it will exists once more. Therefore, a copied singleton will no longer be unique and have TWO instances, one managed by each binding (the original and the copied).

If the binding you need to copy is bound by a context (such as a scoped singleton), you need to specify it:

Example: Copying a tagged scoped singleton
val parent = DI {
    bind<Session>(tag = "req") { scoped(requestScope).singleton { context.session() } }
}

val child = DI {
    extend(parent, copy = Copy {
        copy the binding<Session>() { scope(requestScope) and tag("req") }
    })
    bind<Foo>(overrides = true) { provider { Foo2() } }
}
You can use the context<>(), scope() and tag() functions to specialise your binding copies.

You can also copy all bindings that matches a particular definition :

Example: Copying all that matches
val child = DI {
    extend(parent, copy = Copy {
        copy all binding<String>() (1)
        copy all scope(requestScope) (2)
    })
}
1 Will copy all bindings for a String, with or without a context, scope, tag or argument.
2 Will copy all bindings that are scoped inside a RequestScope.

Finally, you can simply copy all bindings:

Example: Copying all
val child = DI {
    extend(parent, copy = Copy.All)
}

Or you can decide that none are copied (if you do want existing bindings to have access to new bindings):

Example: Copying none
val child = DI {
    extend(parent, copy = Copy.None)
}