Hello guys, I've been doing a few experiments with...
# ktor
a
Hello guys, I've been doing a few experiments with dependency injection and I want to show you this approach using Kodein, any feedback is welcome! (Code in the first comment)
Copy code
fun Application.configureDI() {
    di {
        bind<GetAllUsersRepository>() with singleton { GetAllUsersRepository() }
        bind<GetAllUsersHandler>() with singleton { GetAllUsersHandler(instance()) }
    }
}

fun Application.configureRouting() {
    routing {
        get("/users", executeInvoke<GetAllUsersHandler>())
    }
}

inline fun <reified T : Handler> executeInvoke(): suspend PipelineContext<Unit, ApplicationCall>.(Unit) -> Unit = {
    val handler by closestDI().instance<T>()
    handler(this.call)
}

interface Handler {
    suspend operator fun invoke(call: ApplicationCall)
}

class GetAllUsersHandler(val getAllUsersRepository: GetAllUsersRepository) : Handler {
    override suspend fun invoke(call: ApplicationCall) {
        val users = getAllUsersRepository()
        call.respond(HttpStatusCode.OK, users)
    }
}

class GetAllUsersRepository {
    operator fun invoke() = listOf("<mailto:test@test.com|test@test.com>", "<mailto:other@other.com|other@other.com>")
}
s
🤷 Why bring an abomination which is DI to a beautiful functional language which is Kotlin. Everything is perfectly injectable via parameters/currying.
a
Hi @Sergey Aldoukhov if you could show a few examples I’ll appreciate it!
s
a
Thanks, I’ll check those resources!
s
In essence: • Constructor injection can replace DI framework using “Composition Root” pattern • Constructor injection forces you to refactor when your model is refined, DI lets you to get sloppy and obtain a ball of spaghetti overtime • Constructor is just a function. Any code that uses classes can also be done using functions • Composing a tree of classes via constructor injection is roughly equivalent to creating a set of curried functions.
a
Tbh, I don't see the point in this. Lots of manual wiring that easily can be replaced with DI framework
s
That stuff you posted above is • Hard to read • Hard to debug • IntelliJ won’t help you analyzing the code • New guy can’t just jump in and use your code, needs to spend time learning your library first • When your code grows, you’ll lose track yourself Generally, you’d want to write a general purpose library (as opposed to your business domain library) only if you’re a part of a very large org or you think of yourself a genius or you just experimenting and is not serious about this.
a
I was thinking about that and even I ask ChatGPT to make an example for me but I don't know how to do this 😆
Copy code
fun Application.module() {
    routing {
        get("/users") {
            this@module.createGetAllUsersHandler()(this.call)
        }
    }
}

fun Application.createGetAllUsersHandler(): GetAllUsersHandler {
    val getAllUsersRepository = GetAllUsersRepository(createJdbi())
    return GetAllUsersHandler(getAllUsersRepository)
}

class GetAllUsersHandler(private val getAllUsersRepository: GetAllUsersRepository) {
    suspend operator fun invoke(call: ApplicationCall) {
        val users = getAllUsersRepository.getAll()
        call.respond(HttpStatusCode.OK, users)
    }
}

class GetAllUsersRepository(private val jdbi: Jdbi) {
    fun getAll() {
        return jdbi.withHandleUnchecked { handle ->
            handle.createQuery(
                """
                    select id, email from users
                """.trimIndent()
            )
                .map { rs, _ ->
                    User(
                        id = rs.getString("id"),
                        email = rs.getString("email"),
                    )
                }
                .list()
        }
    }
}

fun Application.createJdbi(): Jdbi {
    val dbConfig = DatabaseConfig(
        host = environment.config.property("database.url").getString(),
        port = environment.config.property("database.url").getString(),
        database = environment.config.property("database.url").getString(),
        user = environment.config.property("database.username").getString(),
        password = environment.config.property("database.password").getString(),
    )

    return Jdbi.create(
        "jdbc:postgresql://$(databaseConfig.host):${dbConfig.port}/${dbConfig.database}?user=${dbConfig.user}&password=${dbConfig.password}"
    )
}

data class User(var id: String, var email: String)

data class DatabaseConfig(val host: String, val port: String, val database: String, val user: String, val password: String)
s
Don’t put routing directly to the module()… This is how I do it:
Copy code
fun Application.module() {
...
   val state = MyState(...)
...
   configureDeviceRest(state)
   configureUserRest(state)
   configureSupportRest(state)
...
}

in another file:

fun Application.configureUserRest(state: MyState) {
    routing {
        route("api/user") {
            userRestUnauthenticatedAPI(this, state)
        }
        authenticate("user-auth") {
            route("api/user") {
                userRestAPI(this, state)
            }
        }
    }
}

private fun userRestAPI(route: Route, state: MyState) {
    <http://route.post|route.post>("set_bla_bla") {
...
Note: composing MyState should only involve lightweight objects, if you need to open connections implement a lazy pattern within these lightweight objects.
a
MyState has all the handlers/controllers, repositories as properties?
Also, why not using extension functions for routes?
s
Yes, MyState is the composition root, has db connections, etc. Plain function with route as a parameter because private (only split into function to make routing table more readable) and I did not want to pop it on every route. I have multiple api’s on the same server and if it would be a public extension function, the separation wouldn’t be as clean.
a
what do you think about something like this:
Copy code
fun Application.module() {
    routing {
        get("/users", getAllUsersHandler())
    }
}

fun getAllUsersHandler(): suspend PipelineContext<Unit, ApplicationCall>.(Unit) -> Unit {
    return {
        val getAllUsersRepository = GetAllUsersRepository(application.createJdbi())
        val getAllUsersHandler = GetAllUsersHandler(getAllUsersRepository)
        getAllUsersHandler(call)
    }
}
s
This is fine for start, but when it grows you would probably need to move routing to separate files. Which will be easy since you would just move your plain functions around.
I have actually misunderstood you from the start, thought you’re writing your own di framework… But that doesn’t change the fact di makes the code harder to follow (not just read, but jump around the chain of dependencies).
a
hahah nope, I'm just playing with Ktor
next thing will be learning more about testing
not testing but "infrastructure", like we did with dependencies
s
Ktor is awesome. Mid project, I had to move %50 of code around to better structure it, and did it in an hour with 99% copy/paste, just minor adjustments to the code itself. All of this without a single bug. Same would be a nightmare with Java.
a
Yeah, I'm really enjoying playing around with it. And I will continue experimenting, honestly thank you for all your help, now I see it clearer