article banner

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.

Example usage:

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.

Playground

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" } }