rupinderjeet
rupinderjeet

Reputation: 2838

How to override a method with supertype-argument

I have a Fruit class like this.

open class Fruit(var taste: String) {

    open fun consume(from: Fruit) {
        taste = from.taste
    }
}

I have an Apple class that extends Fruit class like this.

class Apple(
    var color: String,
    taste: String
): Fruit(taste) {

    // caution: method overrides nothing
    override fun consume(from: Apple) {
        super.consume(from)
        color = from.color
    }
}

This is my usage code:

val fruit: Fruit = Apple(color = "green", taste = "sweet")
val badFruit = Apple(
    color = anyOf("red", "blue", "golden"),
    taste = anyOf("sour+", "sweet+", "chilli+")
)

fruit.consume(from = badFruit)

println("BadFruit: $badFruit")
println("InfectedFruit: $fruit")

Problem:

I can't override following method in Apple class:

override fun consume(from: Apple) {
    super.consume(from)
    color = from.color
}

To correctly override this method, I need to pass in an instance of Fruit class(as in super method). If I do this, I will always have to check if Fruit instance is actually Apple instance. But, shouldn't it just work with former because Apple extends Fruit?

How can I achieve such functionality that when I call consume() on fruit: Fruit = Apple(...), it actually calls Apple#consume() method?

What is a good way to do this?

Upvotes: 0

Views: 234

Answers (1)

Oliver O.
Oliver O.

Reputation: 721

While technical alternatives have been suggested in comments, I'd like to add another perspective. What we're seeing here is a class design problem which comes up when attempting to use inheritance for anything other than a true generalization/specialization relationship.

The example declares:

  1. Each Fruit must be able to consume another Fruit.
  2. An Apple is a kind of Fruit.

Then the idea is:

  • An Apple must not consume any kind of Fruit, but an Apple only. 🚫

If an Apple were really a Fruit, it would fully adhere to the Fruit's declaration and be able to consume another Fruit of any kind. As the intended apple Apple violates rule 1, is not really a Fruit and the language prevents you from declaring it as such.

Trying to work around this (e.g. via runtime checks in overridden methods) masquerades the underlying problem and introduces surprises to those using such classes.

Solution:

  • Use inheritance for true generalization/specialization relationships only. If it is 100% certain, no strings attached, that an apple is a fruit, inheritance is a perfect fit. Otherwise it is not.
  • In this case: Rethink the intended semantics:
    • What's the real meaning of consume?
    • Is there a notion of a fruit consuming an arbitrary (potentially incompatible specialization of another) fruit?
    • Or is it rather individual specializations of fruit which each have their own independent notion of consuming? Then there would be no common consume method at the Fruit level.

Copying Derived Classes via References To A Base Class

Answering the additional question in the comment:

how can I make sure this will copy properties of both SourFruit and CoreFruit?

I'd rather not express SweetFruit and SourFruit as specializations of a CoreFruit. Flavors such as sweet and sour are traits of a fruit and better expressed as properties.

But I could extend your example a bit and then suggest a class design which includes a clone() function providing a deep copy functionality on a base class Flavor. Note that the output shows different hash codes for cloned objects:

data class Fruit(var weight: Double, var flavors: MutableList<Flavor>) {
    fun clone(): Fruit {
        return Fruit(weight, flavors.map { it.clone() }.toMutableList())
    }
}

abstract class Flavor {
    abstract fun clone(): Flavor
}

class SweetFlavor(var intensity: Int, var isHealthy: Boolean) : Flavor() {
    override fun clone(): Flavor {
        return SweetFlavor(intensity, isHealthy)
    }
}

class SourFlavor(var intensity: Int) : Flavor() {
    override fun clone(): Flavor {
        return SourFlavor(intensity)
    }
}

fun main() {
    val apple = Fruit(0.2, mutableListOf(SweetFlavor(4, true), SourFlavor(2)))
    val lemon = Fruit(0.35, mutableListOf(SourFlavor(9)))
    val appleClone = apple.clone()

    println("apple: $apple")
    println("lemon: $lemon")
    println("appleClone: $appleClone")

    appleClone.weight += 0.5
    appleClone.flavors[0] = SweetFlavor(6, false)

    println("apple: $apple")
    println("appleClone: $appleClone")
}

Upvotes: 2

Related Questions