Home > front end >  Kotlin best-practice for domain model class inheritance
Kotlin best-practice for domain model class inheritance

Time:10-25

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?

CodePudding user response:

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