Exercise: DSL-based dependency injection library
Your task is to implement a simple dependency injection library. It should be based on the Registry
class, which should be used to register dependencies. It should have the following methods:
register
- registers a normal dependency that is created every time it is needed. It should take a type and a lambda expression that returns an instance of that type. In the scope of this lambda expression, you should be able to use Registry
to get other dependencies. This function should have both an inline version with reified type, and a non-inline version with the KClass
parameter.singleton
- registers a singleton dependency that is created only once and then reused. It should take a type and a lambda expression that returns an instance of that type. In the scope of this lambda expression, you should be able to use Registry
to get other dependencies. This function should have both an inline version with a reified type, and a non-inline version with the KClass
parameter.get
- returns an instance of a given type. If the type is registered as a singleton, it should return the same instance every time. If the type is registered as a normal dependency, it should return a new instance every time it is called. This function should have both an inline version with reified type, and a non-inline version with the KClass
parameter.exists
- returns true if a given type is registered, otherwise it returns false. This function should have both an inline version with a reified type, and a non-inline version with the KClass
parameter.
You should also implement a registry
function to create a Registry
instance in DSL style. It should take a lambda expression with Registry
as a receiver, and it should return a Registry
instance. In the scope of this lambda expression, you should be able to use Registry
to register dependencies.
data class UserConfiguration(val url: String)
interface UserRepository {
fun get(): String
}
class RealUserRepository(
private val userConfiguration: UserConfiguration,
) : UserRepository {
override fun get(): String =
"User from ${userConfiguration.url}"
}
class UserService(
private val userRepository: UserRepository,
private val userConfiguration: UserConfiguration,
) {
fun get(): String = "Got ${userRepository.get()}"
}
fun main() {
val registry: Registry = registry {
singleton<UserConfiguration> {
UserConfiguration("http://localhost:8080")
}
normal<UserService> {
UserService(
userRepository = get(),
userConfiguration = get(),
)
}
singleton<UserRepository> {
RealUserRepository(
userConfiguration = get(),
)
}
}
val userService: UserService = registry.get()
println(userService.get())
// Got User from http://localhost:8080
val ur1 = registry.get<UserRepository>()
val ur2 = registry.get<UserRepository>()
println(ur1 === ur2) // true
val uc1 = registry.get<UserService>()
val uc2 = registry.get<UserService>()
println(uc1 === uc2) // false
}
This problem can either be solved in the below playground or you can clone kotlin-exercises project and solve it locally. In the project, you can find code template for this exercise in advanced/reflection/DependencyInjection.kt. You can find there example usage and unit tests.
Once you are done with the exercise, you can check your solution here.
import org.junit.Test
import kotlin.reflect.KType
import kotlin.reflect.typeOf
import kotlin.test.assertEquals
import kotlin.test.assertSame
// TODO
data class UserConfiguration(val url: String)
interface UserRepository {
fun get(): String
}
class RealUserRepository(
private val userConfiguration: UserConfiguration,
) : UserRepository {
override fun get(): String =
"User from ${userConfiguration.url}"
}
class UserService(
private val userRepository: UserRepository,
private val userConfiguration: UserConfiguration,
) {
fun get(): String = "Got ${userRepository.get()}"
}
fun main() {
val registry: Registry = registry {
singleton<UserConfiguration> {
UserConfiguration("http://localhost:8080")
}
normal<UserService> {
UserService(
userRepository = get(),
userConfiguration = get(),
)
}
singleton<UserRepository> {
RealUserRepository(
userConfiguration = get(),
)
}
}
val userService: UserService = registry.get()
println(userService.get())
// Got User from http://localhost:8080
val ur1 = registry.get<UserRepository>()
val ur2 = registry.get<UserRepository>()
println(ur1 === ur2) // true
val uc1 = registry.get<UserService>()
val uc2 = registry.get<UserService>()
println(uc1 === uc2) // false
}
class RegistryTest {
@Test
fun `should get registered instance`() {
val registry = Registry()
registry.register(typeOf<String>()) { "ABC" }
assertEquals("ABC", registry.get<String>())
}
@Test
fun `should get registered instance with type`() {
val registry = Registry()
registry.register<String> { "ABC" }
assertEquals("ABC", registry.get<String>())
}
@Test
fun `should get registered single instance`() {
val registry = Registry()
registry.singleton(typeOf<String>()) { "ABC" }
assertEquals("ABC", registry.get<String>())
}
@Test
fun `should get registered single instance with type`() {
val registry = Registry()
registry.singleton<String> { "ABC" }
assertEquals("ABC", registry.get<String>())
}
@Test
fun `should return the same singleton instance`() {
val registry = Registry()
class A
registry.singleton(typeOf<A>()) { A() }
val instance1 = registry.get<A>()
val instance2 = registry.get<A>()
assertSame(instance1, instance2)
}
@Test
fun `should return the same singleton instance with type`() {
val registry = Registry()
class A
registry.singleton<A> { A() }
val instance1 = registry.get<A>()
val instance2 = registry.get<A>()
assertSame(instance1, instance2)
}
@Test
fun `should construct instance using registry`() {
val registry = Registry()
class B
class A(val b: B)
registry.register<A> { A(get()) }
registry.singleton<B> { B() }
val instance = registry.get<A>()
assertSame(instance.b, registry.get<B>())
}
@Test
fun `should respond to exists`() {
val registry = Registry()
registry.register<String> { "ABC" }
assertEquals(true, registry.exists<String>())
assertEquals(false, registry.exists<Int>())
}
@Test
fun `should respond to exists with type`() {
val registry = Registry()
registry.register<String> { "ABC" }
assertEquals(true, registry.exists(typeOf<String>()))
assertEquals(false, registry.exists(typeOf<Int>()))
}
@Test
fun `should throw exception when not exists`() {
val registry = Registry()
registry.register<String> { "ABC" }
assertThrows<IllegalArgumentException> {
registry.get<Int>()
}
}
@Test
fun `should throw exception when not exists with type`() {
val registry = Registry()
registry.register<String> { "ABC" }
assertThrows<IllegalArgumentException> {
registry.get(typeOf<Int>())
}
}
@Test
fun `should create instance using DSL`() {
val registry = registry {
register<String> { "ABC" }
}
assertEquals("ABC", registry.get<String>())
}
@Test
fun `should create user service`() {
val registry: Registry = registry {
singleton<UserConfiguration> {
UserConfiguration("http://localhost:8080")
}
register<UserService> {
UserService(
userRepository = get(),
userConfiguration = get(),
)
}
singleton<UserRepository> {
RealUserRepository(
userConfiguration = get(),
)
}
}
val userService: UserService = registry.get()
assertEquals("Got User from http://localhost:8080", userService.get())
val ur1 = registry.get<UserRepository>()
val ur2 = registry.get<UserRepository>()
assert(ur1 === ur2)
val uc1 = registry.get<UserService>()
val uc2 = registry.get<UserService>()
assert(uc1 !== uc2)
}
}
inline fun <reified T: Throwable> assertThrows(operation: () -> Unit) {
val result = runCatching { operation() }
assert(result.isFailure) { "Operation has not failed with exception" }
val exception = result.exceptionOrNull()
assert(exception is T) { "Incorrect exception type, it should be ${T::class}, but it is $exception" }
}
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.