carbaj0
02/27/2022, 6:37 PMcarbaj0
02/27/2022, 6:38 PMcarbaj0
02/27/2022, 6:40 PMcarbaj0
02/27/2022, 6:47 PMcarbaj0
02/27/2022, 6:52 PMclass App {
companion object {
val service2: Service2 by lazy { with(Repository) { createService2() } }
val service1: Service1 by lazy { Service1 }
}
}
class Activity() {
fun onCreate() {
runBlocking {
with(Repository) { service1.doSomething1() }
service2.doSomething1()
}
}
fun onResume() {
runBlocking {
with(Repository) { service1.doSomething2() }
service2.doSomething2()
}
}
}
interface Repository {
suspend fun doSomething(): Either<String, String> =
"ok".right()
}
interface Service1 {
context(Repository)
suspend fun doSomething1(): Either<String, String>
context(Repository)
suspend fun doSomething2(): Either<String, String>
}
interface Service2 {
suspend fun doSomething1(): Either<String, String>
suspend fun doSomething2(): Either<String, String>
}
simon.vergauwen
02/28/2022, 8:45 AMsimon.vergauwen
02/28/2022, 8:48 AMServiceX
you probably don't want to expose SqlDelight
, Exposed
or Room
.
That is typically also considered counter-productive for testing and is a concern for coupling.
So for this reason people typically model their logic as an interface and hide the fact that they're using one of the above database implementations. So in those cases, it's good to hide the dependency.simon.vergauwen
02/28/2022, 8:57 AMService1
I don't see the need anymore to model it as an interface. It can just be top-level functions without losing any powers, but that is perhaps because this example is a bit too simple.
context(Repository)
suspend fun doSomething1(): Either<String, String>
A good example could be that in repo/service architectures we typically define all logic in Repo
hiding DB
& Network
related dependencies, and we use the service layer to compose the repo specific interfaces/algebras. I think with Context Receivers we'll replace the service layer with just top-level functions.
context(UserRepo, OrderRepo, EffectScope<ErrorType>)
suspend fun User.getAllOrders(): List<Order> {
val orderIds = selectOrderIds(userId).bind()
orderIds.map { orderId -> selectOrder(orderId).bind() }
}
simon.vergauwen
02/28/2022, 8:59 AMService1
or Service2
. Gaining more experience with the language feature, and personal preference or project needs might change how we think and feel about this though.carbaj0
02/28/2022, 6:45 PMcarbaj0
02/28/2022, 6:46 PMsimon.vergauwen
03/01/2022, 7:53 AMinterface APersistence { }
interface BNetwork { }
interface UseCase {
suspend fun program(): Either<E, A>
}
fun useCase(persistence: APersistence, network: BNetwork) = object : UseCase { }
The reason I take this approach over classes is that now you can easily make the constructor suspend
, which gives me the ability to do complex things upon construction in a safe way. Additionally, I never create a new type that can potentially expose additional states or functionality by accident (or by abuse). But it's perhaps just a matter of style.
I'm curious to see how with context receivers we can clean this up by replacing UseCase
with just a top-level function with context(APersistence, BNetwork)
.Javier
03/01/2022, 12:16 PMJavier
03/01/2022, 12:17 PMfun interface
Javier
03/01/2022, 12:19 PMup to date
or from cache
tasks.Javier
03/01/2022, 12:20 PM