I got this code somebody wrote:
abstract class ListItem {
companion object {
private var id = 0
fun getUUID() = id++
}
}
fun getItem(uuid: Int): ListItem? {
for (dessert in Dessert.getAllDesserts())
if (dessert.id == uuid)
return dessert
for (fruit in Fruit.getAllFruits())
if (fruit.id == uuid)
return fruit
return null
}
Example of a sub-class:
data class Fruit(
val id: Int,
val resId: Int,
val name: String,
val origin: String,
val desc: String
): ListItem() {
companion object {
private val fruits = listOf(
Fruit(getUUID(), R.drawable.f1_durian, "Durian", "Indonesia", "Often dubbed the king of fruit, durian is an unusual tropical fruit that grows throughout Southeast Asia. A large spiky outer shell reveals a creamy, almost custard-like flesh, which, besides boasting a mildly sweet flavor, is notorious for being incredibly rank-smelling."),
I don`t get why ListItem is an abstract class. There are no unimplemented methods.
What's the motivation making ListItem an abstract class?
Has someone an idea?
Like the great example mr mcwolf gave, the 'problem' here is a conceptual one: although you could allow for the instantiation of a ListItem, what would its purpose be? It has no 'physical' meaning, if we're talking about food items.
However, it's important to note that an interface would be a better approach, as there are no actual benefits of using inheritance on the example you gave and it might even be misleading.
If, in this case, ListItem is turned into a 'marker' interface, it achieves the purpose of not being directly instantiable and helps with generic typing (if used later on, for example) while not breaking some other desired behavior (e.g. putting both Fruit and Dessert under a Food hierarchy).
Related
This is more of a theoretical Kotlin question about inheritance in relation to domain model classes.
Let's consider this scenario:
I'm building a system to handle books at a library so obviously some kind of book class is needed. I can create this easily with a data class:
data class Book (val title: String, val author: String)
So far so good. Now, this system also handles book reservations which will be a different model specifying pickup location and pickup date, e.g.
data class ReservedBook(val title: String, val author: String, val location: String, val pickupDate: String)
So, of course the ReservedBook class overlaps with the Book class, so I'm thinking what the best approach is to make this a bit more maintainable and understandable.
My initial though was to just make the ReservedBook inherit from the Book class, but Kotlin does not allow data classes to inherit from other data classes as it screws up the constructor/get/set methods.
The easy solution seems to include the Book as a property in the ReservedBook, e.g.
data class ReservedBook(val book: Book, val location: String, val pickupDate: String)
Easy enough, although a bit weird when creating an instance of a ReservedBook that you need to specify the Book inside, e.g.
val newReservedBook = ReservedBook(Book("MyTitle", "MyAuthor"), "Location B", "20-10-2022")
Hence, I was thinking if there was a smarter way of constructing such a setup. I was thinking of creating an abstract class called BaseBook or something like that and then have all the data classes that are more specific inherit from this, although writing the classes seem a bit cumbersome:
abstract class BaseBook(
open val title: String,
open val author: String
)
data class ReservedBook(
override val title: String,
override val author: String,
val location: String,
val pickupDate: String
) : BaseBook(title, author)
This, however, makes it a lot easier to create new instances as you can just write:
val newReservedBook = ReservedBook("MyTitle", "MyAuthor", "Location B", "20-10-2022")
So, I'm curious as what people think would be the best way of handling a situation like this. Maybe you also have a different proposal?
I think #TheLibrarian made a good point in the comments on the original question. Stating that it is weird to make a new book for a reservation.
To achieve this concept, we use some of Kotlin's most powerful tools.
Specifically, this is to make interfaces that operate as Traits (This is based in Scala but the concept applies)
For this kind of design, writing our code to interfaces rather than implementations enables us to design very freely within our applications.
/** Interface for Books called IBook
* We define our [title] and [author] as you would expect
* We also utilize the power of a trait to give us an easy constructor for creating an [IReservedBook]
*/
interface IBook {
val title: String
val author: String
fun reserve(location: String, pickupDate: String): IReservedBook {
return ReservedBook(this, location, pickupDate)
}
}
/** Interface for reserved Books, which are [IBooks]
* We define our [location] and [pickupDate] without having to redeclare from [IBook]
*/
interface IReservedBook : IBook {
val location: String
val pickupDate: String
}
/** The actual class for [IBook] very straight forward */
data class Book(
override val author: String,
override val title: String
) : IBook
/** The actual class for [ReservedBook] where the magic starts to happen
* First, we take in an already existing instance of [IBook] - We want to develop around the interface rather than the
* actual to keep ourselves flexible. This helps us if we have multiple kinds of Books such as a PictureBook or a Reference
*
* Second, we override our base interface for the [IReservedBook]
*
* Next we specify how this actually fulfills [IBook]
* by using the `IBook by Book(book.author, book.title)`
*
* This gives us the best of both worlds by making sure a Book can become a ReservedBook, we also do not need to create
* a new instance of a Book to make it Reserved, and we maintain the freedom to introduce new books, as well as reservations
* without having any impact on our previous code.
*/
data class ReservedBook(
val book: IBook,
override val location: String,
override val pickupDate: String
) : IReservedBook, IBook by Book(book.author, book.title)
fun library() {
val importantBook = Book("A very impressive person", "Kotlin Rules")
val importantReservation = ReservedBook(importantBook, "Nimbus", "Tomorrow")
val popularBook = Book("Dracula", "20 ways to avoid garlic")
val popularReservation = popularBook.reserve("Transylvania", "Right now")
}
This structure builds and allows very well for the composition that Kotlin encourages, as well as keeping things simple and smooth to make expansive domains for information.
I do not fully understand how variance in Generics work. In the code below the classes are as follows Any -> Mammals -> Cats. Any is the supertype, there is a parameter called from in the copy function
From what I understand about the out and in keywords, out allows reference to any of it's subtype, can only be produced not consumed.
in allows reference to any of it's supertype, can only be consumed not produced.
However in the copytest function we are instantiating the function copy. I gave it a catlist1 argument in the from parameter. Since the parameter has an out keyword wouldn't it mean that we can only input parameters that are a subtype of catlist2?
To top of my confusion I have seen many conflicting definitions, for instance , In Kotlin, we can use the out keyword on the generic type which means we can assign this reference to any of its supertypes.
Now I am really confused could anybody guide me on how all of these works? Preferably from scratch, thanks!
class list2<ITEM>{
val data = mutableListOf<ITEM>()
fun get(n:Int):ITEM = data[n]
fun add(Item:ITEM){data.add(Item)}
}
fun <T> Copy(from: list2<out T>, to:list2<T>){
}
fun copytest(){
val catlist1 = list2<Cat>()
val catlist2 = list2<Cat>()
val mammallist = list2<Mammal>()
Copy(catlist1,mammallist)
}
I think maybe you're mixing up class-declaration-site generics and use-site generics.
Class-declaration-site generics
Defined at the class declaration site with covariant out, it is true you cannot use the generic type as the type of a function parameter for any functions in the class.
class MyList<out T>(
private val items: Array<T>
) {
fun pullRandomItem(): T { // allowed
return items.random()
}
fun addItem(item: T) { // Not allowed by compiler!
// ...
}
}
// Reason:
val cowList = MyList<Cow>(arrayOf(Cow()))
// The declaration site out covariance allows us to up-cast to a more general type.
// It makes logical sense, any cow you pull out of the original list qualifies as an animal.
val animalList: MyList<Animal> = cowList
// If it let us put an item in, though:
animalList.addItem(Horse())
// Now there's a horse in the cow list. That doesn't make logical sense
cowList.pullRandomItem() // Might return a Horse, impossible!
It is not logical to say, "I'm going to put a horse in a list that may have the requirement that all items retrieved from it must be cows."
Use-site generics
This has nothing to do with the class level restriction. It's only describing what kind of input the function gets. It is perfectly logical to say, "my function does something with a container that I'm going to pull something out of".
// Given a class with no declaration-site covariance of contravariance:
class Bag<T: Any>(var contents: T?)
// This function will take any bag of food as a parameter. Inside the function, it will
// only get things out of the bag. It won't put things in it. This makes it possible
// to pass a Bag of Chips or a Bag of Pretzels
fun eatBagContents(bagOfAnything: Bag<out Food>) {
eat(bagOfAnything.contents) // we know the contents are food so this is OK
bagOfAnything.contents = myChips // Not allowed! we don't know what kind of stuff
// this bag is permitted to contain
}
// If we didn't define the function with "out"
fun eatBagContentsAndPutInSomething(bagOfAnything: Bag<Food>) {
eat(bagOfAnything.contents) // this is fine, we know it's food
bagOfAnything.contents = myChips // this is fine, the bag can hold any kind of Food
}
// but now you cannot do this
val myBagOfPretzels: Bag<Pretzels> = Bag(somePretzels)
eatBagContentsAndPutInSomething(myBagOfPretzels) // Not allowed! This function would
// try to put chips in this pretzels-only bag.
Combining both
What could be confusing to you is if you saw an example that combines both of the above. You can have a class where T is a declaration site type, but the class has functions where there are input parameters where T is part of the definition of what parameters the function can take. For example:
abstract class ComplicatedCopier<T> {
abstract fun createCopy(item: T): T
fun createCopiesFromBagToAnother(copyFrom: Bag<out T>, copyTo: Bag<in T>) {
val originalItem = copyFrom.contents
val copiedItem = createCopy(originalItem)
copyTo.contents = copiedItem
}
}
This logically makes sense since the class generic type has no variance restriction at the declaration site. This function has one bag that it's allowed to take items out of, and one bag that it's allowed to put items into. These in and out keywords make it more permissive of what types of bags you can pass to it, but it limits what you're allowed to do with each of those bags inside the function.
I'm writing external declarations for LeafletJS 1.8.0, a JavaScript library, using Kotlin 1.6.21.
The Polyline class has a function, getLatLngs() that can return any of these types:
Array<LatLng>
Array<Array<LatLng>>
Array<Array<Array<LatLng>>>
Of course the setter is easy to overload to handle a type-union
open external class Polyline {
open fun setLatLngs(latlngs: Array<LatLng>): Polyline<T>
open fun setLatLngs(latlngs: Array<Array<LatLng>>): Polyline<T>
open fun setLatLngs(latlngs: Array<Array<Array<LatLng>>>): Polyline<T>
}
However it's not possible to overload the getter
open external class Polyline {
// ERROR: Conflicting overloads
open fun getLatLngs(): Array<LatLng>
open fun getLatLngs(): Array<Array<LatLng>>
open fun getLatLngs(): Array<Array<Array<LatLng>>>
}
As a compromise I can set the return type to dynamic and add a comment so users can see the intention.
open external class Polyline {
open fun getLatLngs(): dynamic /* Array<LatLng> | Array<Array<LatLng>> | Array<Array<Array<LatLng>>> */
}
There's an open ticket KT-13108, and an update in November 2021 indicates direct Kotlin support for type unions won't be available until after Kotlin 1.7 is released.
Is there a better way of implementing the external function so the return type is type safe, or users can see the available types that might be returned, and handle each appropriately? What's the idiomatic practice?
Problem:
You are looking for an idiomatic way to describe union types for external declarations with:
Type safety (to ensure protect against runtime exceptions)
Output Type Annotations (for documentation purposes and also IDE code completion)
Control flow that handles each type in the union (so the union type can be used in Kotlin)
Long story short, for any general representation of a JS union-type in Kotlin, it's not possible to hit all three of these criteria without having more information about the instances of those types (due to type-erasure which I'll explain). But, In your case and the vast majority of cases, there is a nice trick to do this by using Kotlin's extension functions.
Solution:
There are two cases that I'll explain what to do to hit these criteria as best as possible:
Union of types that don't use generics (like Cat | Dog | string)
Union of types that do use generics (This is your case as Array<LatLng>, Array<Array<LatLng>>, and Array<Array<Array<LatLng>>> each use Generics for their types)
Union types that don't use Generics:
Say you had the Kotlin external declaration for AnimalOwner that is currently using dynamic as an output for its getPet method:
AnimalOwner.kt (draft)
/*
pretend right here that the package is declared
and file:JsModule decorators are present
*/
external class Cat
external class Dog
external class AnimalOwner {
fun setPet(pet: Cat) // sets the owner's pet to a Cat
fun setPet(pet: Dog) // sets the owner's pet to a Dog
fun setPet(pet: String) // sets the owner's pet to a String
fun getPet(): dynamic // Union of types (Cat, Dog, String)
}
One can specify an external interface to represent the output type. Then, using extension functions, one can define how to cast/morph each instance into each type (or return null if it can't):
Pet.kt
/*
pretend right here that the package is declared
However, JsModule decorators are NOT (and cannot be) present here
*/
// created an interface and gave it an arbitrary name that fits
// what the output to getPet would represent
sealed external interface Pet // we sealed Pet to disallow others from inheriting it
// Create extension functions with fitting names which cast/morph to each type
// (these are not defined externally, they are defined in Kotlin itself):
inline fun Pet.asCat(): Cat? = this as? Cat
inline fun Pet.asDog(): Dog? = this as? Dog
inline fun Pet.asString(): String? = this as? String
Now, we can replace the dynamic keyword in AnimalOwner with Pet (the interface just created):
AnimalOwner.kt (revised)
/*
pretend right here that the package is declared
and JsModule decorators are present
*/
external class Cat
external class Dog
external class AnimalOwner {
fun setPet(pet: Cat)
fun setPet(pet: Dog)
fun setPet(pet: String)
fun getPet(): Pet // <- changed from dynamic to Pet
}
We can now use AnimalOwner by calling each extension function and checking if it is null or not:
fun printPetOf(animalOwner: AnimalOwner) {
val pet = animalOwner.getPet()
pet.asCat()?.also { cat -> console.log("I have a Cat") }
pet.asDog()?.also { dog -> console.log("I have a Dog") }
pet.asString()?.also { animalStr -> console.log("I have a $animalStr") }
}
fun test() {
val johnSmith = AnimalOwner()
johnSmith.setPet(Cat()) // johnSmith has a cat
printPetOf(johnSmith) // console: "I have a Cat"
johnSmith.setPet(Dog()) // johnSmith now has a dog
printPetOf(johnSmith) // console: "I have a Dog"
johnSmith.setPet("Mouse") // johnSmith now has a Mouse
printPetOf(johnSmith) // console: "I have a Mouse"
}
Union types that do use Generics:
This case is a little more complicated due to type-erasure. Let's use a similar example to AnimalOwner where now the owner is specifying lists of Dogs, Cats, or a String of animals:
AnimalOwner.kt (draft)
/*
pretend right here that the package is declared
and JsModule decorators are present
*/
external class Cat
external class Dog
external class AnimalOwner {
fun setPets(pets: List<Cat>) // sets the owner's pets to be a list of Cats
fun setPets(pets: List<Dog>) // sets the owner's pets to be a list of Dogs
fun setPets(pets: String) // sets the owner's pets to a String
fun getPets(): dynamic // Union of types (List<Cat>, List<Dog>, String)
}
At this point, if we attempt to do the same procedure to create an output type as before, we run into a problem when creating casting/morphing functions:
Pets.kt (ERROR)
/*
pretend right here that the package is declared
However, JsModule decorators are NOT (and cannot be) present here
*/
sealed external interface Pets // we sealed Pets to disallow others from inheriting it
inline fun Pets.asCats(): List<Cat>? = this as? List<Cat> // Possible Bug
inline fun Pets.asDogs(): List<Dog>? = this as? List<Dog> // Possible Bug
inline fun Pets.asString(): String? = this as? String
Specifically, we must change the following code this as? List<Cat> and this as? List<Dog> because Generics Types like List<T> lose information on the generic parameter T at runtime. This loss of information is called type-erasure (for more information see here). We must replace this with this as? List<*> for both extension methods because we can't know generics at runtime. This now creates another problem, as of now we cannot delineate between a list of Dogs and a list of Cats. This is where we require some outside knowledge of instances of these lists and how JavaScript getPets() method treats them. This is project specific so for the sake of this example I am going to pretend I have done some research to determine this outside knowledge we speak of.
So let's say we found out that our corresponding JavaScript method for getPets() always represents the returning of an empty list as list of Cats.
Now we have enough information to correct our code to delineate List<Cats> and List<Dog> even though we only have access to List<*>:
Pets.kt (revised)
/*
pretend right here that the package is declared
However, JsModule decorators are NOT (and cannot be) present here
*/
sealed external interface Pets
inline fun Pets.asCats(): List<Cat>? {
val listOfSomething = this as? List<*>
return listOfSomething?.let {
if (it.isEmpty() || it[0] is Cat) {
#Suppress("UNCHECKED_CAST")
it as List<Cat>
} else {
null
}
}
}
inline fun Pets.asDogs(): List<Dog>? {
val listOfSomething = this as? List<*>
return listOfSomething?.let {
if (it.isNotEmpty() && it[0] is Dog) {
#Suppress("UNCHECKED_CAST")
it as List<Dog>
} else {
null
}
}
}
inline fun Pets.asString(): String? = this as? String
Now, in AnimalOwner, we can change the output type of getPets from dynamic to Pets:
AnimalOwner.kt (revised)
/*
pretend right here that the package is declared
and JsModule decorators are present
*/
external class Cat
external class Dog
external class AnimalOwner {
fun setPets(pets: List<Cat>)
fun setPets(pets: List<Dog>)
fun setPets(pets: String)
fun getPets(): Pets // <- changed from dynamic to Pets
}
We can then use AnimalOwner the same way as the non-Generic case:
fun printPetOf(animalOwner: AnimalOwner) {
val pets = animalOwner.getPets()
pets.asCats()?.also { cats -> console.log("I have Cats") }
pets.asDogs()?.also { dogs -> console.log("I have Dogs") }
pets.asString()?.also { animalsStr -> console.log("I have $animalsStr") }
}
fun test() {
val johnSmith = AnimalOwner()
johnSmith.setPets(listOf(Cat(), Cat())) // johnSmith has two cats
printPetOf(johnSmith) // console: "I have Cats"
johnSmith.setPets(listOf<Cat>()) // johnSmith has an empty room of cats (I wonder where they went)
printPetOf(johnSmith) // console: "I have Cats"
johnSmith.setPets(listOf<Dog>()) // johnSmith STILL has 0 cats (Schrodinger's cats?)
printPetOf(johnSmith) // console: "I have Cats"
johnSmith.setPets(listOf(Dog(), Dog(), Dog())) // johnSmith has 3 dogs
printPetOf(johnSmith) // console: "I have Dogs"
johnSmith.setPets("a Mouse, a Horse, and a Sheep") // johnSmith now has "a Mouse, a Horse, and a Sheep"
printPetOf(johnSmith) // console: "I have a Mouse, a Horse, and a Sheep"
}
I would approach this problem like this.
Step 1: Create an abstract external return type say LatLngResult
external interface LatLngResult
Step 2: Set this return type as the return type to your methods returning unions
open external class Polyline {
open fun getLatLngs(): LatLngResult
}
Step 3: Add extension functions to cast your return type as desired
inline fun LatLngResult.asArray1() = asDynamic<Array<LatLng>>()
inline fun LatLngResult.asArray2() = asDynamic<Array<Array<LatLng>>>()
inline fun LatLngResult.asArray3() = asDynamic<Array<Array<Array<LatLng>>>>()
Step 4: Use the function
val res: LatLngResult = polyline.getLatLngs()
// case 1
val array1 : Array<LatLng> = res.asArray1()
// case 2
val array2 : Array<Array<LatLng>> = res.asArray2()
// case 3
val array3 : Array<Array<Array<LatLng>>> = res.asArray3()
Note 1: Just like you would approache it in typescript, you still need to know when is it convinient to use array1, array2, array3
Note 2: Specifying types is still optional in kotlin, I just added them here to make this answer easily digestable
I need to display an image in an ImageView, so I need to make sure that the image url is not null first. Are these 3 options valid?
Answer data class
data class Answer(
val id: Long?,
val title: String?,
val answerImage: AnswerImage?
) {
data class AnswerImage(
val x0: AnswerImageData?,
val x1: AnswerImageData?,
val x2: AnswerImageData?
) {
data class AnswerImageData(
val id: String?,
val url: String?
)
}
}
Option 1
answer.answerImage?.let { answerImage ->
answerImage.x0?.let { answerImageData ->
answerImageData.url?.let {
//display image
}
}
}
Option 2
if (answer.answerImage?.x0?.url != null)
{
//display image
}
Option 3
answer.answerImage?.x0?.url?.let {
//display image
}
Short answer: yes.
Option 1: Would only be a good choice if you actually need to do more things with answerImage and answerImageData rather than just cast it safely. In this specific case, we don't have a use for declaring those variables explicitly. To conclude: option 1 in this case is not a very neat solution, but it does work.
Option 2: should work, because all attributes are immutable. The compiler can then deduce on the next line (inside if scope), that the url property will still be non-null.
Option 3: this is in my opinion the best one: it's the easiest one to process as a reader of the code, as you would generally finish it with code like this: .let { safeUrl -> .. }.
As specified by #Ken Van Hoeylandt all 3 options are valid, another valid option could be to use elvis operator:
fun attemptDisplayingImage(answer: Answer) {
val answerImage = answer.answerImage ?: return
val answerImageData = answerImage.x0 ?: return
val answerImageDataUrl = answerImageData.url ?: return
// display image
}
There's an interesting article about this topic here
Ken covered the answers (they're all fine and do the same thing, the last one is how the language is designed to be used really, nice and neat!) but I wanted to touch on your actual data model.
First, you say you need to check that an AnswerImageData's url isn't null. But the only reason it could be null, is because you've explicitly made it nullable, with a String? type. Is an AnswerImageData with a null url ever valid? Or does it always need to have one? I'm guessing it does, and I'm guessing it always needs an id too - so just make them non-null!
data class AnswerImageData(
val id: String,
val url: String
)
Now all your AnswerImageData objects are guaranteed to have non-null values - they're all valid in that sense, it's baked into your design. So you don't need to null check them anymore!
And the same goes for your other classes - can you have an AnswerImage with null values? This might be a trickier one, let's assume there needs to always be at least one AnswerImageData in an AnswerImage - in which case you can make the first non-null, and the others optional:
data class AnswerImage(
val x0: AnswerImageData,
val x1: AnswerImageData?,
val x2: AnswerImageData?
)
This isn't necessarily the best way to do this - I'd personally prefer a vararg parameter, or some kind of collection, so you can have an arbitrary number of AnswerImageDatas and do operations like .first(predicate) to loop over them all easily. But if you want exactly three slots, three parameters is a way to do it!
Same goes for Answer - I'm guessing that requires an id, title and answerImage - if so, don't let them be null. Enforce that valid structure through your types, it'll make your life a lot easier! And so will avoiding nullable types unless you actually need them!
I don't know if that applies to what you're doing, but it probably does, so it's worth mentioning. (This kind of thing is called *Domain-Driven Design if you want to look into it - basically enforcing the rules and structure of your business logic through the way you design your code, your types, your objects etc.)
All the answers above were great but I wanted to mention something. You declared your properties as nullable so I'm guessing you are getting them from somewhere else (from your data layer if you're familiar with clean architecture).
my recommendation is to create a domain model for your class and map the data to your domain model(which has non-null properties). this way you handle nulls in the mapper. it's cleaner and follows the separation of concerns and single responsibility principles.
interface Mapper<F, S> {
fun firstToSecond(first: F): S
fun secondToFirst(second: S): F
}
data class DataModel(
val id: Long?,
val title: String?,
val answerImage: AnswerImage?
)
data class DomainModel(
val id: Long,
val title: String,
val answerImage: AnswerImage
)
class DataToDomainMapper: Mapper<DataModel, DomainModel> {
override fun firstToSecond(first: DataModel): DomainModel {
return DomainModel(
id = first.id ?: -1,
title = first.title ?: "no title",
answerImage = first.answerImage ?: AnswerImage()
)
}
override fun secondToFirst(second: DomainModel): DataModel {
return DataModel(
id = second.id,
title = second.title,
answerImage = second.answerImage
)
}
}
this way you don't have to handle nulls anywhere else in your code. and for data validation, you can check the id not to be negative. I've shortened your models, but you get the idea
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?
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:
Each Fruit must be able to consume another Fruit.
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")
}