janvladimirmostert
05/05/2023, 6:30 AMinterface Blah {
suspend fun doSomething()
}
class BlahImpl : Blah {
context(Connection)
suspend fun doSomething() { ... }
}
currently the above doesn't compile (and the compiler actually just outputs garbage without hinting that this is not allowed - KT-58442) unless you add the context receiver on the doSomething()
in the interface as well
interface Blah {
context(connection)
suspend fun doSomething()
}
which leads to my question, should the interface really know in what context(s) it will be used, doesn't that defeat the purpose of an interface?
the actual context in which the implementation runs sounds like implementation detail that now pollutes the interface.
maybe the same can be said for suspend, should that really be in the interface too?
that being said, if it's not in the interface, then you would be forced to specify at the call-site which implementation you are referring to which would then either require a context or no context or to call it from a coroutineScope or just normally.simon.vergauwen
05/05/2023, 6:36 AMFragment
or Ktor PipelineContext
.
In your example the context of the method is another interface
so it’s still completely abstract.
IMO this gives a “stronger” coupling to Connection
or databases. If you want the interface to be more flexible then that context
shouldn’t be there but it kind-of depends on your use-cases and requirements for the `interface`/contractIvan Pavlov
05/05/2023, 9:01 AMinterface Blah {
suspend fun doSomething()
}
class BlahImpl : Blah {
context(Connection)
suspend fun doSomething() { ... }
}
suspend fun acceptsBlah(blah: Blah) {
// if it's BlahImpl we must provide context, but it's unknown because we use interface
doSomething()
}
Youssef Shoaib [MOD]
05/05/2023, 1:48 PMCtx
and hence the requirement will be passed all the way up the call chain until the piece of code that knows that you use BlahImpl
Ben Woodworth
05/05/2023, 4:56 PMcontext(Connection)
on the class, though for your use case I'm not sure that's what you'll want.
But yeah, like Youssef said, the context on doSomething needs to be in the interface function's signature so they're the same signature. And if the Impl needs a specific kind of connection, make the interface 's connection genericjanvladimirmostert
05/14/2023, 3:18 PMcontext
the interface too.
Delegates seem like the better choice
interface Blah {
fun doSomething()
}
class FakeBlah : Blah {
override fun doSomething() {
TODO("Not yet implemented")
}
}
class RealBlah : Blah {
override fun doSomething() {
TODO("Not yet implemented")
}
}
class BlahApp(blah: Blah): Blah by blah {
constructor() : this(blah = RealBlah())
}
I can now have connection in the real implementation and no connection in the fake implementation
class RealBlah(val connection: Connection) : Blah {
override fun doSomething() {
// do something with connection
}
}
seems cleaner than polluting the interface with a context(Connection)
Youssef Shoaib [MOD]
05/14/2023, 3:42 PMcontext(Connection) class RealBlah: Blah
, and the BlahApp
can either propagate the context requirement up, or simply provide it there.
The difference between the 2 is that by having context
in the interface, you're taking in a Connection
each time doSomething
is called. This is useful if Connection
is a value that changes often like a Transaction for instance. In the 2nd case, Connection
is actually an implementation detail that needs to only be provided once, hence why you can just pass it in to the constructor, and again context receivers support that use case through context classesjanvladimirmostert
05/14/2023, 3:50 PMinterface Blah {
fun doSomething()
}
class FakeBlah : Blah {
override fun doSomething() {}
}
context (Connection)
class RealBlah() : Blah {
override fun doSomething() {}
}
class BlahDelegate(blah: Blah) : Blah by blah
fun main() {
BlahDelegate(blah = FakeBlah())
with(Connection()) {
BlahDelegate(blah = RealBlah())
}
}