Ioc Container

Dependency Injection allows you to manage a class's dependencies in a much cleaner way than the class itself creating and wiring them. Once you have set up the bindings, a class can have the dependencies injected via a constructor. The proper use of dependency injection frees you from the hassle of wiring all the dependencies. It also makes writing tests easier.

The dependencies don't always need to be injected in your class via a constructor. If you have access to the container that holds all the bindings, you could simply ask for instances of the dependencies. Depending on how you have set up the bindings, you could either get a new copy every time you ask or get a singleton instance of a dependency.

Here is an example of a config class where a singleton instance of Environment class is automatically injected via its constructor:


class AdminConfig(env: Environment) : Config {
    val adminEmail = env("admin_email", "admin@example.com")
}

In the following example, nothing gets injected in the constructor. Instead, we ask HttpCall to make an instance of AdminConfig. This is possible because HttpCall is a container and hence is very capable of resolving dependencies.


class AdminController : Controller() {
    fun show(call: HttpCall) {
        val adminConfig = call.make<AdminConfig>()
    }
}

Binding

Binding is nothing more than putting a dependency into a container mentioning how you want to have it constructed. All you need is the container that can be accessed from somewhere else so that you can ask the container for one or more dependencies.

More often than not, you will be doing bindings from within service providers and service providers are always injected the global app instance, which itself is an IOC container. You use this app instance to register your dependencies.

Simple Bindings

Register a binding on a container using the bind method and pass the java classname of the class you are trying to bind.


    class StripePaymentProcessor(client: HttpClient) {
        init { println("StripePaymentProcessor instance Created") }
    }


    class HttpClient {
        init { println("HttpClient instance Created") }
    }

   // ...
   override fun register(app: Application) {
        app.bind(HttpClient::class.java)
        app.bind(StripePaymentProcessor::class.java)
    }
   // ...

After above bindings, when you ask for StripApi, you'd get a new copy of it every time. A new copy of HttpClient is injected to StripePaymentProcessor constructor as well during the resolution.

Singleton Bindings

To get one single shared instance of a dependency, you could use the container's singleton method. A singleton dependency gets resolved only once.


    class StripePaymentProcessor {
        init { println("StripePaymentProcessor instance Created") }
    }

   // ...
   override fun register(app: Application) {
        app.singleton(StripePaymentProcessor::class.java)
    }
   // ...

Instance Bindings

If you already have an object instance and want to bind that instance, you could just use container's bind method for binding it. This instance will always be returned when asked for it. In a way this is like a singleton binding but without auto-injecting the dependencies of this instance that you are binding, which is up to you now.


   // ...
   override fun register(app: Application) {
        val processor = StripePaymentProcessor()
        app.singleton(processor)
    }
   // ...

Abstract Bindings

Instead of binding a concrete class, you can also bind a concrete implementation to its abstract class or its interface. This way you don't have to depend on a specific implementation of a class but only on the APIs that you need from a class. When resolving, you will refer to the abstraction instead of the concrete implementation, of course!


    interface PaymentProcessor {
        fun process()
    }

    class StripePaymentProcessor : PaymentProcessor {
        init { println("StripePaymentProcessor instance Created") }

        override fun process() {
            println("Payment processed through Stripe")
        }
    }

   // ...
   override fun register(app: Application) {
        app.bind(PaymentProcessor::class.java, StripePaymentProcessor::class.java)
    }
   // ...

/tip/ Even with interface bindings you can either do Simple Bindings, Singleton Bindings, or Instance Bindings based on whether you want a new copy, a shared single copy, or an object instance.

Factory Bindings

Instead of binding a class name or an instance, you can also bind a callback function that gets invoked every time a dependency needs to be resolved. Just remember that the actual binding resolved is whatever the last statement of this factory callback is.


   override fun register(app: Application) {
        app.bindFactory {
            println("StripePaymentProcessor factory binding called")

            // This is the actual binding. In this case we are returning 
            // a new instance of StripePaymentProcessor every time this 
            // factory callback gets invoked.
            StripePaymentProcessor()
        }
    }

    class StripePaymentProcessor {
        init { println("StripePaymentProcessor instance Created") }
    }

/alert/ Be careful binding "mutating" objects with the app container. Since these bindings will be shared with all the incoming requests, if the bindings mutate their states, you might run into race conditions. For these kinds of mutating global objects it is upto you to guarantee the thread safety of your app by taking appropriate measures.

Resolving Dependencies

There are two ways to resolve a dependency — via constructor injection or using make method.

Constructor Injection

If a class depends on some other classes, it can just declare the dependencies in its constructor to get them automatically injected. As long as this class itself is registered in the container, the container will be able to resolve the dependencies including all the transitive dependencies.


    // An instance of HttpClient gets automatically injected
    class StripePaymentProcessor(client: HttpClient) {
        init { println("StripePaymentProcessor instance created") }
    }


    // When resolving this class as a dependency of some other class(es), 
    // an instance of Logger class gets automatically injected as well.
    class HttpClient(logger: Logger) {
        init { println("HttpClient instance created") }
    }


    class Logger {
        init { println("Logger instance created") }
    }


    // ...
    override fun register(app: Application) {
        app.bind(Logger::class.java)
        app.bind(HttpClient::class.java)
        app.bind(StripePaymentProcessor::class.java)
    }
    // ...   

Using make()

Another way to have a dependency resolved out of a container is to use make() method. To resolve a dependency using make, just like binding, you need a shared container that contains the bindings that you have wired.


class SubscriptionController : Controller() {
    fun show(call: HttpCall) {
        val processor = call.make<PaymentProcessor>()
    }
}

Using makeMany()

With makeMany() you can resolve multiple instances of a type that have a common ancestor — by either extending an abstract class or by implementing an interface.


// declare
class StripePaymentProcessor : PaymentProcessor
class BrainTreePaymentProcessor : PaymentProcessor

// bind
container.bind(BrainTreePaymentProcessor::class.java)
container.bind(StripePaymentProcessor::class.java)

// resolve
val processors: List<PaymentProcessor> = container.makeMany<PaymentProcessor>()

HttpCall as a DI Container

Just like dev.alpas.AlpasApp, HttpCall is a DI container as well. HttpCall is created for every request but since it is a child container created off of the app instance, any bindings that are registered in the app instance container is also available from an HttpCall object. However, the reverse is not true — bindings registered in an HttpCall instance are not available via app instance container.

Also, any bindings that are registered with an HttpCall object gets closed at the end of the call. This is because an HttpCall is created for each request and gets closed when the request is fulfilled.

/alert/ Only add new bindings to an HttpCall if you want to share these bindings within the same request lifecycle. These kinds of bindings are usually thread safe — but not guaranteed — as they are only shared within one request call.