Skip to content

Instantly share code, notes, and snippets.

@nomisRev
Last active September 13, 2021 22:48
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save nomisRev/1f91710ebec1709d4ce8059812482624 to your computer and use it in GitHub Desktop.
Save nomisRev/1f91710ebec1709d4ce8059812482624 to your computer and use it in GitHub Desktop.
Kotlin DI with receivers & interface delegation
import arrow.core.*
import memeid.UUID
data class User(val email: String, val name: String) {
companion object
}
data class ProcessedUser(val id: UUID, val email: String, val name: String) {
companion object
}
object ProcessingError
interface Repo {
suspend fun fetchUsers(): List<User>
}
fun MockRepo(): Repo = object : Repo {
override suspend fun fetchUsers(): List<User> =
listOf(
User("simon@arrow-kt.io", "Simon"),
User("raul@arrow-kt.io", "Raul"),
User("jorge@arrow-kt.io", "Jorge")
)
}
interface Persistence {
suspend fun User.process(): Either<ProcessingError, ProcessedUser>
suspend fun List<User>.process(): Either<ProcessingError, List<ProcessedUser>> =
traverseEither {
it.process()
}
}
fun MockPersistence(): Persistence = object : Persistence {
override suspend fun User.process(): Either<ProcessingError, ProcessedUser> =
if (email.contains(Regex("^(.+)@(.+)$"))) Either.Right(ProcessedUser(UUID.V4.squuid(), email, name))
else Either.Left(ProcessingError)
}
/*
* When using the generic method as shown below you define top-level functions
* which could be considered a "Service" which is typically composed by repos
* from the network or persistence layer.
* These functions are your business logic, which are in turn typically called by
* your routes or controllers.
*
* The biggest advantage of this approach is that this function explicitly defines
* everything that it uses in its signature.
* Because of that you can call if from anywhere that satisfies those constraints.
*/
/**
* The generic top-level function
* Enables the [getProcessUsers] function (or syntax) when the [Persistence] & [Repo] constraints are met.
*/
suspend fun <R> R.getProcessUsers(/* add any arguments as needed */): Either<ProcessingError, List<ProcessedUser>>
where R : Repo,
R : Persistence =
fetchUsers().process()
/**
* We define a class that satisfies both [Persistence] & [Repo] such that the
* [getProcessUsers] function automatically gets added through the top-level definition.
*
* We use Kotlin's interface delegation system to automatically implement the interfaces
* by passing runtime representations of them.
*/
class DataModule(
persistence: Persistence,
repo: Repo
) : Persistence by persistence, Repo by repo
suspend fun main(): Unit {
// This is your router { get { } } router definition or
// your Android launch { } or compose function.
// Generic top-level function automatically got enabled
val processedUsers = DataModule(MockPersistence(), MockRepo()).getProcessUsers()
println(processedUsers)
// Call the alternative approach
val processedUsers2 = DataModule2(MockPersistence(), MockRepo()).getProcessUsers2()
println(processedUsers2)
}
/**
* Another approach to defining [DataModule] could be using an interface and a function.
*/
interface DataModule2 : Persistence, Repo
fun DataModule2(persistence: Persistence, repo: Repo): DataModule2 =
object : DataModule2, Repo by repo, Persistence by persistence {}
/**
* When using this style you could also consider an alternative way of defining [getProcessUsers]
*/
suspend fun DataModule2.getProcessUsers2(/* add any arguments as needed */): Either<ProcessingError, List<ProcessedUser>> =
fetchUsers().process()
@nomisRev
Copy link
Author

nomisRev commented Jul 6, 2021

Discussion on Kotlin Slack concerning some of this:

https://kotlinlang.slack.com/archives/C5UPMM0A0/p1624026498160200

suspend fun <R> R.processUser(userId: Long): Unit
   where R : Persistence<User>,
         R : Repo<User>,
         R : Logger,
         R : Config,
         R : Processor<User> /*, R : ... */ {
  val user = userForId(userId)
  log("user for id: $user")
  val info = fetchInfoForUser(user)
  process(config, user, info)
}

So to satisfies this you need a larger composition of interfaces, which is often referred to as "god-class" or "god-dependency".
This is not actually an issue for this style of programming because the function and the "god-class" are completely decoupled and completely unaware of each other. Meaning that it automatically is also 100% testable with whatever techniques you prefer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment