Represent object state using sealed class
In my time tracking application, I have a TimeInterval
-class, which represents
an interval in time using two properties, start
and stop
.
The application allows the user to start a new time tracking session and then stop the session. Which means that this class needs to support both states.
As the application was originally written in Java, the state is inferred from
the value of the stop
property, i.e. if the value is zero the time interval is
active 1.
data class TimeInterval(val start: Long, val stop: Long)
val active = TimeInterval(start = 1, stop = 0)
val inactive = TimeInterval(start = 1, stop = 2)
Using this approach to represent the different states works, but it requires that each developer working with the code knows about it 2. And, the code do not in any way communicate this to the developer, other than the usage pattern.
After migrating the entire application to Kotlin, and getting inspired by
different articles, books and podcasts on functional programming, I decided to
improve this by using a sealed class
with each state represented by a
different class.
sealed class TimeInterval {
data class Active(val start: Long): TimeInterval()
data class Inactive(val start: Long, val stop: Long): TimeInterval()
}
val active = TimeInterval.Active(start = 1)
val inactive = TimeInterval.Inactive(start = 1, stop = 2)
The active state now clearly indicates that the value for the stop
property
should not be used. The object state, as persisted in the database, is now only
an implementation detail that never goes beyond the repository boundary.
Another benefit of representing each state as a separate type is that we can encode more domain knowledge into the type system and have the compiler help enforce this logic.
As a simple example, the active state needs to be able to transition from an
active state to an inactive state, i.e. a clockOut
-method.
check(timeInterval !is TimeInterval.Active)
timeInterval.clockOut(Date())
Or, if we need to perform some calculations based on the value of start
and
stop
, we can cache the value using a by lazy
implementation for the inactive
state since the value will not change. The value for the active state will still
be calculated for each separate call.
The real
TimeInterval
-class is a bit more complicated, but it has been simplified here for the sake of this example. ↩︎This is often referred to as Tribal knowledge. ↩︎