Setter returning this vs builder

Viewed 1289

I was wondering, when constructing an object, is there any difference between a setter returning this:

public User withId(String name) {
    this.name = name;
    return this;
}

and a builder (for example one which is generated by Builder Generator plugin for IDEA)?

My first impression is that a setter returning this is much better:

  • it uses less code - no extra class for builder, no build() call at the end of object construction.
  • it reads better:
    new User().withName("Some Name").withAge(30);
    
    vs
    User.UserBuilder.anUserBuilder().withName("Some Name").withAge(30).build();
    

Then why to use builder at all? Is there anything I am missing?

6 Answers

Builders are design patterns and are used to bring a clear structure to the code. They are also often used to create immutable class variables. You can also define preconditions when calling the build() method.

The crucial thing to understand is the concept of an immutable type.

Let's say I have this code:

public class UnitedStates {
    private static final List<String> STATE_NAMES =
        Arrays.asList("Washington", "Ohio", "Oregon", "... etc");

    public static List<String> getStateNames() {
        return STATE_NAMES:
    }
}

Looks good, right?

Nope! This code is broken! See, I could do this, whilst twirling my moustache and wielding a monocle:

UnitedStates.getStateNames().set(0, "Turtlia"); // Haha, suck it washington!!

and that will work. Now for ALL callers, apparently there's some state called Turtlia. Washington? Wha? Nowhere to be found.

The problem is that Arrays.asList returns a mutable object: There are methods you can invoke on this object that change it.

Such objects cannot be shared with code you don't trust, and given that you don't remember every line you ever wrote, you can't trust yourself in a month or two, so, you basically can't trust anybody. If you want to write this code properly, all you had to do is use List.of instead of Arrays.asList, because List.of produces an immutable object. It has zero methods that change it. It seems like it has methods (it has a set method!), but try invoking it. It won't work, you'll get an exception, and crucially, the list does not change. It is in fact impossible to do so. Fortunately, String is also immutable.

Immutables are much easier to reason about, and can be shared freely with whatever you like without copying.

So, want your own immutable? Great - but apparently the only way to make one, is to have a constructor where all values are set and that's it - immutable types cannot have set methods, because that would mutate them.

If you have a lot of fields, especially if those fields have the same or similar types, this gets annoying fast. Quick!

   new Bridge("Golden Gate", 1280, 1937, 2737);

when was it built? How long is it? What's the length of the largest span?

Uhhhhhhh..... how about this instead:

   newBridge()
        .name("Golden Gate")
        .longestSpan(1280)
        .built(1937)
        .length(2737)
        .build();

sweet. Names! builders also let you build over time (by passing the builder around to different bits of code, each responsible for setting up their bits). But a bridgebuilder isn't a bridge, and each invoke of build() will make a new one, so you keep the general rules about immutability (a BridgeBuilder is not immutable, but any Bridge objects made by the build() method are.

If we try to do this with setters, it doesn't work. Bridges can't have setters. you can have 'withers', where you have set-like methods that create entirely new objects, but, calling these 'set' is misleading, and you create both a ton of garbage (rarely relevant, the GC is very good at collecting short lived objects), and intermediate senseless bridges:

    Bridge goldenGate = Bridge.create().withName("Golden Gate").withLength(2737);

somewhere in the middle of that operation you have a bridge named 'Golden Gate', with no length at all.

In fact, the builder can decide to not let you build() bridge with no length, by checking for that and throwing if you try. This process of invoking one method at a time can't do that. At best it can mark a bridge instance as 'invalid', and any attempt to interact with it, short of calling .withX() methods on it, results in an exception, but that's more effort, and leads to a less discoverable API (the with methods are mixed up with the rest, and all the other methods appear to throw some state exception that is normally never relevant.. that feels icky).

THAT is why you need builders.

NB: Project Lombok's @Builder annotation gives you builders for no effort at all. All you'd have to write is:

import lombok.Value;
import lombok.Builder;

@Value @Builder
public class Bridge {
    String name;
    int built;
    int length;
    int span;
}

and lombok automatically takes care of the rest. You can just Bridge.builder().name("Golden Gate").span(1280).built(1937).length(2737).build();.

I think your question is better formulated like:

Shall we create a separate Builder class when implementing the Builder Pattern or shall we just keep returning the same instance?

According to the Head First Design Patterns:

Use the Builder Pattern to encapsulate the construction of a product and allow it to be constructed in steps.

Hence, the Encapsulation is important point.

Let's now see the difference in the approaches you have provided in your original question. The main difference is the Design, of how you implement the Builder Pattern, i.e. how you keep building the object:

  1. In the ObjecBuilder separate class approach, you keep returning the Builder object, and you only(!) return the finalized/built Object, after you have finalized building, and that's what better encapsulates creation process, as it's more consistent and structurally well designed approach, because you have a clearly separated two distinct phases:

    1.1) Building the object;

    1.2) Finalizing the building, and returning the built instance (this may give you the facility to have immutable built objects, if you eliminate setters).

  2. In the example of just returning this from the same type, you still can modify it, which probably will lead to inconsistent and insecure design of the class.

It depends on the nature of your class. If your fields are not final (i.e. if the class can be mutable), then doing this:

new User().setEmail("alalal@gmail.com").setPassword("abcde");

or doing this:

User.newBuilder().withEmail("alalal@gmail.com").withPassowrd("abcde").build();

... changes nothing.

However, if your fields are supposed to be final (which generally speaking is to be preferred, in order to avoid unwanted modifications of the fields, when of course it is not necessary for them to be mutable), then the builder pattern guarantees you that your object will not be constructed until when all fields are set. Of course, you may reach the same result exposing a single constructor with all the parameters:

public User(String email, String password);

... but when you have a large number of parameters it becomes more convenient and more readable to be able to see each of the sets you do before building the object.

One advantage of a Builder is you can use it to create an object without knowing its precise class - similar to how you could use a Factory. Imagine a case where you want to create a database connection, but the connection class differs between MySQL, PostgreSQL, DB2 or whatever - the builder could then choose and instantiate the correct implementation class, and you do not need to actually worry about it.

A setter function, of course, can not do this, because it requires an object to already be instantiated.

The key point is whether the intermediate object is a valid instance.

If new User() is a valid User, and new User().withName("Some Name") is a valid User, and new User().withName("Some Name").withAge(30) is a valid user, then by all means use your pattern.

However, is a User really valid if you've not provided a name and an age? Perhaps, perhaps not: it could be if there is a sensible default value for these, but names and ages can't really have default values.

The thing about a User.Builder is the intermediate result isn't a User: you set multiple fields, and only then build a User.

Related