Implementing nullable object state
Previously I wrote about avoiding nulls using NullObject
and Optional
method return value. In this post I will focus on modelling optionality in an object internal state in cases where NullObject
is not suitable. This means when implementing the basic building blocks for internal state of Value Objects and Entities or working with data structures instead of behaviour.
If we keep our classes small then this is a much smaller problem than having nulls passed around between objects. Still, the more of the actual domain constraints we are able to convey in our static code structure the better. This means making the optionality obvious instead of forcing the reader to look up usages of the field.
In general we have two choices:
- use annotations like Lombok
NotNull
to mark what fields cannot be null - use some kind of optional value container type like Vavr
Option
(note that we should not use Java Optional for fields — more details)
Annotations approach
With Lombok it looks like this:
@Getter
@AllArgsConstructor
class Customer {
@NotNull
Id id
@NotNull
Name name
@Getter(AccessLevel.NONE)
Email email ... Optional<Email> getEmail() {
return Optional.ofNullable(email)
} }
The main benefit of this solution is that it does not require any new dependencies assuming that Lombok is used anyway.
However, there are several drawbacks. Typically most of the fields should not be nullable. So it is counterintuitive that we need to mark the fields following the general rule of non nullability instead of marking the nullable as a special case.
Secondly, it is too easy to forget adding the NotNull
annotation since it does not directly affect our own code. This means not having a NotNull
on a field cannot be trusted as it may be an omission by accident.
Thirdly, Lombok NotNull
is just an explicit NPE so does not really offer protection on the outer edges (e.g when handling incoming HTTP requests) of our application. Without tests it will just buy us early NPE at runtime and save some debugging effort.
We can alleviate some of these problems by using static code quality checkers like FindBugs or NullAway. The latter can be used with Nullable
annotations. Main problem with static checkers is that unless they integrate seamlessly with our IDE or local build they are easy to ignore.
Optional value container approach
With optional value containers like Vavr Option
the main drawback is having to introduce a new library. However, this approach allows to make the optionality of a value really explicit. We force ourselves to handle the null case and don’t rely on some external tool to do the verification for us.
I would not use Option
for any parameters though. Providing overloaded versions of constructor and methods is more convenient for consumers. It looks like this:
@Getter
@AllArgsConstructor(AccessLevel.PRIVATE)
class Customer {
Id id
Name name
Option<Email> email public Customer(Id id, Name name) {
this(id, name, Option.none())
}
public Customer(Id id, Name name, Email email) {
this(id, name, Option.of(email))
}}
There is no need to manually implement getter for the optional “email” field which is another small benefit.
What is really awesome with this approach is that we can also use Option in our command objects when transforming HTTP requests. This allows us to handle the nulls before they reach deeper into our application.
vavr supports jackson serialization/deserialization with vavr-jackson lib (in Spring we just need to declare new VavrModule bean).
An example new customer command with optional email:
@Getter
@Setter
class NewCustomerRequest {
String firstName
String lastName
Option<String> email
}
This is a lot more elegant than having to remember that some fields may be null whenever working with the command object.
Summary
Both solutions to nullability require everyone in the team to follow the agreed approach. However, I think modelling optional fields using optional value containers like Option
is more explicit and easier to follow than using annotations no matter how these are processed. The benefits of Option
become especially obvious when implementing command objects for handling incoming requests.