Null object pattern and sealed classes

In a recent code review one of my colleagues was concerned that part of the code I wrote might cause us problems because of the null object pattern I chose to use.

The code was something like this:

class Task(
val description: String,
val assignedTo: AssignedTo
)
sealed class AssignedTo {
object Nobody : AssignedTo()
class User(val name: String) : AssignedTo()
}
fun main() {
val buyMilk = Task("Buy milk", AssignedTo.Nobody)
val writePost = Task("Write post", AssignedTo.User("le0nidas"))
print(buyMilk, writePost)
}
private fun print(vararg tasks: Task) {
tasks.forEach { task ->
val user = when(task.assignedTo) {
AssignedTo.Nobody -> "nobody"
is AssignedTo.User -> task.assignedTo.name
}
println("Task '${task.description}' is assigned to $user")
}
}

and my colleague was referring to the Nobody usage.

That got me thinking. Can this usage of sealed classes be considered as an implementation of the null object pattern? Also, why is the usage of this design pattern a bad thing?

Null object pattern (wikipedia)

So, what is this pattern? This pattern is one way to solve the

we don’t want a method to return a null value and force ourselves to check it before usage

my definition 😛

Ok, it might be better to have an example:

Lets say that we have a service that returns the workout we did in a particular date. Each workout has the duration of the exercise and if we did not workout on the given date the service returns null (we’ll pretend for a moment that Kotlin does not have null safety or those great extensions like map, sum etc):

abstract class Workout(val duration: Duration)
class Walking(duration: Duration) : Workout(duration)
class Swimming(duration: Duration) : Workout(duration)
class Running(duration: Duration) : Workout(duration)
fun workoutService(date: LocalDate): Workout? {
val workouts = mapOf(
LocalDate.of(2020, 4, 25) to Walking(Duration.ofHours(2)),
LocalDate.of(2020, 4, 23) to Swimming(Duration.ofHours(1)),
LocalDate.of(2020, 4, 22) to Running(Duration.ofMinutes(30))
)
return workouts[date]
}
fun main() {
val days = listOf(
LocalDate.of(2020, 4, 22), LocalDate.of(2020, 4, 23), LocalDate.of(2020, 4, 24), LocalDate.of(2020, 4, 25)
)
var sum: Long = 0
for(day in days) {
val workout = workoutService(day)
if (workout == null) {
continue
}
sum = sum + workout.duration.toMillis()
}
println("Total duration: ${Duration.ofMillis(sum)}") // Total duration: PT3H30M
}

By introducing a null object, for those days that we did not workout, we can remove the null checks and have a more readable code:

abstract class Workout(val duration: Duration)
class Walking(duration: Duration) : Workout(duration)
class Swimming(duration: Duration) : Workout(duration)
class Running(duration: Duration) : Workout(duration)
// null object:
object NoWorkout : Workout(Duration.ZERO)
fun workoutService(date: LocalDate): Workout? {
val workouts = mapOf(
LocalDate.of(2020, 4, 25) to Walking(Duration.ofHours(2)),
LocalDate.of(2020, 4, 23) to Swimming(Duration.ofHours(1)),
LocalDate.of(2020, 4, 22) to Running(Duration.ofMinutes(30))
)
return workouts[date] ?: NoWorkout // usage of null object
}
fun main() {
val days = listOf(
LocalDate.of(2020, 4, 22), LocalDate.of(2020, 4, 23), LocalDate.of(2020, 4, 24), LocalDate.of(2020, 4, 25)
)
var sum: Long = 0
for(day in days) {
val workout = workoutService(day)
sum = sum + workout!!.duration.toMillis() // yes, we are that!! sure
}
println("Total duration: ${Duration.ofMillis(sum)}") // Total duration: PT3H30M
}

Why is this a bad design?

From the example we could easily conclude that this pattern is quite helpful. Right?

Well, as everything in our industry: it depends! When we need a way to have some default values so that our calculations won’t crash and burn then it is a good choice. But if we use it in ways that we hide things we might end up in false successes.

For example, lets assume that we have a storage interface and a factory function that returns the proper storage implementation depending on a given value:

interface Storage {
fun save(workout: Workout)
}
class LocalStorage : Storage {
override fun save(workout: Workout) {
// it saves in our database
}
}
class CloudStorage : Storage {
override fun save(workout: Workout) {
// it saves in someone else's database
}
}
object NullStorage : Storage {
override fun save(workout: Workout) {
// it does nothing
}
}
fun getStorage(type: String): Storage {
return when (type) {
"local" -> LocalStorage()
"cloud " -> CloudStorage()
else -> NullStorage
}
}

If something is not configured well we might end up using, through out our entire project, the NullStoragethinking that we have saved everything when in reality we lost our data!

In case you missed it there is a space in the cloud-type so no matter how many times we ask for the “cloud”-storage we will get the null-storage back. Silly example with a silly mistake but imagine what could go wrong in real projects!

So, back to the code that started all this

First thing is first:

Q: Is this usage of sealed classes an implementation of the null object pattern?

A: No. The Nobody object is just a representation of a valid state for the AssignedTo concept and does not provide any default values or hides any method usage.

Q: Can sealed classes be used for implementing the null object pattern?

A: Yes. Sealed classes can help us in having many values for one concept but the fact that we can use full fledged classes, that can also inherit a bunch of things, is quite powerful and can be easily misused. I don’t see anything stopping us from implementing the storage-example using sealed classes so we just need to think things carefully.

And now the twist:

As is, the code does not implement the design pattern but with a small change we can make it not only to implement it but to do it badly too! We’ll just move the name property to the parent and have Nobody provide an empty value:

class Task(
val description: String,
val assignedTo: AssignedTo
)
sealed class AssignedTo(val name: String) {
object Nobody : AssignedTo("")
class User(name: String) : AssignedTo(name)
}
fun main() {
val buyMilk = Task("Buy milk", AssignedTo.Nobody)
val writePost = Task("Write post", AssignedTo.User("le0nidas"))
print(buyMilk, writePost)
// Task 'Buy milk' is assigned to
// Task 'Write post' is assigned to le0nidas
}
private fun print(vararg tasks: Task) {
tasks.forEach { task ->
println("Task '${task.description}' is assigned to ${task.assignedTo.name}")
}
}

Q: Why is this bad? It does not hide anything, it provides a default value and to be honest we just need to pass “nobody”, instead of an empty string, to the super constructor.

A: Well, this time it depends on where we use the pattern and not how. If this code is part of our core layer then we are allowing this layer to decide on presentation issues! It should be the presentation layer that will check what value the assignedTo has and print “nobody”!

Our previous implementation of AssignedTo was forcing us to make the distinction between all of its values so we did not have much of a choice but to let the print function decide.

In conclusion

Think twice before using the null object pattern and, if you are in a team, try to put code reviews as part of your work flow. It is always good to have a fresh pair of eyes look at your code and even better to have a few people to discuss (and argue) about your choices.

Trying to explain something will make you understand it better!

A smooth refactor using sealed classes and a factory function

The problem

Lets say we have a contacts app and one of the screens shows the contact’s phone number.

// domain:
class PhoneNumber(val value: String)
class Contact(val phoneNumber: PhoneNumber)
// screen:
class PhoneNumberScreen(
private val phoneNumber: PhoneNumber
) {
fun render() {
println("Phone number: ${phoneNumber.value}")
}
}
// presentation layer:
fun main() {
val contact = Contact(PhoneNumber("12345"))
show(contact.phoneNumber) // "Phone number: 12345"
val contactWithInvalidPhoneNumber = Contact(PhoneNumber(""))
show(contactWithInvalidPhoneNumber.phoneNumber) // "Phone number: "
}
private fun show(phoneNumber: PhoneNumber) {
val phoneNumberScreen = PhoneNumberScreen(phoneNumber)
phoneNumberScreen.render()
}

The problem with that code is that we can easily end up with instances that contain invalid state:

A phone number screen with an empty phone number!

Approach #1:

One way to prevent it is to add some logic in the presentation layer:

Open the phone number screen only if the phone number is not empty

fun main() {
val contact = Contact(PhoneNumber("12345"))
show(contact.phoneNumber) // "Phone number: 12345"
val contactWithInvalidPhoneNumber = Contact(PhoneNumber(""))
show(contactWithInvalidPhoneNumber.phoneNumber) // does not show anything
}
private fun show(phoneNumber: PhoneNumber) {
if (phoneNumber.value.isEmpty()) {
return
}
val phoneNumberScreen = PhoneNumberScreen(phoneNumber)
phoneNumberScreen.render()
}

Unfortunately this approach does not provide an actual solution but a patch. Our main goal is to have a PhoneNumberScreen that handles ONLY valid phone numbers.

Approach #2:

What we need is to move the necessary checks inside the PhoneNumberScreen class.

We could check the number’s validity on render and show some kind of message when there is no phone number.

class PhoneNumberScreen(
private val phoneNumber: PhoneNumber
) {
fun render() {
if (phoneNumber.value.isNotEmpty()) {
println("Phone number: ${phoneNumber.value}")
} else {
println("Invalid phone number")
}
}
}
fun main() {
val contact = Contact(PhoneNumber("12345"))
show(contact.phoneNumber) // "Phone number: 12345"
val contactWithInvalidPhoneNumber = Contact(PhoneNumber(""))
show(contactWithInvalidPhoneNumber.phoneNumber) // "Invalid phone number"
}
private fun show(phoneNumber: PhoneNumber) {
val phoneNumberScreen = PhoneNumberScreen(phoneNumber)
phoneNumberScreen.render()
}

It is quite clear that this solution provides a bad UX. Why open a screen when the user cannot use it? Also, in any additional usage of phoneNumber inside the PhoneNumberScreen we need to make the same check as in render() and handle both of its states.

Approach #3:

What we really need is to make sure that if the screen is created, then it is certain that it has a valid phone number. There are two ways to achieve that. The first one is by checking upon creation that the phone number is valid and throw an exception if it is not.

class PhoneNumberScreen(
private val phoneNumber: PhoneNumber
) {
init {
require(phoneNumber.value.isNotEmpty()) { "cannot handle invalid phone number" }
}
fun render() {
println("Phone number: ${phoneNumber.value}")
}
}
fun main() {
val contact = Contact(PhoneNumber("12345"))
show(contact.phoneNumber) // "Phone number: 12345"
val contactWithInvalidPhoneNumber = Contact(PhoneNumber(""))
show(contactWithInvalidPhoneNumber.phoneNumber) // prints nothing
}
private fun show(phoneNumber: PhoneNumber) {
try {
val phoneNumberScreen = PhoneNumberScreen(phoneNumber)
phoneNumberScreen.render()
} catch (ex: IllegalArgumentException) {
}
}

It works but we need to document it and add a try-catch wherever we create a screen instance.

The second one is by having a helper function that creates the screen only if the phone number is valid.

class PhoneNumberScreen private constructor(
private val phoneNumber: PhoneNumber
) {
fun render() {
println("Phone number: ${phoneNumber.value}")
}
companion object {
fun create(phoneNumber: PhoneNumber): PhoneNumberScreen? {
return when {
phoneNumber.value.isNotEmpty() -> PhoneNumberScreen(phoneNumber)
else -> null
}
}
}
}
fun main() {
val contact = Contact(PhoneNumber("12345"))
show(contact.phoneNumber) // "Phone number: 12345"
val contactWithInvalidPhoneNumber = Contact(PhoneNumber(""))
show(contactWithInvalidPhoneNumber.phoneNumber) // prints nothing
}
private fun show(phoneNumber: PhoneNumber) {
val phoneNumberScreen = PhoneNumberScreen.create(phoneNumber)
phoneNumberScreen?.render()
}

This also works as expected but once again we need to document it and on top of that handle any null values returned by the helper function.

Nevertheless this solution seems fine and for a very small code base is quite acceptable. The problem is that it does not scale alongside the code base. Imagine how many helper functions we need to implement every time we have to use a phone number instance in our components if we want to keep them “clean”.

The actual problem

The actual problem lies in the PhoneNumber itself. It represents more than one states and each time we use an instance of it we must “dive” in its value and translate it to that state.

What we really need is a better representation of a valid and invalid phone number.

Final approach

This is where we use sealed classes and separate the two states:

sealed class PhoneNumber
object InvalidPhoneNumber : PhoneNumber()
data class ValidPhoneNumber(val value: String) : PhoneNumber() {
init {
require(value.isNotEmpty()) { "the number cannot be empty" }
}
}

This way we accomplish our main goal: we change the PhoneNumberScreen to accept only instances of ValidPhoneNumber and can now be certain that the screen will be used only with valid data. The code is self documented and any further development in the screen class will not have to consider other states for the phone number:

class PhoneNumberScreen(
private val phoneNumber: ValidPhoneNumber
) {
fun render() {
println("Phone number: ${phoneNumber.value}")
}
}

One big drawback of this change is that every usage of the PhoneNumber class has just broke (see: creation of Contact instances).

Fortunately there is a quick solution for that! Factory function:

A function that has the same name with the previously used class (PhoneNumber) and takes a single string parameter. If the parameter is not empty it returns a ValidPhoneNumber. In any other case it returns an InvalidPhoneNumber:

fun PhoneNumber(value: String): PhoneNumber =
when {
value.isNotEmpty() -> ValidPhoneNumber(value)
else -> InvalidPhoneNumber
}

The end result is almost the same as the starting point but this time we have clear states and components that can guarantee that they will not crash because of erroneous data:

fun main() {
val contact = Contact(PhoneNumber("12345"))
show(contact.phoneNumber) // "Phone number: 12345"
val contactWithInvalidPhoneNumber = Contact(PhoneNumber(""))
show(contactWithInvalidPhoneNumber.phoneNumber) // prints nothing
}
private fun show(phoneNumber: PhoneNumber) {
if (phoneNumber is ValidPhoneNumber) {
val phoneNumberScreen = PhoneNumberScreen(phoneNumber)
phoneNumberScreen.render()
}
}

Make your code reveal its usage

A small and simple example that shows one of the benefits of having domain objects.

Lets assume we need to make a request for some kind of a token and the flow for doing so goes like this:

  1. Validate an email address
  2. With the validated address validate a password
  3. With both the validated values request for the token

The not so revealing way

fun validateEmailAddress(value: String) {
// does some validation
}
fun validatePassword(validEmailAddress: String, password: String) {
// does some validation
}
fun requestToken(validEmailAddress: String, validPassword: String): String {
return "some kind of token"
}

Q: Can the consumer of this API, without knowing the aforementioned flow, figure it out just by looking at the functions’ signatures?

A: I guess she could if the parameters’ names were like that. If not she needs to read the bodies of those functions to see that there is some kind of order that needs to be followed.

Q: Can the creator of this code be 100% certain that the functions will be used as expected and the passed parameters will be valid? For example, will requestToken always be called last and with valid addresses and passwords?

A: No! There is nothing that can guarantee that so, just to be safe, we check in every function that the provided values are valid and make the code flexible enough that, for example, each function could be used on its own. That will, potentially, lead to code duplication or unnecessary abstractions.

Q: What about errors and invalid values? Can the consumer of this API predict the code’s behavior on erroneous inputs without reading a documentation?

A: No. Just no.

The revealing way

First lets enrich our API with some, much needed, domain objects:

sealed class EmailAddress
data class ValidEmailAddress(val value: String) : EmailAddress()
data class InvalidEmailAddress(val value: String, val error: String): EmailAddress()
sealed class Password
data class ValidPassword(val value: String) : Password()
data class InvalidPassword(val value: String, val error: String): Password()
data class Token(val value: String)

Having those constructs we can change the functions and make them reveal both the order they can be used and their behavior:

fun validateEmailAddress(value: String): EmailAddress {
// does some validation and
// if the validation fails it returns InvalidEmailAddress
// otherwise a ValidEmailAddress
}
fun validatePassword(emailAddress: ValidEmailAddress, password: String): Password {
// does some validation against the valid email address
// if the validation fails it returns an InvalidPassword
// otherwise a ValidPassword
}
fun requestToken(emailAddress: ValidEmailAddress, password: ValidPassword): Token {
// having only valid values makes the code simple
// just make the request and return the token
return Token("some kind of token")
}

So lets pretend that we are the consumer of this API and all we have is the code.

Our main goal is to request for a token. By just looking at the functions’ signatures we see that there is a requestToken. Nice! Our task is half way done! What do we need for calling this function? A valid email address and a valid password. Ok. How do we get one of each?

Looking again at the signatures we see validateEmailAddress and validatePassword that return an EmailAddress and Password respectively. Lets check what those are. These are abstractions and each of them has been extended to a valid and an invalid state. The invalid one carries the error that occurred too! So, back to the functions.

We see that validating a password needs to be fed with a valid email address so we first need to call validateEmailAddress, then validatePassword and finally requestToken.

That’s it. We didn’t read the code of the functions and we didn’t have to worry about our inputs.

Use sealed classes for better domain representation

Lets start with the business logic.

We have a task. A task can be unassigned OR it can be assigned either to a user OR a group.

First implementation: the ugly way

One way to implement this is by putting all the logic 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.")
  }
  }
}
view raw uglytask.kt hosted with ❤ by GitHub

By default the task is unassigned and if we want an assigned task we provide a user or a group. The or factor is being enforced by a check in the constructor.

This implementation not only relies on nulls to represent the business logic but it also hides the logic from the developer who has to read the code to understand how to create an assigned task.

The null checking comes also up when we want to figure out if and where a task is assigned which is tedious and can easily lead to bugs. As an example lets consider a simple function that prints the task’s assignment state:

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 two points that we should not neglect are readability and scalability. When using UglyTask if we want our code to be readable, in all cases, we have to pass the arguments by their names (thank you Kotlin 🙂 ):

val le0nidas = User("le0nidas")
val kotlinEnthusiasts = Group("kotlin enthusiasts")
UglyTask("buy milk").printAssignment()
UglyTask("write post", assignedUser = le0nidas).printAssignment()
// here we pass the parameter by name to avoid the usage of null
// which makes it less readable
UglyTask("write kotlin", assignedGroup = kotlinEnthusiasts).printAssignment()
UglyTask("write kotlin", null, kotlinEnthusiasts).printAssignment()

As far as scalability, consider how many changes we need to do to add a new assigned entity. One to the constructor, one to the init function to enforce our business logic and one in every function that we use the task’s state (see: printAssignment()) which adds even more null checks.

Second implementation: The less ugly way

Another way is by 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 also puts all the logic in the task but it removes those null checks and makes it easier for the developer to understand it:

With that said, all other drawbacks in readability and scalability remain the same:

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}")
}
}
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()

Final implementation: 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)
view raw task.kt hosted with ❤ by GitHub

Now, printAssignment() leverages all of Kotlin’s powers, including smart cast, making it easier to the eye:

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}")
}
}

and the rest of the code does not need any extra help like named arguments:

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()
view raw task_usage.kt hosted with ❤ by GitHub

As for scalability, when we want to add a new way of assignment we just extend AssignedTo and we are good to go.