article banner (priority)

Effective Kotlin Item 35: Consider using dependency injection

This is a chapter from the book Effective Kotlin. You can find it on LeanPub or Amazon.

One of the most important patterns in modern programming is dependency injection. It is a technique that allows you to create objects that depend on other objects without creating these dependencies on your own. This is important because it makes your code more flexible and reusable. In Kotlin, the most popular way to do dependency injection is to use constructor-based dependency injection.

The idea behind constructor-based dependency injection is simple. Instead of creating instances of the classes your class needs to compose (dependencies of this class), you specify the types of these dependencies in the constructor of your class. This way, instead of creating its dependencies on its own, a class receives them from the outside. This is called inversion of control. Let me show you an example of a class that creates its dependencies:

class UserService { private val userRepository = DatabaseUserRepository() private val emailService = MailchimpEmailService() fun registerUser(email: String, password: String) { val user = userRepository.createUser(email,password) emailService.sendEmail(user.email, "Welcome!") } }

And this is how this class would look if we used constructor-based dependency injection:

class UserService( private val userRepository: UserRepository, private val emailService: EmailService ) { fun registerUser(email: String, password: String) { val user = userRepository.createUser(email,password) emailService.sendEmail(user.email, "Welcome!") } }

Dependency injection can also be done using setters or property delegates, but I suggest constructor-based dependency injection because it’s easier to test and makes dependencies explicit.

The advantages of dependency injection are:

  • It makes dependencies explicit. When you look at the constructor of a class, you know what dependencies it has.
  • It makes testing easier. You can easily mock dependencies and test your class in isolation.
  • It makes your code more flexible. You can easily replace dependencies with other implementations.
  • It makes your code more reusable. If you define your dependency with an interface, you can reuse your class in different contexts by providing different implementations of dependencies.

When we use dependency injection, we delegate the actual object creation to outside our active object. Nevertheless, we finally need to define how these dependencies are created. For this, it is popular to use a dependency injection framework: a library that allows you to define how dependencies are created and is then used to create concrete dependencies for you. Consider the following structure of classes:

interface DatabaseClient { /* ... */ } class PostgresDatabaseClient : DatabaseClient { /* ... */ } interface UserRepository { /* ... */ } class DatabaseUserRepository( private val databaseClient: DatabaseClient ) : UserRepository { /* ... */ } interface EmailClient { /* ... */ } class MailchimpEmailClient : EmailClient { /* ... */ } interface EmailService { /* ... */ } class MailchimpEmailService( private val emailClient: EmailClient ) : EmailService { /* ... */ } class UserService( private val userRepository: UserRepository, private val emailService: EmailService ) { /* ... */ }

If we used Koin, a popular Kotlin dependency injection framework, this is how we could define how each dependency is created:

val userModule = module { single<DatabaseClient> { PostgresDatabaseClient() } single<UserRepository> { DatabaseUserRepository(get()) } single<EmailClient> { MailchimpEmailClient() } single<EmailService> { MailchimpEmailService(get()) } single { UserService(get(), get()) } }

The created module can be used to initialize and start the dependency injection framework; then, we can get instances of dependencies that are defined all around our project.

val userRepo: UserRepository by inject() val userService: UserService = get()

Dependency injection frameworks have several big advantages:

  • We need to define only once how components should be created. After that, we can just define what dependencies we need and they are created by our dependency injection framework.
  • We can easily replace dependencies with other implementations. We just need to change the definition of how dependencies are created.
  • We can easily mock dependencies in tests. We can provide different implementations of dependencies for tests.
  • We can easily reuse our classes in different contexts. We can provide different implementations of dependencies for different contexts.
  • We can easily create singletons: dependencies that are created only once and then reused.

Summary

  • Dependency injection is a pattern that allows us to create dependencies outside our class.
  • Dependency injection makes dependencies explicit, makes testing easier, and makes code more flexible and reusable.
  • Dependency injection frameworks allow us to define how dependencies are created and use them all around our project.