How to extend enums in Kotlin?

21,676

Solution 1

There is more to the reason why enum inheritance is not supported than "inheritance is evil". In fact, a very practical reason:

enum class BaseColor { BLUE, GREEN, RED }

val x: BaseColor = ... // must be one of the 3 enums, right?
// e.g. when {} can be done exhaustively with BLUE, GREEN, RED

enum class DerivedColor : BaseColor { YELLOW }

val y: BaseColor = ... // now, it can also be YELLOW
// here, you lose the guarantee that it's a value in a limited set
// and thus the main advantage of enums

There are multiple options to achieve what you like:

1. Different enums implement a common interface

I would refer you to this answer.

Interfaces are a very flexible solution and allow you to derive an unlimited number of enums. If this is what you need, go for it.

2. Sealed classes

In Kotlin, sealed classes are a generalization of enums, that allows you to retain state in each value. All derived classes of a sealed class must be known up-front and declared in the same file. The advantage compared to interfaces is that you can limit the sealed class to a fixed set of possible types. This allows you to omit the else branch in when, for example. The drawback is that it's not possible to add types outside the sealed class by design.

Semantically, you have an enum of enums: the first level determines which enum class type is used, and the second level determines which enumerator (constant) inside that enum class is used.

enum class DefaultError { INTERNET_ERROR, BLUETOOTH_ERROR, TEMPERATURE_ERROR }
enum class NfcError { NFC_ERROR }
enum class CameraError { CAM_ERROR }

sealed class Error {
    data class Default(val error: DefaultError) : Error()
    data class Nfc(val error: NfcError) : Error()
    data class Camera(val error: CameraError) : Error()
}

fun test() {
    // Note: you can use Error as the abstract base type
    val e: Error = Error.Default(DefaultError.BLUETOOTH_ERROR)

    val str: String = when (e) {
        is Error.Default -> e.error.toString()
        is Error.Nfc -> e.error.toString()
        is Error.Camera -> e.error.toString()
        // no else!
    }
}

Solution 2

You can extend an Enum. Kind of. But not with inheritance. Enums can implement an interface. That means to extend it you would simply add another enum implementing the same interface.

Lets say you have an error. This error has an error code. Default error are implemented as DefaultError enum and can be extended by adding aditional enums implementing the Error interface.

interface Error {
    fun code(): Int
}

enum class DefaultError(private val code: Int) : Error {
    INTERNET_ERROR(1001),
    BLUETOOTH_ERROR(1002),
    TEMPERATURE_ERROR(1003);

    override fun code(): Int {
        return this.code
    }
}

enum class NfcError(private val code: Int) : Error {
    NFC_ERROR(2001);

    override fun code(): Int {
        return this.code
    }
}

enum class KameraError(private val code: Int) : Error {
    CAM_ERROR(3001);

    override fun code(): Int {
        return this.code
    }
}

Solution 3

The simple answer is that you can't extend enums in Kotlin the way you would want to.

I have to agree with Miha_x64's comment stating that inheritance is "evil" and should only be used when it legitimately makes sense (yes, there are still situations when inheritance is the way to go). I believe that instead of actually trying to work around the design of enums in Kotlin, why don't we design our solution differently? I mean: Why do you think you need such an enum hierarchy, to begin with? What's the benefit? Why not simply have some "common errors" instead and specific errors for whichever concrete area needs very specific errors? Why even use enums?

If you are dead set on using enums, then Januson's solution might be your best bet, but do please use meaningful error codes because using "1001", "1002", "233245" is so 1980s and utterly horrible to read and work with. I find "INTERNET_ERROR" and "BLUETOOTH_ERROR", etc. just as cryptic... can we really not do any better and be more specific about what went wrong so that whoever reads the error code CAN actually understand what is wrong without needing to dig through the internet or through some hefty documentation for the next many minutes/hours? (except, of course, if there are some legitimate reasons why the code needs to be as small as possible - e.g. message size limitations, bandwidth limitations, etc.)

In case you are not dead set on using enums, then you could consider the following:

data class ErrorCode(
    val code: String,
    val localeKey: String,
    val defaultMessageTemplate: String
)
val TENANT_ACCESS_FORBIDDEN = ErrorCode(
    "TENANT_ACCESS_FORBIDDEN",
    "CommonErrorCodes.TENANT_ACCESS_FORBIDDEN",
    "Not enough permissions to access tenant ''{0}''."
)
val NO_INTERNET_CONNETION = ErrorCode(
    "NO_INTERNET_CONNETION",
    "DeviceErrorCodes.NO_INTERNET_CONNETION",
    "No internet connection."
)
val NO_BLUETOOTH_CONNECTION = ErrorCode(
    "NO_BLUETOOTH_CONNECTION",
    "DeviceErrorCodes.NO_BLUETOOTH_CONNECTION",
    "No bluetooth connection."
)
val TEMPERATURE_THRESHOLD_EXCEEDED = ErrorCode(
    "TEMPERATURE_THRESHOLD_EXCEEDED",
    "DeviceErrorCodes.TEMPERATURE_THRESHOLD_EXCEEDED",
    "Temperature ''{0}'' exceeds the maximum threshold value of ''{1}''."
)

Because all the above codes, in essence, act as static constants, comparing against them is as easy as comparing enums (e.g.: if (yourException.errorCode == NO_INTERNET_CONNECTION) { // do something }).

Inheritance is really not needed, what you really need is a clear separation between common and non-common error codes.

Share:
21,676
Noam Silverstein
Author by

Noam Silverstein

Updated on July 09, 2022

Comments

  • Noam Silverstein
    Noam Silverstein almost 2 years

    In my Kotlin project, I have a DefaultError enum

    enum class DefaultError {
        INTERNET_ERROR,
        BLUETOOTH_ERROR,
        TEMPERATURE_ERROR
    }
    

    I would like to extend them so that I have

    enum class NfcAndDefaultError : DefaultError {
        //DefaultError inherited plus
        NFC_ERROR
    }
    

    and another enum

    enum class KameraAndDefaultError : DefaultError {
        //DefaultError inherited plus
        CAM_ERROR
    }
    

    Now, I have

    enum class NfcDefaultError {
        INTERNET_ERROR,
        BLUETOOTH_ERROR,
        TEMPERATURE_ERROR,
        NFC_ERROR
    }
    

    and

    enum class KameraAndDefaultError {
        INTERNET_ERROR,
        BLUETOOTH_ERROR,
        TEMPERATURE_ERROR,,
        CAM_ERROR
    }
    

    I bet Kotlin has a nice way there?

  • Noam Silverstein
    Noam Silverstein almost 5 years
    does NfcError has INTERNET_ERROR, BLUETOOTH_ERROR & TEMP_ERROR as well?
  • Januson
    Januson almost 5 years
    No. To consumer it will look like values of a single enum but the provider has to access values through their specific enums.
  • CoolMind
    CoolMind almost 5 years
    Thanks! Sealed classes cannot be extended in other files, so their benefit is low.