DEV Community

le0nidas
le0nidas

Posted on

Use sealed classes for better domain representation

Lets start with the business logic.

We have a task.
A task can be assigned to a user OR a group OR to no one.

First: the ugly way

One way to implement this is by putting everything in the task:

class UglyTask(
        val name: String,
        val assignedUser: User? = null,
        val assignedGroup: Group? = null
) {

    init {
        if (assignedUser != null && assignedGroup != null) {
            throw IllegalArgumentException("a task can be assigned to either a user OR a group.")
        }
    }
}

This implementation is not just ugly (all those nulls and the check on initialization) it also hides the business logic.
The developer must read the class's code to understand how to create an assigned task.

Moreover, the simple thing of figuring out if and where a task is assigned becomes tedious and can easily lead to bugs. For example writing a function to print where a task is assigned has to do null checks and does not leverage all of kotlin's powers:

fun UglyTask.printAssignment() {
    when {
        assignedGroup == null && assignedUser == null -> println("\"$name\" is assigned to no one")
        assignedGroup != null -> println("\"$name\" is assigned to to group: ${assignedGroup.name}")
        assignedUser != null -> println("\"$name\" is assigned to to user: ${assignedUser.name}")
    }
}

Finally if you want your code to be readable, in all cases, you must try a little bit harder:

    val le0nidas = User("le0nidas")
    val kotlinEnthusiasts = Group("kotlin enthusiasts")

    UglyTask("buy milk").printAssignment()
    UglyTask("write post", assignedUser = le0nidas).printAssignment()
    UglyTask("write kotlin", assignedGroup = kotlinEnthusiasts).printAssignment()

Here we pass the parameters by name to omit the null usage when creating a task for a group:

UglyTask("write kotlin", null, kotlinEnthusiasts).printAssignment()

Second: The less ugly way

Another way is be having multiple constructors, each for every valid assignment:

class LessUglyTask private constructor(
        val name: String,
        val assignedUser: User?,
        val assignedGroup: Group?
) {

    constructor(name: String) : this(name, null, null) // assigned to no one
    constructor(name: String, assignedUser: User) : this(name, assignedUser, null) // assigned to a user
    constructor(name: String, assignedGroup: Group) : this(name, null, assignedGroup) // assigned to a group
}

This implementation is a little bit better since it helps the developer to understand faster the logic behind task assignment

less-ugly-task-creation.png

but it too has a couple drawbacks starting with the fact that when writing a function like the one above.. you actually end up with all the necessary null checks:

fun LessUglyTask.printAssignment() {
    when {
        assignedGroup == null && assignedUser == null -> println("\"$name\" is assigned to no one")
        assignedGroup != null -> println("\"$name\" is assigned to to group: ${assignedGroup.name}")
        assignedUser != null -> println("\"$name\" is assigned to to user: ${assignedUser.name}")
    }
}

Furthermore it is not easily scalable. If we want to add another way of assignment we need to change the primary constructor AND add a new second constructor for it (the scalability issue is one that the first implementation has too but i will not go there because.. the null checks get multiplied!).

As far as readability goes it still needs a little Kotlin love but it will not lead to anything like above:

    val le0nidas = User("le0nidas")
    val kotlinEnthusiasts = Group("kotlin enthusiasts")

    LessUglyTask("buy milk").printAssignment()
    LessUglyTask("write post", assignedUser = le0nidas).printAssignment()
    LessUglyTask("write kotlin", assignedGroup = kotlinEnthusiasts).printAssignment()

Finally: the sealed classes way :)

The best way to implement the business logic is by using kotlin's sealed classes. This way we can represent our business logic straight into our code and also keep our code clean, readable and scalable:

sealed class AssignedTo
object AssignedToNoOne : AssignedTo()
data class AssignedToUser(val user: User) : AssignedTo()
data class AssignedToGroup(val group: Group) : AssignedTo()

class Task(val name: String, val assignedTo: AssignedTo = AssignedToNoOne)

As for the print function see for your self how easily can be read and how it leverages all kotlin's powers (smart cast for the win):

fun Task.printAssignment() {
    when (assignedTo) {
        is AssignedToNoOne -> println("\"$name\" is assigned to no one")
        is AssignedToGroup -> println("\"$name\" is assigned to ${assignedTo.group.name}")
        is AssignedToUser -> println("\"$name\" is assigned to ${assignedTo.user.name}")
    }
}

Not to mention how good our code looks when creating tasks:

    val le0nidas = User("le0nidas")
    val kotlinEnthusiasts = Group("kotlin enthusiasts")

    Task("buy milk", AssignedToNoOne).printAssignment()
    Task("write post", AssignedToUser(le0nidas)).printAssignment()
    Task("write kotlin", AssignedToGroup(kotlinEnthusiasts)).printAssignment()

And if we want to add a new way of assignment we just extend the AssignTo class!

Latest comments (0)