Implementation by delegation should not delegate methods that are implemented in super-class/super-interface


#1

I have an interface with a number of implementations, some implemented using delegates. These classes have properties and some courtesy functions to set them with simpler syntax, and less code duplication.

data class Position(val x: Double, val y: Double)

class MutableBounds {
  var position: Position

  fun setPosition(x: Double, y: Double) {
    position = Position(x, y)
  }
}

The problem is when I want to delegate to this from some other class, setPosition is delegated, and I’d like to make modifications to var position in a sub-class and setPosition to not be delegated, but let only methods that would be undefined be delegated.

class SideEffectBounds(val bounds): MutableBounds by bounds {
  var position: Position
    get() = bounds.position
    set(value) {
      bounds.position = value
      doSideEffect()
    }

Problem: Given such a class as the above, setPosition() will be delegated to bounds and doSideEffect() will not be called.

Workarounds:

  1. declare setPosition inside this class and set the var manually. This is error-prone, and cumbersome to write and loses the supposed gains of delegates.
  2. Write more interfaces/classes, and only delegate to the exact interface that is required. This is very cumbersome and requires the addition of some abstract class and an interface.
  3. Use extension functions. This may not be suitable in all situations as sometimes polymorphism is required and different classes can optimise differently.
  4. Don’t use delegates. Write out some abstract class DelegateBounds that only delegates the intended functions and calls the rest on itself.

Another solution I tried was separating the concrete implementations into some AbstractMutableBounds

SideEffectBounds(val bounds): AbstractMutableBounds, MutableBounds by bounds

but this still had the same problem, although it did give me a warning. But I still find it to be unexpected that the methods of this class are delegated even though they are defined in this classes super-class. I expect that we would only delegate those methods that are not defined in this class or its super classes. However, the implementation is to delegate everything that can be delegated that is not defined in this class.

Can this behaviour be changed? Can there be a way to achieve this behaviour? If not, how else should I approach this?


#2

I agree with the point. What can be changed is difficult. Ideally the situation is an error (ambiguous implementation - assuming that the parent doesn’t implement the type itself) as using automatically using an implementation in the parent is hidden. In any case I think a warning would be in place for this. Additionally warnings don’t change the language so they can be implemented easier.


#3

It is a warning if the method is defined in a super-class, but if it is a default method in a super-interface there is no warning.

It’d be great to be able to specify either behaviour, eager or “lazy” delegation, the latter is what I’m after here. A kind of delegation that will prioritise its own super-types (classes and interfaces) method defs over delegation.

This would remove a lot of boilerplate and/or potentially confusing extension methods if this was possible to specify this behaviour.


#4

3 or more Members of the closing panel at Kotlin Conf stated delegation was their least favourite feature. Could that because of unexpected behaviour in the example above?


#5

For me what is greatly limiting usability of delegation is that delegated object must already implement the interface, so we can’t use it to add interface conformation to existing class that we cannot extend easily.


#6

For me what is greatly limiting usability of delegation is that delegated object must already implement the interface

This is the use-case of delegation. Your object can automatically implement an interface by delegating unimplemented methods to a delegate.

so we can’t use it to add interface conformation to existing class that we cannot extend easily.

Even if you could it wouldn’t result in an object that could be used in place of your delegate. Also would you want it to implement every public method of the delegate? I would find such a construct this incredibly hard to follow or reason about.


#7

I mean something like this. Lets say I have final class A { fun method1(), fun method2(), fun method3() } and interface B { fun method1(), fun method2() }. I would like to be able to do class C (a : A) : B by a <- it would take only public methods from B interface (without requiring class A to explicitly implement this interface, just in practice) - and possibly asking to implement in C any missing methods from B

This would be useful for example when we have some data structures coming from external libraries that we would prefer to wrap in our interface - currently it requires writing such proxy object by hand


#8

It sounds useful to remove some boilerplate, but would be somewhat brittle, any change to A, means you have to change B. It inverts the usual relationship between class and interface.