Written by Michał Amerek
Pragmatic Programmer
Published March 12, 2020

To build or not to build – refactoring towards immutability

Builder pattern to gain object immutability is the most commonly used solution. I analyze its advantages, disadvantages and alternatives based on a real-life example.

Immutability in itself is widely considered good. First of all, it avoids side effects, and thus makes the code and handling it safer. Personally, I think that it is always worth considering whether the immutability in our particular case is justified. In my opinion, it is seriously questioned by the mutable nature of the entity about which I wrote in To mutate or not – on Entities and Value Objects. This time, however, I will focus on a real-life example and convention adopted there. I assume here that the issue of immutability is known as well as the solution using Builder.

Finals and copies

Let’s say then that we want our object to be immutable. So, it would be useful to:

  • have our class final, so that it could not be extended (although there are some objections to it out there)
  • create, during construction, copies of mutable objects that are to be on hold
  • return copy of an object from mutators

In the first, unquestionable step, we make all our fields final and initialized in the constructor.  But what if:

  • some things are optional but the others required?
  • or the valid state of the object might be created in different ways, in different variations of properties and relations between them?

The solution here is to use telescoping constructors. However, if we still have more fields and these variations, we can quickly see that this leads to a serious mess in the code.

public class Clazz {

    private final field1;
    (…)
    private final fieldN;

    private Clazz(field1, (…), fieldN) { (…) }

    public Clazz(field1, field2) {
        return this(field1, field2, null, (…), null);
    }
    
    (…)

    public Clazz(field1, fieldN) {
        return this(field1, null, (…), null, fieldN);
    }
}

Builder to the rescue

And here comes the Builder and permanently inscribes in our convention of the immutability of objects. The code lives and grows with new properties, and we will from now on, each time a new one is added:

  • add it to the fields, to the parameters of a constructor of a target object (created by Builder) and to equals()  and hashcode() 
  • duplicate it on the Builder itself
  • and additionally, automatically, create certain mutator withNewField() , which modifies the state of Builder
  • and while creating a target object, in build()  method, pass it to a target object constructor

That’s the example from real-life. Our object has 25 properties, and 800 lines of code, just  to handle them. There is no other business logic, no methods. In addition, we live in the world of REST and CRUD, and the HTTP request is directly mapped to it thanks to JSON annotations and deserialization. Later, we still modify the data using Builder. And of course, each object has its own Builder. 

public class Target {

    private final field1;
    (…)
    private final fieldN;

    private Target(field1, (…), fieldN) { (…) }

    public static class Builder {

        private field1;
        (…)
        private fieldN;

        public Builder withField1(field1) {
            this.field1 = field1;
            return this;
        }

        (…)
        public Builder withFieldN(fieldN) {
            this.fieldN = fieldN;
            return this;
        }

        public Target build() {
            return new Target(this.field1, …, this.fieldN);
        }
    }
}

What have we done? We extracted a mutable state to Builder, which we use in the code so often yet, but we wanted to avoid mutability. Moreover, we pass it between different components that modify it and we rely on those changes (a real-life example).

Questions are born

Our Builder almost ceases to differ from another, otherwise questioned, Java Bean convention. Instead of setters, we have the same number of methods mutating the state, so-called withers. A significant difference is the build()  method, which, unlike Bean, allows validation of the object’s state after mutation. It is worse if, in reality, we do not have any validation there or we manipulate our data arbitrarily (a real-life example).

It is reasonable to ask yourself a few questions:

  • is our object really immutable? Perhaps. However, it changes so often, and in so many ways, is that simply mutable?
  • or maybe just the opposite, it doesn’t change often and we don’t have to automatically expose mutating methods for every property?
  • do we always call all n with() methods one after the other, or maybe only a specific group in certain circumstances? Do we need a fluent interface at all? What are our use cases? What about Single Responsibility?
  • Mybe we don’t have to pass everything to the constructor if in the end some things are required and some others are not?
  • Finally, maybe we could take a step back and avoid Builder at all, still ensuring the immutability of our object? But how?

How?

Going back – now we don’t add telescoping constructors and we don’t make all the fields final. We introduce constructor only for the required properties, we group their relations into various smaller objects, provide only the necessary mutators and we still return copies. Our object can remain immutable without boilerplate code. That sounds easy. And it is worth to keep this in mind before going for any convention.

public class Clazz {

    private final Something required1;
    private final SomethingOther required2;
    (…)
    private SomethingOptional optional;

    public Clazz(final Something required1, final SomethingOther required2) {
        this.required1 = required1;
        this.required2 = required2;
    }

    public Clazz somethingOptional(final SomethingOptional optional) {
        final Clazz copy = new Clazz(this.required1, this.required2);
        copy.optional = optional; 
        return copy;
    }
}

The question is, how to make such drastic changes if the adopted convention spreads over the entire codebase? How to avoid duplication of code, boilerplate, and at the same time preserve the advantages of Builder, which we are used to:

  • named mutators, instead of, and cause of lack in Java, named parameters
  • state validation on build after mutation
  • fluent interface

Builder with Reference

What if Builder would create a target object in the constructor, and mutating methods with()  would work directly on it, and the build()  would produce a copy of that target object?

public class Target {

    private final requiredField;
    (…)
    private optionalField;

    private Target(requiredField) {
       this.requiredField = requiredField;
    }

    public static class Builder {

        private final Target;
        
        public Builder(requiredField) {
            this.target = new Target(requiredField);
        }
        (…)
        public Builder withOptionalField(optionalField) {
            this.target.optionalField = optionalField;
            return this;
        }

        public Target build() {
            validate(this.target);
            return this.target.copy();
        }
    }
}

Keep in mind that this approach has one major drawback. It assumes that there is a simple and constant way to create a target object with all its required properties known during the Builder’s construction. However, if we have the ability to produce the correct object in different variants, which is what the de facto Builder is for, then we fall into the trap from which we escaped. This issue was solved by telescoping constructors and now we would have to bring it back to Builder itself, which is not good.

Wrapper for Data

What about reversing the above approach and treating Builder as fully mutable data (data object) in the situation when our target object is an immutable one, and making the target as a wrapper for this data – its copy?

The solution seems to retain all the advantages of Builder and avoid its disadvantages.

public class Target {

    private final Data data;

    public Target(final Data data) {
        validate(data);
        this.data = data.copy();
    }
}

We’ve introduced coarse-grained constructor with one argument, but that’s the way we actually should start with anyway – to reduce the number of constructor arguments by grouping them into other objects.

We’ve also made a clear separation between mutable data and the immutable target object that cares about most important things – validation and the copy. Our data object seems to remind the Data Transfer Object, commonly used in a layered architecture. Then, the mapping of request to the data (instead of the target object) seems also reasonable.

@POST
public Data create(final Data data) {
    (…)
}

Even if it is out of domain model scope and lives in the application, and shouldn’t have contact with the target object, we actually could treat both as our (request) model as far as it is about the data. So the transformation, commonly used in so-called Assemblers (fromDto() , toDto() ), can also be treated as a part of that model:

public class Data {

    // fields

    private Data() {}

    public static Data from(final Target target) {
        // set fields on new from Target
    }

    public Target create() {
        return new Target(this);
    }

}

Functional modifier

Ok, but how to set the data fields? Again, with constructor consisting of all the fields? Setters or withers? If it is about mapping JSON request we could just use annotations with direct access to the private fields (with @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY ).

Moreover, the data fields can be actually thought of as a public and modifiable (which is, of course, controversial) – our wrapper (Target) would ensure their validation and invariability still so we would be safe anyway! Then, we could also enter one method mutator to keep the interface fluent for people preferences:

public class Data {

    public field1;
    (…)
    public fieldN;

    public Data with(final Consumer<Data> dataConsumer) {
        dataConsumer.accept(this);
        return this;
    }
}

and use it like:

Target target = data.with(data -> {
    data.field1 = (…) ;
    (…)
    data.fieldN = (…) ;
}).create();

Summary

Finally, leaving aside mapping JSON to model and mixing different annotations on a one and the same single entity, like persistence ones and presentation ones (by the way – which is not good either), we could really achieve immutability in a simpler way. The point is to think about the object and its responsibilities, but not about the data. The correct way would be:

public class Clazz {
   
    (…)

    public Clazz makeTheDifferenceInSingleCall(final AllYouNeed withAllYouNeed) {
        final Clazz copy = this.copy();
        // modify a copy with all you need
        return copy;
    }
}

I hope that the above kinda-code is understandable.

That’s all about immutability. If you want to avoid side effects, just deal with copies but not in order to share mutable object. The Builder itself can be helpful in some mentioned cases, like reducing telescoping constructors or generally in producing a valid object in different variants or family of objects. Ironically, the mutable nature of Builder can be also on purpose. That is to provide some context for different providers, where the target object can’t be created right away. Also, think here about some process being performed in steps. Think about it as some kind of Wizard. The valid and complete object is produced (build) on the last step, but until then the Builder is stored. Then most probably it would be good to limit its interface to the purpose of provider or step using some abstraction. Anyway, for whatever reason you use Builder, it’s mutable! So, we have to be careful dealing with it to not fell into the trap of side effects still.

Written by Michał Amerek
Pragmatic Programmer
Published March 12, 2020