Kotlin has some nice features, and one of them I find really useful and use regularly, is delegation. However, while it works well for one to one relationships (one interface delegated to one implementation), there are instances when one needs to use a proxy approach, where a main class implements an interface, and has not just one, but multiple instances of implementations of the same interface to delegate the calls to.
Currently I solve this issue manually, which can result in a lot of manual boilerplate writing. I’d like to avoid this, as this approach is something that can be safely generated by even annotation processors, however a language native solution (even if it’s just compile-time desugaring and codegen), would be incredibly useful.
A sample of the pattern I’m using:
interface MyStuff {
fun myCall(param: Any?)
}
class StuffImplOne: MyStuff {
override fun myCall(param: Any?) { /* do stuff 1 here */ }
}
class StuffImplTwo: MyStuff {
override fun myCall(param: Any?) { /* do stuff 2 here */ }
}
class StuffImplThree: MyStuff {
override fun myCall(param: Any?) { /* do stuff 3 here */ }
}
class StuffProxy: MyStuff {
private val children: List<MyStuff> // Initialised, of course
override fun myCall(param: Any?) {
children.forEach { it.myCall(param) }
}
}
This would come extremely handy in situations when a single call needs to be spread to multiple users - say, logging spread out to multiple outputs, or analytics to multiple endpoints.
It does not yet include your problem. Maybe you want to add to the discussion there. However I don’t think your use case is common enough to warrant a language feature, even though I understand it. But maybe I didn’t think of the area in programming where this is a common pattern ( I know you mentioned logging, but this is only a handfull of frameworks and most people don’t implement this themselfs)
I’m not sure that it is easy to do this automatically. Any automatic spreading would need to be compatible in that multiplexing is valid. This is not only having a Unit return type on all methods (otherwise how do you determine the return value), but also none of the implementations are allowed to have side effects that implicitly expect single invocation.
You’re describing the Gang-of-Four Composite Pattern here…
It’s not really something that I’d think should be implemented at the language level for delegation, but might prove useful as a compiler plugin for an annotation (as it’s a very common pattern). Something like
@Composite(MyStuff::class)
class Container: MyStuff {
...
}
and the compiler plugin could generate children and myCall to make it effectively:
class Container: MyStuff {
var children: List<MyStuff>(); // optional arg to annotation to make this private
override fun myCall(...params...) {
children.forEach { it.myCall(...params...); }
}
...
}
Probably useful to do for a few other Design Patterns as well
And there’s the question of how exceptions should be handled: pass through? log and continue? collect and re-throw as composite exception? etc. So, coming up with a general-purpose implementation - which doesn’t lead people into common mistakes - would be hard.
I’m not sure the idea of a compiler plugin is so good. While I love annotation processing/compiler plugins in general, I don’t like to have too much “magic” going on, without good reasons. It just makes sharing code/working with other people much harder.
I could support the idea of such a plugin if it supports many different coding patterns. That way it might get enough usage so it won’t just confuse new people when they see the code.
Also there are the problems @pdvrieze and @HughG pointed out. Your plugin would need to sovle all those issues, porbably by providing different solutions, which would lead to a complex system for something that in most cases is done in a few lines.
It seems as tho there are multiple different ways that this could be implemented that all conflict with one another. One way to make this easier tho would be by having a class that implements that interface and takes a list of objects of that interface and then does the delegation (let’s call it SuperWrapper. Then, in you actual classes, you can just have a constructor parameter or a property which is a list of those interfaces, and then just delegate the interface to the SuperWrapper. Something kinda like this:
interface Test {
fun doStuff1(arg1: Int, arg2 : String)
fun doStuff2(arg1: Any) : String
}
class TestSuperWrapper(val tests: List<Test>){
fun doStuff1(arg1: Int, arg2: String){
tests.forEach { it.doStuff1(arg1, arg2)}
}
fun doStuff2 (arg1: Any) : String{
return tests.map { it.doStuff2(arg1) }.join(",")
}
}
class MyClass(tests: List<Tests>) : Test by TestSuperWrapper(tests) {
// Other stuff
}
This doesn’t solve the issue of having to manually write the whole wrapper/proxy class. My goal was to have it automated, since it’s such a chore (especially if we’re talking about a massive interface). Your comment here just repeats the already existing “solution” that I wanted to simplify either via a language feature or a compiler plugin.
Well, the problem is that there is a lot of complex stuff that can’t really have sane defaults at all. For example, how would you handle return values? Should the language feature or the compiler plugin return the first result, the last result, combine them all in some way, or what? There’s also the issue of exceptions; however, it can be solved by just composing them all together into one exception. IMO, there are a lot of possible ways that this feature can be implemented, and all of them clash with one another.