1

I'm curious about an example given in Kotlin documentation regarding sealed classes:

fun log(e: Error) = when(e) {
    is FileReadError -> { println("Error while reading file ${e.file}") }
    is DatabaseError -> { println("Error while reading from database ${e.source}") }
    is RuntimeError ->  { println("Runtime error") }
    // the `else` clause is not required because all the cases are covered
}

Let's imagine the classes are defined as follows:

sealed class Error

class FileReadError(val file: String): Error()
class DatabaseError(val source: String): Error()
class RuntimeError : Error()

Is there any benefit for using when over using polymorphism:

sealed class Error {
    abstract fun log()
}

class FileReadError(val file: String): Error() {
    override fun log() { println("Error while reading file $file") }
}
class DatabaseError(val source: String): Error() {
    override fun log() { println("Error while reading from database $source") }
}
class RuntimeError : Error() {
    override fun log() { println("Runtime error") }
}

The only reason I can think of is that we may not have access to the source code of those classes, in order to add our log method to them. Otherwise, it seems that polymorphism is a better choice over instance checking (see [1] or [2] for instance.)

Sadeq Dousti
  • 3,346
  • 6
  • 35
  • 53
  • Yes, in this case I think polymorphism is preferable. But that's only because in this example, all you're doing is logging a String regardless of which subclass it is. If, for example, you want to be able to inspect the `file` property in the case of a FileReadError, you can't do that with polymorphism. – Tenfour04 Dec 19 '22 at 21:28
  • @Tenfour04: Thanks a lot for the answer. Just for me to understand more clearly the last part of your comment: Let's have a `when` statement which depending on the class, does some action. If the class is `FileReadError`, it inspects the `file` property. I think this can also be implemented via polymorphism. Or maybe I didn't get what you meant. – Sadeq Dousti Dec 19 '22 at 21:38
  • Using `when` still involves polymorphism, because the various `Error`s get smart-cast to the specific type they match, and then you can do things like access `file` or `source` as appropriate. Sealed classes give you two advantages really - they define *all* the possible subtypes (so you can do exhaustive pattern matching, e.g. in a `when`) similar to an enum's set of constants; and they allow you to define completely disparate subtypes, like how two of yours have a `String` parameter (with different names) and one has no extra data at all (and could be a singleton `object` instead) – cactustictacs Dec 19 '22 at 22:21
  • Also this is sort of an object-oriented vs functional argument - should those `log` messages be encapsulated in the objects themselves, as part of the data? Or is it something that should be defined at the point of use, in the `when` block? This is a simple example here, but you can imagine reusing those types in various places, and wanting to do different logging with them (or at least displaying different messages depending on the context). By keeping the types and their data simple, and extracting the situational stuff, you can keep things cleaner. Depends on the application really! – cactustictacs Dec 19 '22 at 22:26
  • @cactustictacs Thanks. Comparing my second example to the first, both use `sealed` classes. So, the advantage of using `sealed` classes over non-`sealed` ones is not being questioned. If I do the `when` approach, an addition type checking is performed, and also there are other disadvantages as explained in the links provided. – Sadeq Dousti Dec 19 '22 at 22:26
  • @cactustictacs Your second comment makes a lot of sense :) – Sadeq Dousti Dec 19 '22 at 22:28
  • Your second example could be done with an interface though, the difference really is the exhaustive nature (the compiler knows what *all* the implementing classes are since you define them in the sealed class). The `when` pattern can be really concise and useful, especially for things like this (where you define all the possible errors you need to handle) and sealed classes just dovetail with that really nicely! But yeah, you can do it whichever way you like. They're a lot like enums, and in the same way you might want to add data to the enum, or keep it as a simple type and build upon that – cactustictacs Dec 19 '22 at 22:34
  • Responding to your request for clarification. Your DatabaseError and RuntimeError classes don’t have a file property to inspect because it doesn’t make sense for them to. I don’t see how this could be done with polymorphism. – Tenfour04 Dec 20 '22 at 02:01

2 Answers2

4

This is described as "Data/Object Anti-Symmetry" in the book Clean Code: A Handbook of Agile Software Craftsmanship by Robert C. Martin.

In the first example (Data style), you are keeping your error classes dumb with an external function that handles all types. This style is in opposition to using polymorphism (Object style) but there are some advantages.

Suppose you were to add a new external function, one that returns an icon to show the user when the error happens. The first advantage is you can easily add this icon function without changing any line in any of your error classes and add it in a single place. The second advantage is in the separation. Maybe your error classes exist in the domain module of your project and you'd prefer your icon function to be in the ui module of your project to separate concerns.

So when keeping the sealed classes dumb, it's easy to add new functions and easy to separate them, but it's hard to add new classes of errors because then you need to find and update every function. On the other hand when using polymorphism, it's hard to add new functions and you can't separate them from the class, but it's easy to add new classes.

Trevor
  • 1,349
  • 10
  • 16
2

The benefit of the first (type-checking) example is that the log messages do not have to be hardcoded into the Error subclasses. In this way, clients could potentially log different messages for the same subclass of Error in different parts of an application.

The second (polymorphic) approach assumes everyone wants the same message for each error and that the developer of each subclass knows what that error message should be for all future use cases.

There is an element of flexibility in the first example that does not exist in the second. The previous answer from @Trevor examines the theoretical underpinning of this flexibility.

jaco0646
  • 15,303
  • 7
  • 59
  • 83