Loading...

Favor Polymorphism Over Enumerations

Clean Code
1. November 2020
3 Minuten Lesezeit
Beitrag teilen:
Gefällt dir der Beitrag?
Du wirst den Newsletter lieben!

Last week I reviewed the data design of a colleague. He had to improve a feature and while working on it he noticed that the existing design was not sufficient to properly reflect the changes he had to do. Intuitive he followed the practices of other models in the code and introduced an enumeration to distinguish between different alert types that can be created in the application. Using an enumeration for it is not the optimal solution and in this post I want to describe why you should always question when you see an enumeration in your code and think about solving it with polymorphism instead.

So let’s start with an example to understand the issue.

class Alert(
        val id: Int,
        val userId: Int,
        val threshold: Double,
        val alertType: AlertType
)
enum class AlertType {
    POWER_CONSUMPTION,
    TEMPERATURE
}

As you can see, we defined a class called Alert which can be created by a user. To distinguish in the application between the different alert types we introduced the AlertType enumeration.

When we would trigger such an alert from a service we would write:

class AlertService {
    fun triggerAlert(user: User, alert: Alert) {
        when (alert.alertType) {
            AlertType.POWER_CONSUMPTION -> handlePowerConsumptionAlert()
            AlertType.TEMPERATURE -> handleTemperatureAlert()
        }
    }
}

As you can see we need to switch over the different enumerations to handle the different alert types. As soon as we introduce a new AlertType we need to search for all places in the code where we distinguish between the different types. When we forget a place we can easily introduce bugs. Additionally, a reviewer can’t validate if we handled every place in our codebase without opening the project himself and performing the same search.
Another caveat is when we persist Alert in a data store we share the same table and when we want to select all alerts of a specific type we need to filter the table.

Now imagine we need to have additional data to react on an alert. For example, we need to know the battery type to react differently on a TEMPERATURE alert. In our example we would change the Alert class to:

class Alert(
        val id: Int,
        val userId: Int,
        val threshold: Double,
        val alertType: AlertType,
        val batteryType: String
)

So the new batteryType field does only make sense when we have a TEMPERATURE alert type. But this relation is not reflected in the data itself and can only be detected by looking in the code and how it works.

The better solution is using polymorphism for it. Actually this is the intended use case for polymorphism in the first place. We should introduce an abstract Alert class and inherit from it.

abstract class Alert(
        val id: Int,
        val userId: Int,
        val threshold: Double
) {
    abstract fun handle()
}

class TemperatureAlert(id: Int, userId: Int, threshold: Double, val batteryType: String) : Alert(id, userId, threshold) {
    override fun handle() {
        // notice that we can handle the alert differently now - depending on the given batteryType
        TODO("Handle temperature alert")
    }
}

class PowerConsumptionAlert(id: Int, userId: Int, threshold: Double) : Alert(id, userId, threshold) {
    override fun handle() {
        TODO("Handle power consumption alert")
    }
}

The calling code in the AlertService now just calls handle() and does not need to know about the interior anymore.

class AlertService {
    fun triggerAlert(user: User, alert: Alert) {
        alert.handle()
    }
}

Additionally, in an ORM you can easily decide to create two tables to reflect the different alert types. These makes filtering for the different types unnecessary.

Kennst du schon Marcus' Backend Newsletter?

Neue Ideen. Jede Woche!
Top