Immutable Objects with Builder Pattern

6
Clap

Builder pattern is useful in creating Immutable objects in multiple steps . Typical example in java is

String str = new StringBuilder()
             .append("Hi")
             .append("How are you?")
             .toString();

As we can see, the above builder helps in creating immutable String in multiple steps.

Java class which has a member variable generally has get and set methods, often called by many names like Plain Old Java Objects (POJO), Model Objects, Value Objects, transfer objects, etc.

Often these classes are used to hold an object at a state with getter and setter methods, like below,

package com.webagam;

public class Employee {
private String id;
private String name;
private String department;
private String organization;

public String getId() {
    return id;
}
public void setId(String id) {
    this.id = id;
}
public String getName() {
    return name;
}
public void setName(String name) {
    this.name = name;
}
public String getDepartment() {
    return department;
}
public void setDepartment(String department) {
    this.department = department;
}
public String getOrganization() {
    return organization;
}
public void setOrganization(String organization) {
    this.organization = organization;
}
}

Builder pattern helps create an immutable object and below are some techniques used to accomplish builder pattern.
Note, whether an object should be immutable or mutable depends on the use cases.
The motive here is only to demonstrate some ways to make achieve immutability.

Method 1

package com.webagam.simple;
public class Employee {
private String id;
private String name;
private String department;
private String organization;
private String email;

public Employee(String id, String name, String department, String organization, String email) {
    this.id = id;
    this.name = name;
    this.department = department;
    this.organization = organization;
    this.email = email;
}

public String getId() {
    return id;
}

public String getName() {
    return name;
}

public String getDepartment() {
    return department;
}

public String getOrganization() {
    return organization;
}

public String getEmail() {
    return email;
}

public static void main(String args[]) {
    String id = "xyz123";
    String name = "user1";
    String department = "HR";
    String organization = "webagam Comp";
    String email = "user1@webagam.com";

    Employee emp = new Employee(id, name, organization, department, email);
}
}

The simplest way to make the above employee immutable is to remove setter methods and make the constructor take all its member as a parameter.

Advantage:

  1. Often simple to achieve, i.e. not to have setter method and take all in the constructor.

Disadvantage:

  1. All parameters, even if the user does not intend to set have to be passed or need multiple constructors.
  2. More prone to errors as the variable can be mismatched. In the above, if we look at it carefully it can be noted organization and department are interchanged in creating new Employee object
    new Employee(id, name, organization, department, email);

Method 2:
Using traditional inner class for builder.

package com.webagam.builder1;

public class Employee {
private String id;
private String name;
private String department;
private String organization;
private String email;

private Employee(String id, String name, String department, String organization, String email) {
    this.id = id;
    this.name = name;
    this.department = department;
    this.organization = organization;
    this.email = email;
}

public String getId() {
    return id;
}

public String getName() {
    return name;
}

public String getDepartment() {
    return department;
}

public String getOrganization() {
    return organization;
}

public String getEmail() {
    return email;
}

public static class Builder {

    private String id;
    private String name;
    private String department;
    private String organization;
    private String email;

    public Builder withId(String id) {
        this.id = id;
        return this;
    }

    public Builder withName(String name) {
        this.name = name;
        return this;
    }

    public Builder withDepartment(String department) {
        this.department = department;
        return this;
    }

    public Builder withOrganization(String organization) {
        this.organization = organization;
        return this;
    }

    public Builder withEmail(String email) {
        this.email = email;
        return this;
    }

    public Employee build() {
        return new Employee(id, name, department, organization, email);
    }

}

public static void main(String args[]) {
    String id = "xyz123";
    String name = "user1";
    String department = "HR";
    String organization = "webagam Comp";
    String email = "user1@webagam.com";
    Employee emp = new Employee.Builder()
                   .withId(id)
                   .withName(name)
                   .withDepartment(department)
                   .withOrganization(organization)
                   .withEmail(email)
                   .build();

}
}

This approach works perfectly fine but one of the disadvantages of this is a more boilerplate method as we need to have a corresponding set method.

Method 3:
Builder with no member variable and redirecting set method.

package com.webagam.builder2;
public class Employee {
private String id;
private String name;
private String department;
private String organization;
private String email;

private void setId(String id) {
    this.id = id;
}

private void setName(String name) {
    this.name = name;
}

private void setDepartment(String department) {
    this.department = department;
}

private void setOrganization(String organization) {
    this.organization = organization;
}

private void setEmail(String email) {
    this.email = email;
}

private Employee() {

}

public String getId() {
    return id;
}

public String getName() {
    return name;
}

public String getDepartment() {
    return department;
}

public String getOrganization() {
    return organization;
}

public String getEmail() {
    return email;
}

public static class Builder {
    Employee emp = new Employee();

    public Builder withId(String id) {
        emp.setId(id);
        return this;
    }

    public Builder withName(String name) {
        emp.setName(name);
        return this;
    }

    public Builder withDepartment(String department) {
        emp.setDepartment(department);
        return this;
    }

    public Builder withOrganization(String organization) {
        emp.setOrganization(organization);
        return this;
    }

    public Builder withEmail(String email) {
        emp.setEmail(email);
        return this;
    }

    public Employee build() {
        return emp;
    }

}

public static void main(String args[]) {
    String id = "xyz123";
    String name = "user1";
    String department = "HR";
    String organization = "webagam Comp";
    String email = "user1@webagam.com";
Employee.Builder builder = new Employee.Builder()    
Employee emp = builder
                   .withId(id)
                   .withName(name)
                   .withDepartment(department)
                   .withOrganization(organization)
                   .withEmail(email)
                   .build();
    //builder.withEmail(ddd@ddd.com); //this will change the emp object.
}
}

In this approach as you can see, builder class does not have any member variable and it directly sets the value in the actual object.

There are many disadvantages with this approach,

  1. As set methods are redirected, this may not be truly immutable because anyone who gets hold of Builder will be able to change an already built object. Hence breaking the use case of immutability.
  2. Typically every call to “build()” returns a new instance of the actual object that is being built. Hence any changes done after built will only affect the successive build(), but not in the above case. Hence again breaking the case of immutability.

Method 4:
Using generics and lambda function to achieve builder,

package com.webagam.builder3;
import java.util.function.Consumer;
public class Employee {
private String id;
private String name;
private String department;
private String organization;
private String email;

private Employee(String id, String name, String department, String organization, String email) {
    this.id = id;
    this.name = name;
    this.department = department;
    this.organization = organization;
    this.email = email;
}

public String getId() {
    return id;
}

public String getName() {
    return name;
}

public String getDepartment() {
    return department;
}

public String getOrganization() {
    return organization;
}

public String getEmail() {
    return email;
}

public static class Builder {
    public String id;
    public String name;
    public String department;
    public String organization;
    public String email;

    public Builder with(Consumer<Builder> function) {
        function.accept(this);
        return this;
    }
    public Employee build() {
        return new Employee(id, name, department, organization, email);
    }

}

public static void main(String args[]) {
    String id = "xyz123";
    String name = "user1";
    String department = "HR";
    String organization = "webagam Comp";
    String email = "user1@webagam.com";
    //1
    Employee emp1 = new Employee.Builder()
            .with($ -> {
                $.id = id;
                $.name = name;
                $.organization = organization;
                $.department = department;
                $.email = email;
            }).build();
    //Uncommenting the below code will result in compilation error. 

    //department = “Fin”; 
    //2
    Employee emp2 = new Employee.Builder()
            .with($ -> {$.id = id;})
            .with($ -> $.name=name)
            .with($ -> $.department = department)
            .build();

}
}

In the above, as you can see we have used a java lambda function to achieve builder.

Advantages:

  1. There need not be any setter method, hence no boiler plate code.
  2. Code looks clear and can also be chained as shown above.

Disadvantages:

  1. Builder class member variables are public. Personally, I don’t think this should be an issue but it may not be agreeable to the general policy of making everything private.
  2. As we use the lambda function, the variable used in the lambda function should be final or implicitly final.
    i.e. in the above example, all variables are not changed once they are set, hence java makes it implicitly final.
    However, uncommenting the “department = fin” code in the above example will result in a compilation error.

Method 5:

Method 5 is same as method 4 with an additional builder method which can tackle one of the disadvantage (2nd one) mentioned above.

package com.webagam.builder4;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
public class Employee {
….. // Omitted for brevity
public static class Builder {
    public String id;
    public String name;
    public String department;
    public String organization;
    public String email;

    public Builder with(Consumer<Builder> function) {
        function.accept(this);
        return this;
    }

    public <D> Builder with(BiConsumer<Builder, D> function, D value) {
        function.accept(this, value);
        return this;
    }

    public Employee build() {
        return new Employee(id, name, department, organization, email);
    }

}

public static void main(String args[]) {
    String id = "xyz123";
    String name = "user1";
    String department = "HR";
    String organization = "webagam Comp";
    String email = "user1@webagam.com";

    //0
    Employee emp = new Employee.Builder()
            .with(($, fname) -> {
                $.name = fname;
            }, name)
            .build();
    //1
    Employee emp1 = new Employee.Builder()
            .with($ -> {
                $.id = id;
                $.name = name;
                $.organization = organization;
                //$.department = department; // If uncommented, this will be a compilation error
                $.email = email;
            }).build();

    //2
    Employee emp2 = new Employee.Builder()
            .with($ -> {$.id = id;})
            .with($ -> $.name=name)
            //.with($ -> $.department = department) 
            //if the above line is uncommented, it will be compilation error 
            //as department is changing and not final
            .build();

    department = "Finance";
    //3
    Employee emp3 = new Employee.Builder()
            .with($ -> {$.id = id;})
            .with($ -> $.name=name)
            .with( ($,dept) -> {
                $.department = dept;}, 
                    department)
            .build();
}
}

Advantage & dis advantage are similar to method 4, except for the fact that it helps get rid of 2nd dis advantage.

Method 6:
Finally, Project Lombok is a good tool to generate getter or setter (or both) not just for builder use case but also for any class.
Hence Employee can be with @Getter annotation and Builder can have @Setter annotation.

This do help in getting rid of boiler plate.

package com.webagam.builder5;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;

public @Getter class Employee {private String id;
private String name;
private String department;
private String organization;
private String email;

private Employee(String id, String name, String department, String organization, String email) {
    this.id = id;
    this.name = name;
    this.department = department;
    this.organization = organization;
    this.email = email;
}


public @Data static class Builder {
    public String id;
    public String name;
    public String department;
    public String organization;
    public String email;

    public Employee build() {
        return new Employee(id, name, department, organization, email);
    }

}
}

Each of the above methods can be debated to be good or bad, but hope each of them will have a place in different use cases.


Also published on Medium.

One Reply to “Immutable Objects with Builder Pattern”

Comments are closed.