Search code examples
kotlindata-class

How to mimic or achieve IS-A relationships for Kotlin data classes


I have been exploring Kotlin and have written a small program/script that does a task I find boring.

In the development of the program I use data classes to represent a Playlist. at one point in the design I wanted to have a special type of Playlist that was EmptyPlaylist.

I couldn't get this to work.

How would you achieve this relationship with Kotlin?

In Java, I would just extend Playlist (or perhaps create an interface/abstract class for them both to inherit from).

I just wanted to be able to have a List<Playlist> rather than List<Playlist?>

In the end I just created a Playlist object, but I am interested in if it is possible to create IS-A hierarchies with data classes.


Solution

  • UPD: Kotlin Beta added restrictions on data classes, so the answer has also been updated.

    • Data classes cannot be abstract, open, sealed or inner;
    • Data classes may not extend other classes (but may implement interfaces).

    So the only option for building their hierarchies is interfaces. This restriction may be released in future versions.


    The idea of inheriting EmptyPlaylist from Playlist which is presumed non-empty according to your words looks a little bit contradictory, but there are options:

    • You can make both TracksPlaylist and EmptyPlaylist implement some Playlist interface. This is reasonable because EmptyPlaylist may not contain everything that a Playlist does. This would look like:

      public interface Playlist
      
      data class TracksPlaylist(val name: String, val tracks: List<Track>) : Playlist
      data class EmptyPlaylist(val name: String): Playlist
      
    • If you don't want multiple instances of EmptyPlaylist to even exist, you can make it a val instead of distinct class and avoid is-checks, just comparing to

      val EMPTY_PLAYLIST = TracksPlaylist("EMPTY", listOf())
      

      This is applicable when empty playlists are all equal so that a single object is sufficient to represent them all.

    • You can use sealed classes. This doesn't allow you to use data classes, but sealed classes may fit for building class hierarchies like this.

      sealed class is an abstract class which can only have its subclasses be declared inside its body. This gives the compiler guarantees that these are the only subclasses. Example:

      sealed class Playlist(val name: String) {
          class Tracks(name: String, val tracks: List<Track>): Playlist(name)
          class Playlists(name: String, val innerPlaylists: List<Playlist>): Playlist(name)
          object Empty: Playlist("EMPTY")
      }
      

      After that, you will be able to use when statement without specifying else branch if you state all the subclasses and objects of the sealed class:

      when (p) {
          is Playlist.Tracks -> { /* ... */ }
          is Playlist.Playlists -> { /* ... */ }
          Playlist.Empty -> { /* ... */ }
      }
      

      However, this option requires you to deal with equals, hashCode, toString, copy and componentN on your own, if you need them.