Reputation: 2838
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")
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
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:
Fruit
must be able to consume
another Fruit
.Apple
is a kind of Fruit
.Then the idea is:
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:
consume
?consume
method at the Fruit
level.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