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.
Related
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).
I'm trying to reduce boilerplate on something I'm working on and wondering if something is possible - I suspect it's not but was looking for confirmation
class Something<T> {
private val list = mutableListOf<T>()
fun addToList(value: T) = list.add(value) }
So if I wanted to use this with a class like:
class Data(number: Int, letter: Char)
I'd have to use addToList like:
addToList(Data(1,"a"))
Is there some way to use the supplied type T to construct the method addToList dynamically? So that the class would be instantiated like:
val thing = Something<Data>()
but then addToList were called like
addToList(1,"a")
Like I said, don't think this is possible but was looking for confirmation.
What I was really trying to do was come up with something that would allow me to do this without declaring Data at all, but instead just define the structure and the subsequent addToList method when Something() was instantiated - not sure if I have described this all that well but if anyone has any suggestions in general around that I'd be grateful!
Thanks!
There are Pair and Triple tuple classes provided in the standard library which allows you to avoid declaring a class for simple combinations of values. If you need more than 3 parameters of different types, you'd need to create your own class or use a library that provides larger tuple classes. If all types are the same, you can use List instead of a tuple.
In my opinion even Triple is pushing it and anything with more than two distinct properties should just have its own data class defined.
class Something<A, B> {
private val list = mutableListOf<Pair<A, B>>()
fun addToList(valueA: A, valueB: B) = list.add(Pair(valueA, valueB))
}
val something = Something<Int, String>()
something.addToList(1, "a")
An alternate approach if you want to keep the flexibility of your Something class to hold anything would be to use an extension function.
class Something<T> {
private val list = mutableListOf<T>()
fun addToList(value: T) = list.add(value)
}
fun <A, B> Something<Pair<A, B>>.addToList(valueA: A, valueB: B) =
addToList(Pair(valueA, valueB))
val something = Something<Pair<Int, String>>()
something.addToList(1, "a")
I have a Java class that holds generic information on databse entities (i.e. their id).
#Data
public class DbEntity {
protected final String id;
public DbEntity(String id) {
this.id = id;
}
}
We use Lombok #Data to generate getters, toString, equals...
In Java I would simply extend this class and add #Data once again.
#Data
class JavaSubClass extends DbEntity {
public JavaSubClass(String id) {
super(id);
}
}
In a newer service we use Kotlin but would like to reuse standard classes such as DbEntity.
My first approach was to simply declare a data class such as
data class SubClass1(val id: String, val name: String) : DbEntity(id)
Accidental override: The following declarations have the same JVM signature (getId()Ljava/lang/String;):
fun <get-id>(): String defined in com.demo.SubClass1
fun getId(): String! defined in com.demo.SubClass1
After some reading I found several solutions, all of which I'm not super happy with.
Don't use data classes. This works but leaves me with the task of implementing equals etc.
class SubClass4(id: String, val name: String) : DbEntity(id)
Duplicate the field. This works but we end up with two fields that could go out of sync.
data class SubClass3(val subId: String, val name: String) : DbEntity(subId)
Assign a different name to the getter. This fundamentally also duplicates the field, but hides the getter.
data class SubClass2(#get:JvmName("getId_") val id: String, val name: String) : DbEntity(id)
As I said, I'm not happy with any of the solution presented above. Having an abstract super class or an interface instead would certainly be more appropriate. However the Entity class resides in a library that primarily Java projects depend on. I'm hesitant to change it just because of a new Kotlin dependnecy.
Did anyone encounter similar issues and has advice on how to solve them?
As a workaround, until KT-6653 - Kotlin properties do not override Java-style getters and setters is fixed, I would go for a variant of your point 3, i.e.:
data class SubClass(#get:JvmName("bogusId") private val id: String, val name: String) : DbEntity(id)
The benefit of this variant is, that you always access the "original" getId-function. You will not use the bogusId()-function as it is not visible/accessible (accessing it via reflection makes no sense... you are only interested in the actual id-field). This works and looks similar for both sides: from Java as also from Kotlin. Still, under the hood this variant uses 2 fields, but in the best case you can just replace it in future with something like:
data class SubClass(override val id: String, val name : String) : DbEntity(id)
I (often) have a resource with two states, pre-created and post-created, where both states have the same fields except for an id field. id is null in the pre-created state and non-null in the post-created state.
I would like to define and use this resource in a clean and type-safe way.
It's common to represent this ID field as a nullable, which handles both scenarios with minimal boilerplate in the class definition. The problem is that it creates a lot of boilerplate in the business logic because you can't assert whether a resource is pre-created or post-created by looking at its type.
Here is an example of the nullable approach:
data class Resource(val id: String?, val property: String)
This is simple to define, but not as simple to handle with due to lack of compile-time guarantees.
Here's an example of a more type-safe approach:
sealed class Resource(val property: String) {
class WithoutID(property: String): Resource(property)
class WithID(val id: String, property: String): Resource(property)
}
This allows me to pass around Resource.WithID and Resource.WithoutID, which have all the same fields and methods, except for id.
One inconvenience with this type-safe approach is that the resource definition code gets quite bloated when you have many property fields. This bloating makes the code harder to read.
I'm wondering if there's an alternative approach with less boilerplate, or if Kotlin has any features that make this kind of thing simpler.
What about defining
sealed class MayHaveId<T> { abstract val record: T }
class WithId<T>(val id: String, override val record: T): MayHaveId<T>()
class WithoutId<T>(override val record: T): MayHaveId<T>()
class Resource(val property: String)
// and other similar types
and using WithId<Resource> and WithoutId<Resource>? In Scala you could add an implicit conversion from MayHaveId<T> to T, but not in Kotlin, alas, nor can you write : T by record. Still should be clean enough to use.
One of the options is to get into composition relying on properties inside interfaces.
interface Resource {
val property: String
}
interface WithId : Resource {
val id: Int
}
interface WithOtherField : Resource {
val otherField: Any
}
class WithoutIdImpl(override val property: String) : Resource
class WithIdImpl(override val id: Int, override val property: String) : WithId
class WithIdAndOtherField(
override val id: Int,
override val otherField: Any,
override val property: String) : WithId, WithOtherField
I didn't get from your example, how you're going to switch between two states of Resource. So probably there is a gap to overcome.
Probably, Smart casts will allow to switch states.
I have an immutable object:
class Foo(
val name: String,
val things: List<Thing>
)
A third party lib creates the Foo object with some 'null' Thing objects.
I am creating a new object:
val foo = thirdPartyGetFoo()
val filteredFoo = Foo(foo.name, foo.things.filterNotNull())
That works, however AndroidStudio greys out the filterNotNull function call and presents a warning:
Useless call on collection type: The inspection reports filter-like
calls on already filtered collections.
Is this the right way to filter that list? Should I ignore the warning or is there a better way?
You do not specify what library creates the object with nulls. Some deserialization libraries can use static factory methods which you could configure, and then have the factory method strip the null. For example, if this were Jackson you would simply:
class Foo(val name: String, val things: List<Thing>) {
companion object {
#JsonCreator
#JvmName("createFromNullable")
fun create(name: String, things: List<Thing?>) = Foo(name, things.filterNotNull())
fun create(name: String, things: List<Thing>) = Foo(name, things)
}
}
Then...
val goodFoo = jacksonObjectMapper().readValue<Foo>(someJsonWithNulls)
Maybe your library has options that are similar?
If not, and you don't have 100 of these things with this problem, I would probably create a temporary class to hold the results and convert that to the final class:
open class FooNullable(val name: String, open val things: List<Thing?>) {
open fun withoutNulls(): Foo = Foo(name, things.filterNotNull())
}
class Foo(name: String, override val things: List<Thing>) : FooNullable(name, things) {
override fun withoutNulls(): Foo = this
}
Then you can deserialize into FooNullable and just call withoutNulls() to get the other flavor that is clean. And if you accidentally call it on one without nulls already, it just does nothing.
val goodFoo = Foo("", emptyList<Thing>())
val alsoGoodFoo = goodFoo.withoutNulls() // NOOP does nothing
val badFoo = thirdPartyGetFoo()
val betterFoo = badFoo.withoutNulls() // clean up the instance
val safeFoo = thirdPartyGetFoo().withoutNulls() // all at once!
Not the cleanest, but does work. The downsides is this second step, although it looks like you were already planning on doing that anyway. But this model is safer than what you proposed since you KNOW which type of object you have and therefore you continue to be typesafe and have the compiler helping you avoid a mistake.
You don't have to use inheritance as in the above example, I was just trying to unify the API in case there was a reason to have either version in hand and know which is which, and also act upon them in a similar way.