Effective Kotlin Item 35: Consider using dependency injection
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.
Marcin Moskala is a highly experienced developer and Kotlin instructor as the founder of Kt. Academy, an official JetBrains partner specializing in Kotlin training, Google Developers Expert, known for his significant contributions to the Kotlin community. Moskala is the author of several widely recognized books, including "Effective Kotlin," "Kotlin Coroutines," "Functional Kotlin," "Advanced Kotlin," "Kotlin Essentials," and "Android Development with Kotlin."
Beyond his literary achievements, Moskala is the author of the largest Medium publication dedicated to Kotlin. As a respected speaker, he has been invited to share his insights at numerous programming conferences, including events such as Droidcon and the prestigious Kotlin Conf, the premier conference dedicated to the Kotlin programming language.
Owen has been developing software since the mid 1990s and remembers the productivity of languages such as Clipper and Borland Delphi.
Since 2001, He moved to Web, Server based Java and the Open Source revolution.
With many years of commercial Java experience, He picked up on Kotlin in early 2015.
After taking detours into Clojure and Scala, like Goldilocks, He thinks Kotlin is just right and tastes the best.
Owen enthusiastically helps Kotlin developers continue to succeed.
Nicola Corti is a Google Developer Expert for Kotlin. He has been working with the language since before version 1.0 and he is the maintainer of several open-source libraries and tools.
He's currently working as Android Infrastructure Engineer at Spotify in Stockholm, Sweden.
Furthermore, he is an active member of the developer community.
His involvement goes from speaking at international conferences about Mobile development to leading communities across Europe (GDG Pisa, KUG Hamburg, GDG Sthlm Android).
In his free time, he also loves baking, photography, and running.
Emanuele is passionate about Android and has been fascinated by it since 2010: the more he learns, the more he wishes to share what he knows with others, which is why he started maintaining his own blog.
In his current role as Senior Android Developer at Mozio, he is now focusing on Kotlin Multiplatform Mobile: he has already given a couple of talks on this topic on various occasions, so far.