Effective Kotlin Item 40: Prefer class hierarchies to tagged classes
It is not uncommon in large projects to find classes with a constant "mode" that specifies how the class should behave. We call such classes tagged, as they contain a tag that specifies their mode of operation. There are many problems with them, and most of them originate from the fact that different responsibilities from disparate modes are fighting for space in the same class even though they are generally distinguishable from each other. For instance, in the following snippet, we can see a class that is used in tests to check if a value fulfills some criteria. This example is simplified, but it is a real sample from a large project1.
There are many downsides to this approach:
- Additional boilerplate from handling multiple modes in a single class.
- Inconsistently used properties, as they are used for different purposes. The object generally has more properties than most modes need, as they might be required by other ones. For instance, in the example above,
valueis not used when the mode is
- It is hard to protect state consistency and correctness when elements have multiple purposes and can be set in a few ways.
- It is often required to use a factory method, because otherwise, it is hard to ensure that objects are created correctly.
Instead of tagged classes, we have a better alternative in Kotlin: sealed classes. Instead of accumulating multiple modes in a single class, we should define multiple classes for each mode, and use the type system to allow their polymorphic use. Then the additional
sealed modifier seals those classes as a set of alternatives. Here is how it could have been implemented:
Such implementation is much cleaner as there are no multiple responsibilities tangled with each other. Every object has only the required data and can define what parameters it is willing to have. Using a type hierarchy allows to overcome all shortcomings of tagged classes. We can also easily add methods as extension functions using
Tagged classes are not the same as state pattern
Tagged classes should not be confused with the state pattern, a behavioral software design pattern that allows an object to alter its behavior when its internal state changes. This pattern is often used in front-end controllers, presenters or view models (respectively from MVC, MVP, and MVVM architectures). For instance, let’s say that you write an application for morning exercises. Before every exercise, there is preparation time, and in the end, there is a screen that states that you are done with your training.
When we use the state pattern, we have a hierarchy of classes representing different states, and a read-write property we need to represent which state is the current one:
The difference here is that:
- The state is a part of a bigger class with more responsibilities.
- The state changes.
The state is generally kept in a single read-write property
state. The concrete state is represented by an object, and we prefer this object to be a sealed class hierarchy instead of a tagged class. We also prefer that as an immutable object and whenever we need to change it, we change
state property. It is not uncommon for
state later be observed to update the view every time it changes:
In Kotlin, we use type hierarchies instead of tagged classes. We most often represent those type hierarchies as sealed classes. It does not collide with the state pattern, which is a popular and useful pattern in Kotlin. They actually cooperate as when we implement state, we prefer to use sealed hierarchies instead of tagged classes. This is especially visible when we implement complex yet separable states on a single view.
In the full version, it contained many more modes.