Sunday 15 May 2022

Kotlin Enum Class versus Sealed Class

 Enum Class: 

  • It helps to represent a constant set of possible options and values i.e. group of constants, 
  • This can be used when a variable can only accept value out of a small set of known values 
  • It will increases the compile-time checking and avoid errors from passing in invalid constants. 
  • Easy to document which values are valid to use and what it implies.
For example, enumeration defines as like as below 
Flags accounting ledger (DEBIT, CREDIT),
Factory outlet process, (CRUSHING, WINDING, DRYING)
A concrete set of payment mode (UPI, NEFT, IMPS, AEPS) 
Handling API request status (LOADING, SUCCESS, RETRY, FAILURE), etc.

It holds the value which are always specific to the item, this value is mutable and static to the each items, this functionality is often used to attach some constant values on each item through the constructor.

Kotlin enums have methods and their implementations are also specific to the items, when we define them, enum class must have the abstract method, and each item mandatory to override it.
enum class Flavours(val preparationTime: Long) {
    VANILLA(3000L) {
	  // Even I don’t have the discount, I need to be overrided
        override fun discount(amt: Double): String {
            return ""
        }
    },
    CHOCOLATE(5000L) {
        override fun discount(amt: Double): String {
            return "Discount is $amt"
        }
    },
    COOKIES_CREAM(1000L) {
        override fun discount(amt: Double): String {
            return "Discount is $amt"
        }
    };

    var brand: String = "Amul"
    abstract fun discount(amt: Double): String
}

val flav1 = Flavours.COOKIES_CREAM
flav1.brand = "Arun"
val flav2 = Flavours.COOKIES_CREAM

// changing the brand of flav2 but it affects flav1 too, because It is item specific
flav2.brand = "Ponlait"   

val flav3 = Flavours.CHOCOLATE
flav3.brand = "Vadilal"
    
println("${flav1.name} | ${flav1.brand} | ${flav1.discount(7.5)}")
println("${flav2.name} | ${flav2.brand} | ${flav2.discount(2.5)}")
println("${flav3.name} | ${flav3.brand} | ${flav3.discount(3.5)}")

Output:
COOKIES_CREAM | Ponlait | Discount is 7.5
COOKIES_CREAM | Ponlait | Discount is 2.5
CHOCOLATE | Vadilal | Discount is 3.5

Here in the above code snippet we are able to see the brand will be the value which is specific to item (refer the comment and output value highlighted) and discount function is mandatory to override on all the enum values.  

Therefore iterating over enum values is easy, and their serialization/deserialization is simple and efficient (as they are generally represented just by name) and automatically supported by most libraries for serialization (like Gson, Jackson, Kotlin Serialization, etc.). They also have ordinal, and automatically implemented toString, hashCode and equals. 

Sealed Class: 
  • It helps to represent constrained hierarchies in which an Object can only be of one of the given types.
  • This class can have a specific number of subclasses i.e. restricted subclass hierarchy and each can be handled through multiple instances, 
  • It is an abstract classes (no possibility to create an instance)
In case, if we are sharing our code as a compiled jar/aar file to our client with the sealed class in it.  Our client can’t inherit or subclass our sealed classes.

Code Snippet for Reference:

sealed class Flavours(val preparationTime: Long)

class Vanilla(preparationTime: Long) : Flavours(preparationTime) {
    fun getCreamType() {
        println("Inside Vanilla")
    }
}

class Chocolate(preparationTime: Long, val isDiscount: Double) : Flavours(preparationTime) {
    fun knowChocolateType() {
        println("Inside Chocolate")
    }
}

class CookieCream(preparationTime: Long, val isDiscount: Double, val isSmokey: Boolean) : Flavours(preparationTime) {
    fun getCookieType() {
        println("Inside Cookie")
    }
}

fun getMyFlavor(flavorType: Flavours) = when (flavorType) {
    is Chocolate -> {
        flavorType.knowChocolateType()
    }

    is CookieCream -> {
        flavorType.getCookieType()
    }

    is Vanilla -> {
        flavorType.getCreamType()
    }
} 

//While calling this function
getMyFlavor(Chocolate(3000L, 10.50))
getMyFlavor(CookieCream(3000L, 10.50, true))
getMyFlavor(Vanilla(3000L))

Output:
Inside Chocolate
Inside Cookie
Inside Vanilla

Other Uses Case – Code Snippet:

// Handling Ledger balance & API Response along with generics (Covariance)
sealed class LedgerExpenses<out D, out C>
class Expense<D>: LedgerExpenses <D, Nothing>()
class Incomes<C>: LedgerExpenses <Nothing, C>()

// Handling multiple Eye lense
sealed class EyeLense
object IndianLense(val manuf:String, val degree:Double): EyeLense ()
object CollaborationLense: EyeLense (val degree:Double): EyeLense ()
class CustomizedLense(val src: String, val owner:Owner): EyeLense ()

// Handling API Response along with generics (Covariance)
sealed class Response<out R>
class Success<R>(val value: R): Response<R>()
class Failure(val error: Throwable): Response<Nothing>()

Conclusion:




Similarities:
  • The set of values for an enum type is also restricted like the sealed class restricting the subclasses.
  • Enum and sealed class increase the compile-time checking by restricting the constants or types to be matched at compile-time instead of runtime check.

Differences:
  • Each enum constant exists only as a single instance, whereas a subclass of a sealed class can have multiple instances, each with its own state. The state of an object is stored in fields (Variables).

Final Touch…
  • Enum handles with concrete set of values, whereas sealed classes uses concrete set of classes. 
  • Enum have the methods values() and valueOf, so we can serialize and de-serialize the values. 
  • Enums have ordinal and we can hold constant data. 
  • Sealed classes can hold instance-specific hierarchy. 
  • Sealed class helps to define on our own custom set of objects and we can use them with the multiple instance.
  • Sealed class is packed and secured way of creating a hierarchy of defined instance and those definitions are restricted to inherit.  (It violates the Open Close Principle, but this required for such an use cases)