I have a set of component interfaces
interface ITest<I> {
operator fun invoke(p: I)
}
interface OTest<O> {
operator fun invoke(): O
}
interface IOTest<I, O> {
operator fun invoke(p: I): O
}
and a corresponding functional interface
interface TestAdder<T> {
fun add_f(p: T) {} //default impl
}
that gets inherited to add the components from above to respective (functional) collection interfaces
interface ITestSet<I> : TestAdder<ITest<I>> {
val i_v: I
fun i_f1(p: I) {} // default impl
fun i_f2(p: I) {} // default impl
fun i_f3(p: I) {} // default impl
}
interface OTestSet<O> : TestAdder<OTest<O>> {
val o_v: O
fun o_f1(p: O) {} // default impl
fun o_f2(p: O) {} // default impl
fun o_f3(p: O) {} // default impl
}
interface IOTestSet<I, O> : TestAdder<IOTest<I, O>> {
val i_v: I
val o_v: O
// same as ITestSet<I>
fun i_f1(p: I) {} // default impl
fun i_f2(p: I) {} // default impl
fun i_f3(p: I) {} // default impl
// same as OTestSet<O>
fun o_f1(p: O) {} // default impl
fun o_f2(p: O) {} // default impl
fun o_f3(p: O) {} // default impl
fun io_f1(p: I): O
...
}
So far, so unnecessay: ideally IOTestSet<I, O>
should inherit the functionality defined in ITestSet<I>
and OTestSet<O>
:
interface IOTestSet<I, O> : ITestSet<I>, OTestSet<O>, TestAdder<IOTest<I, O>> {
fun io_f1(p: I): O
...
}
but obviously the TestAdder<T>
interface introduces inconsistency within the inheritance chain.
This smells like an age-old, archetypal paradigm (and probably even an XY) problem, still it seems I have to ask:
Q: How to compose this inheritance chain?
Or what is the established/much better/less and not unnecessarily convoluted/more elegant design pattern?
CodePudding user response:
As for why this doesn't work, it is because you are inheriting from the same interface, just with different type arguments. And the reason why you can't do that is explained here.
Composition
When inheritance doesn't work, we can try composition. (See also: "Prefer Composition Over Inheritance")
Rather than having ITestSet
and OTestSet
inherit TestAdder
, which is the root cause of your "inheriting from the same interface but with different type arguments" problem, add get-only properties of type TestAdder
to ITestSet
, OTestSet
, as well as IOTestSet
.
Then IOTestSet
will be able to inherit ITestSet
and OTestSet
. The full code is as below:
// I have added variance modifiers where appropriate
interface ITest<in I> {
operator fun invoke(p: I)
}
interface OTest<out O> {
operator fun invoke(): O
}
interface IOTest<in I, out O> {
operator fun invoke(p: I): O
}
interface TestAdder<in T> {
fun add_f(p: T) {} //default impl
}
interface ITestSet<I> {
val i_v: I
val inputAdder: TestAdder<ITest<I>>
fun i_f1(p: I) {} // default impl
fun i_f2(p: I) {} // default impl
fun i_f3(p: I) {} // default impl
}
interface OTestSet<O> : TestAdder<OTest<O>> {
val o_v: O
val outputAdder: TestAdder<OTest<O>>
fun o_f1(p: O) {} // default impl
fun o_f2(p: O) {} // default impl
fun o_f3(p: O) {} // default impl
}
interface IOTestSet<I, O> : ITestSet<I>, OTestSet<O> {
val ioAdder: TestAdder<IOTest<I, O>>
fun io_f1(p: I): O
}
Implementing the interfaces
From what I can see, you can still implement this new set of interfaces in the same way as you did before - e.g. to implement ITestSet<String>
, you just do:
class Foo: ITestSet<String> {
override val i_v: String = "Some String"
override val inputAdder = object: TestAdder<ITest<String>> {
// implementation goes here...
}
}
Note that it is possible to provide a default implementation for inputAdder
:
interface ITestSet<I> {
val i_v: I
val inputAdder: TestAdder<ITest<I>> get() = object: TestAdder<ITest<I>> {
// implement your thing here...
}
...
}
but this recreates the object every time you access inputAdder
, which may not be the semantics you intended.
This brings me to another difference of this design, compared to your original design: you allow implementations to implement inputAdder
, outputAdder
, and ioAdder
in whatever way they like. This means that they are not necessarily implemented so that e.g. each ITestSet
always has the same inputAdder
, like in your original design, where the "inputAdder" of each ITestSet
is always itself. Of course, this isn't a problem if you have total control over the code.