Using Koin for Dependency Injection

29 November 2019

Koin provides a modern DI framework which supports Kotlin idioms and is usable across conventional, mobile and Cloud based applications.

Dependency Injection is Dead

During the 2000s Dependency Injection was the poster child of good OO design. However in more recent times its reputation and relevance have declined. Whilst no one wants to hardwire encapsulated dependencies the utility of DI frameworks has been impacted by the move away from monolithic deployments towards microservices and beyond.

The original concept of microservices was that they would be too 'micro' to require a DI framework. Plus the division of reponsibilities into sets of independent services takes away the need for a DI tool to 'reconfigure on the fly'. Now we deploy our services via a cloud provider, and adopt consumption based pricing, the wisdom of configuring all our components on startup via reflection is questionable. It is definitely no longer the case that our services will remain running for days or months at a time and startup costs are therefore irrelevant.

In addition to the above the mechanism for specifying the 'wiring' in DI has always been problematic. XML proved too verbose and onerous to maintain, but annotations have spread through our codebase like a virus, desecrating the original vision of 'POJO Components'. The happy medium is (allegedly) DSLs, but older languages like Java and C# are not a good fit for creating them.

Finally we now have an alternative to conventional DI, which is to use the Functional Programming features available in all modern programming languages. Former advocates of OO based DI have made a persuasive case that FP idioms (such as Higher Order Functions) are a better way of removing Encapsulated Dependencies from our code.

Long Live Dependency Injection!

So does this mean we are done with DI frameworks? Not at all! But it does mean that we want a framework which:

  • Is as lightweight and unobtrusive as possible
  • Makes use of modern JVM languages like Kotlin
  • Supports configuration via a DSL and FP idioms
  • Does not rely heavily on the reflection library
  • Works across the full range of deployment options

This is precisely the set of features which we get from Koin :-)

Starting Out With Koin

Let's take a simple example where DI could be applied. In the code below we have a Shop class which relies on a PricingEngine, a StockCheckEngine and a PurchaseEngine. When the Shop is asked to make a purchase it uses the engines to check there is enough of the item in stock, calculate the charge to be billed to the customer and authorise this charge against the customer's credit card. The makePurchase method returns one of the values from the Result enumeration.

enum class Result { SUCCESS, OUT_OF_STOCK, AUTH_FAILED }

interface PricingEngine {
    fun price(quantity: Int, stockNum: String): Double
}

interface StockCheckEngine {
    fun checkQuantity(stockNum: String): Int
}

interface PaymentEngine {
    fun authorise(amount: Double, cardNum: String): Boolean
}

class Shop(private val pricingEngine: PricingEngine,
           private val stockEngine: StockCheckEngine,
           private val paymentEngine: PaymentEngine) {

    fun makePurchase(stockNum: String, quantity: Int, cardNum: String): Result {
        return if(stockEngine.checkQuantity(stockNum) >= quantity) {
            val charge = pricingEngine.price(quantity, stockNum)
            if(paymentEngine.authorise(charge, cardNum)) {
                SUCCESS
            } else {
                AUTH_FAILED
            }
        } else {
            OUT_OF_STOCK
        }
    }
}

For the purpose of our demo we will have simple stubbed impementations of the three interfaces. In the real world we could have as many as needed, and these could be as complex as required. It's worth noting that a very common mistake made in DI is to assume that there should only ever be a trivial 'mock' implementation for testing and the 'real' implementation for production.

class PricingEngineStub : PricingEngine {
    override fun price(quantity: Int, stockNum: String) = quantity * 1.2
}

class StockCheckEngineStub : StockCheckEngine {
    override fun checkQuantity(stockNum: String) = if (stockNum.startsWith("AB")) 100 else 10
}

class PaymentEngineStub : PaymentEngine {
    override fun authorise(amount: Double, cardNum: String) = amount < 100
}

Having created our skeleton let's see how Koin itself works. The following main method creates an instance of Koin, configures it via a DSL, and then executes several helper methods to demonstrate how the components have been wired together:

fun main() {
    val koinInstance = koinApplication {
        printLogger()
        modules(
            module {
                // Configure an unnamed shop with unnamed dependencies
                //  all components are singletons
                single { Shop(get(), get(), get()) }
                single<PricingEngine> { PricingEngineStub() }
                single<StockCheckEngine> { StockCheckEngineStub() }
                single<PaymentEngine> { PaymentEngineStub() }

                // Configure a named shop with unnamed dependencies
                //  via a factory function
                factory(named("shop2")) { Shop(get(), get(), get()) }

                // Configure a named shop with named dependencies
                //  via a factory function
                factory(named("shop3")) {
                    Shop(get(named("pricing")),
                         get(named("stock")),
                         get(named("payment")))
                }

                single(named("pricing")) { PricingEngineStub() }
                single(named("stock")) { StockCheckEngineStub() }
                single(named("payment")) { PaymentEngineStub() }
            }
        )
    }

    val koin = koinInstance.koin

    showInjectionByType(koin)

    showInjectionByName(koin)

    showFindingByType(koin)

    showDynamicConfiguration(koin)

}

As with all Kotlin DSLs we are making copious use of functions which accept lambdas with receivers. For example, the module function accepts a lambda, which will in turn have a receiver of type Module.

The single and factory functions are methods of the Module type, the former creates an instance of a component as a Singleton whilst the latter accepts a lambda as a factory function. Note that components are registered by type as the default, but we can change this by supplying a Qualifier object. The named function creates a Qualifier which enables Dependency Injection by name.

Let's see these component declarations at work by instantiating them. In the code below we:

  • Create an instance of the Shop based solely on its type
  • Create instances based on the names 'shop2' and 'shop3'
  • Iterate over all the components which are of type Shop
  • Register a new component on the fly and then create it
private fun showInjectionByType(koin: Koin) {
    printTitle("Show DI By Type")
    val shop = koin.get<Shop>()
    val result = shop.makePurchase("AB12", 20, "DUMMY987")
    printTabbed(result)
}

private fun showInjectionByName(koin: Koin) {
    printTitle("Show DI By Name")

    val firstShop = koin.get<Shop>(named("shop2"))
    val secondShop = koin.get<Shop>(named("shop3"))

    val firstResult = firstShop.makePurchase("CD34", 20, "DUMMY987")
    printTabbed(firstResult)

    val secondResult = secondShop.makePurchase("CD34", 20, "DUMMY987")
    printTabbed(secondResult)
}

private fun showFindingByType(koin: Koin) {
    printTitle("Show Finding All")
    koin.getAll<Shop>().forEach {
        val result = it.makePurchase("AB12", 90, "DUMMY987")
        printTabbed(result)
    }
}

private fun showDynamicConfiguration(koin: Koin) {
    val tmp = Shop(PricingEngineStub(), StockCheckEngineStub(), PaymentEngineStub())
    koin.declare(tmp, named("shop4"))

    printTitle("Show Dynamic Configuration")
    val shop = koin.get<Shop>(named("shop4"))
    val result = shop.makePurchase("AB12", 20, "DUMMY987")
    printTabbed(result)
}

fun printTitle(title: String) = println("--- $title ---")
fun printTabbed(input: Any)  = println("\t$input")

Here's the output from the above functions

--- Show DI By Type ---
 	SUCCESS
--- Show DI By Name ---
	OUT_OF_STOCK
	OUT_OF_STOCK
--- Show Finding All ---
	AUTH_FAILED
	AUTH_FAILED
	AUTH_FAILED
--- Show Dynamic Configuration ---
 	SUCCESS

Koin Underneath the Hood

Koin works without any use of proxies, code generation or reflection. This makes it as usable in Android Apps and Ktor Microservices as standard Java programs. A Koin Container can be managed directly via the koinApplication function. Alternatively, you can use the startKoin function to register a container into the GlobalContext - allowing any type which inherits from 'KoinComponent' to automatically make use of DI. The lack of reflection does lead to some limitations, for example parameters to constructors are not automatically injected, but require an explicit call to get.

Conclusions

Koin offers a solution to the most pressing criticisms of DI frameworks. You can now use one DI tool across all your projects, as opposed to Dagger on Android and Spring in the Enterprise. Your components can be configured without the issues associated with XML and Annotations. Plus you can leverage all the functional features of Kotlin in combination with standard OO idioms.

Article By
blog author

Garth Gilmour

Head of Learning