No history yet

Introduction to Dependency Injection

What is Dependency Injection?

Imagine you're a chef in a busy restaurant. Your main job is to cook amazing dishes. To do this, you need ingredients like vegetables, meat, and spices. Now, what if you had to grow your own vegetables and raise your own cattle every time you needed to make a meal? It would be incredibly inefficient. You'd be a farmer, a butcher, and a chef all at once. Your kitchen would be tightly coupled to your farm.

Instead, a well-run kitchen has its ingredients delivered. The chef simply asks for what they need, and it's provided. The chef can then focus entirely on cooking. This separation of roles is the core idea behind Dependency Injection (DI).

In software, a "dependency" is an object that another object needs to do its job. Dependency Injection is a design pattern where an object receives its dependencies from an external source rather than creating them itself.

Let's look at a simple example without DI. Here, our Car class is responsible for creating its own Engine. The Car depends on the Engine, and it creates its own instance of it.

class Engine {
    fun start() {
        println("Engine is running.")
    }
}

class Car {
    // The Car creates its own Engine instance.
    // This is a tight coupling.
    private val engine = Engine()

    fun drive() {
        engine.start()
        println("Car is moving.")
    }
}

This works, but it's inflexible. The Car is permanently stuck with that specific Engine. What if we want to use a different kind of engine, like an electric one? Or what if we want to test the Car without starting a real engine? We can't easily do that. The Car and Engine are tightly coupled.

Injecting the Dependency

Now let's apply Dependency Injection. Instead of the Car creating its Engine, we'll provide, or "inject," the Engine from the outside. The most common way to do this is through the class's constructor.

// We can define a common interface for different engines.
interface Engine {
    fun start()
}

// A specific implementation of the Engine.
class GasEngine : Engine {
    override fun start() {
        println("Gas engine is running.")
    }
}

class Car(private val engine: Engine) { // The Engine is injected!
    fun drive() {
        engine.start()
        println("Car is moving.")
    }
}

fun main() {
    // We create the dependency first.
    val myEngine = GasEngine()
    
    // Then we inject it into the Car.
    val myCar = Car(myEngine)
    myCar.drive()
}

Notice the difference? The Car class no longer knows or cares how to create an Engine. It just knows it needs some kind of Engine to function. This approach is called loose coupling.

Dependency Injection (DI) is a design pattern that is widely used in modern software development to achieve loose coupling between components, improve testability, and modularize the code.

Benefits and Frameworks

So why go through this effort? Loosely coupled code gives us several key advantages:

BenefitDescription
Increased FlexibilityYou can easily swap out different implementations of a dependency. For example, you could inject an ElectricEngine into our Car without changing the Car class at all.
Improved TestabilityWhen testing, you can inject "mock" or fake versions of dependencies. This allows you to test a class in isolation without worrying about its dependencies' complex logic.
Better Code OrganizationDI encourages you to break your application into smaller, more manageable modules that are independent of each other. This makes the codebase easier to understand and maintain.

As applications grow, managing all these dependencies manually can become complicated. Imagine having to create and pass dozens of objects around. That's where DI frameworks come in.

DI frameworks are tools that automate the process of creating and injecting dependencies. You tell the framework what depends on what, and it handles the rest. In the Kotlin world, some popular frameworks include:

  • Koin: A pragmatic and lightweight DI framework for Kotlin.
  • Hilt (for Android): A dependency injection library for Android that reduces the boilerplate of doing manual DI in your project.
  • Dagger: A fully static, compile-time dependency injection framework for both Java and Kotlin.

These frameworks act like a master chef's assistant, ensuring every component gets the exact dependencies it needs, right when it needs them.

Quiz Questions 1/5

In the chef and kitchen analogy, what does the practice of having ingredients delivered (instead of the chef growing them) represent in software development?

Quiz Questions 2/5

What is the primary characteristic of 'tightly coupled' code?