Home > Net >  Generic Interface inheritance
Generic Interface inheritance

Time:09-03

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.

  • Related