Blog | The Immutable, the Builder and Jackson | Convit
The Immutable, the Builder and Jackson
Why would we need immutable classes? Aren't classes using setters and getters, i.e. POJOs, enough?
With the upcoming of even more distributed systems and multithreaded programming it's getting harder and harder to reason about the state of a software system. POJOs or mutable classes may be shared between different threads or actors of a software system. They may change the state of the class and due to the non deterministic nature of distributed systems one can not predict the state of a class that was changed by different actors. This is commonly known as a data race condition.
Immutable classes or immutable data in general take one variable out of the reasoning about the state. The state of immutable data is known after creation and will never change. As it never changes, immutable classes are also inherently threadsafe and may be shared between threads.
How do we make a Java class immutable?
- All fields should be final and a class should only provide getters.
- All return values of accessor methods must be immutable as well.
- A special case are own classes that are members of another class. These must be immutable as well to guarantee total immutability.
Using the implementation above there is only one way to construct an instance of `Product`; using the constructor. Depending on the number of fields the constructor arguments may become unwieldy. Some of them may be optional as well and we have a nice cascade of telescoping constructors. An option to circumvent these "problems" is using a Builder.
According to wikipedia
the Builder is a design pattern designed to provide a flexible solution to various object creation problems in object-oriented programming. The intent of the Builder design pattern is to separate the construction of a complex object from its representation.
Here we use it to hide the unwieldy number and optional constructor parameters.
The simplest builder has a corresponding method for every field that temporarily saves the designated value in a field and creates the actual object when calling a designated method often called
create. An example is noted down below:
Every initializer method returns itself so calls may be chained fluently. Default values may be set on field initialization. Mutable structures may be used to allow cumulative addition of list values or similar values. Also one could put some initialization logic or guards into an initializer method.
When the builder is in place it is good practice to add a static method
builder() and a static
copy(Pojo p) method to the original pojo. The `builder` method creates a new empty instance of the Builder class. Whereas the `copy` method takes an already populated instance of the immutable class and returns a builder that is preinitialized with the values of the existing instance. Parts of it may then be overwritten See the repository for implementations.
One disadvantage of this builder implementations is that they are not threadsafe. Different threads using the same builder instance may run into a data race. So it must not be shared between different threads. But as we can shortly see it is easier to use than an immutable builder.
Immutable version of a builder
A builder may also be made immutable and inherits the same advantages as an immutable object. The builder will be threadsafe and one can reason about the state if used in multiple threads.
To make a builder immutable the initializer methods must return a new instance of the builder with the new values. See below for an example:
The advantage is that these kind of builders are also threadsafe. But you must always assure to assign the return value of an initializer method. That is why the mutable version of the builder is easier to use. Once a value is set for an instance it is set. Whereas in the immutable builder case the change is lost when the return value is not assigned.
Now we have nice immutable classes that may only be created using a builder so we always know what state they are in. What happens if we try to send these objects via HTTP/JSON to somebody else?
Serializing such an object to json using the Jackson library should work out of the box, as long as the getter follow the JavaBean conventions and are prefixed with
get. If they don't, every getter needs to contain an annotation
@JsonProperty so jackson can identify them as a json property. Any other jackson annotation that controls serialization may be used here as well.
Deserializing does not work out of the box as the setters are missing. There are two ways to enable succesful deserialization of these objects. Annotating the immutable classes constructor with
@JsonCreator and every parameter with
@JsonProperty. This is error prone as when adding new properties is easily forgotten. Also it violates the assumption that we have only one mean to construct an object and it is possible to bypass logic we added to the builder when using the constructor directly. Especially default values, nullability checks and others.
Fortunately Jackson already addresses this problem and provides us with annotations that allow to use a builder instead of the constructor for deserializing an object from json.
The immutable classes must be annotated with
@JsonDeserialize(builder = ProductBuilder.class) and the builder class must be declared as a pojo builder and be annotated with
@JsonPOJOBuilder(). The creator method of the builder and prefixes for the initializer methods may be specified as parameters. See JsonPOJOBuilder javadoc for details. By default the prefix is
with and the constructor methods called
build. So in the code below we need to set the prefix to empty
"" as we don't use a prefix.