Item 2: Consider a builder when faced with many parameters - saurabhojha/Effective-java GitHub Wiki
When the number of optional fields in a class increases, their instantiation starts to become cumbersome, unreadable, and confusing to the client code.
When a class has more than four fields, remembering the order of parameters becomes difficult and may lead to confusion and runtime errors.
Example:
Let us use the example of Chitti robot from Robot.
To make our Chitti class, let us define the following required fields:
- clock speed in hertz (int)
- memory in zettabyte (int)
Let us define four optional fields:
- water resistance (default true)
- fire resistance (default true)
- inbuilt hand gun (default false)
- relation with Sana (an enumeration)
With so many parameters in place, it's time to analyse different techniques we can use for instantiating the object of class "Chitti".
Anti-pattern:
These are certain patterns in software development that are considered bad practices. These patterns should be strictly avoided.
THE TELESCOPING CONSTRUCTOR ANTI-PATTERN:
A telescoping constructor anti-pattern is a technique through which we can overload the constructors to instantiate objects with different set of parameters. This design is not a good practice and is yet used widely. (You could say it's borderline anti-pattern).
Chitti class following telescopic constructors can be defined as follows:
class Chitti {
public static enum RELATION {TOYFRIEND,BOYFRIEND};
//required parameters.
private int speedInThz;
private int memoryInZettabyte;
// Optional parameters.
private RELATION relationWithSana;
private boolean fireResistance;
private boolean waterResistance;
private boolean handGun;
//constructor with all parameters.
public Chitti(int speedInThz,int memoryInZettabyte, RELATION relationWithSana, boolean fireResistance, boolean waterResistance,
boolean handGun) {
this.speedInThz = speedInThz;
this.memoryInZettabyte = memoryInZettabyte;
this.relationWithSana = relationWithSana;
this.fireResistance = fireResistance;
this.waterResistance = waterResistance;
this.handGun = handGun;
}
// creating default chitti without a handgun
public Chitti(int speedInThz,int memoryInZettabyte, RELATION relationWithSana, boolean fireResistance, boolean waterResistance) {
this(speedInThz,memoryInZettabyte,relationWithSana,fireResistance,waterResistance,false);
}
// creating default chitti without handgun and with water resistance
public Chitti(int speedInThz,int memoryInZettabyte, RELATION relationWithSana, boolean fireResistance) {
this(speedInThz,memoryInZettabyte,relationWithSana,fireResistance,true,false);
}
// creating default chitti without handgun and with water resistance and fire resistance
public Chitti(int speedInThz,int memoryInZettabyte, RELATION relationWithSana) {
this(speedInThz,memoryInZettabyte,relationWithSana,true,true,false);
}
//creating default chitti with who is a toyfriend of Sana
public Chitti(int speedInThz,int memoryInZettabyte) {
this(speedInThz,memoryInZettabyte,RELATION.TOYFRIEND,true,true,false);
}
}
As you can see the code already looks messy.
Disadvantages of this pattern:
- If you want to create an object with the optional fields the client code for instantiation becomes long and confusing. For example: In order to create a Chitti who is Sana's boyfriend, and is not water resistant, the instantiation would look like:
Chitti chitti = new Chitti(1,1,Chitti.RELATION.BOYFRIEND,true,false,true);
As we can see this instantiation is not very clear to a client code. To instantiate such an object would mean careful reading of the documentation.
- With great parameters, comes great confusion. And a possible chance of wrong instantiation. Suppose you want to create a water resistant chitti who's susceptible to fire damage, then the instantiation would look like:
Chitti chitti = new Chitti(1,1,Chitti.RELATION.BOYFRIEND,true,false,true);
But wait, you just made a chitti with fire resistance but susceptible to water damage (fire resistance is the fourth parameter!!). Which is the second weakness of this anti-pattern.
Long sequences of identically typed parameters can cause subtle bugs. If the client accidentally reverses two such parameters, the compiler won’t complain, but the program will misbehave at runtime.
-
Telescoping of constructors does not scale well with no of fields. As the number of fields in the class increase, the constructor code gets cumbersome and difficult to read and maintain.(Good luck instantiating chitti with tons of features)
-
All the constructors delegate to a default constructor. This makes the code redundant.
THE JAVA BEANS PATTERN:
In short, initialise object using empty constructor and then use setters to set the values later.
Our Chitti class implemented using this pattern is as follows:
class Chitti {
public static enum RELATION {TOYFRIEND,BOYFRIEND};
//required fields with impossible values
private int speedInThz = -1;
private int memoryInZettabyte = -1;
//optional fields
private boolean fireResistance = true;
private boolean waterResistance = true;
private boolean handGun = false;
private RELATION relationWithSana = RELATION.TOYFRIEND;
//empty constructor
Chitti() {
}
//setters for field initialisation.
public void setSpeedInThz(int speedInThz) {
this.speedInThz = speedInThz;
}
public void setMemoryInZettabyte(int memoryInZettabyte) {
this.memoryInZettabyte = memoryInZettabyte;
}
public void setFireResistance(boolean fireResistance) {
this.fireResistance = fireResistance;
}
public void setWaterResistance(boolean waterResistance) {
this.waterResistance = waterResistance;
}
public void setHandGun(boolean handGun) {
this.handGun = handGun;
}
public void setRelationWithSana(RELATION relationWithSana) {
this.relationWithSana = relationWithSana;
}
}
At first it might seem like a good design. It eliminates the redundancy we suffered in the telescoping constructors. It is way more readable as well. A client won't even mess up the parameters now. But there are some serious disadvantages java beans pattern suffers.
- The state of the object might not be as we desire while it is getting constructed. In case of large code, this inconsistency is very difficult to debug. Suppose we want to create a Chitti robot that has is to be used as a fire detecting robot and thus has to be susceptible to fire damage. This can be done in the following way.
Chitti chitti = new Chitti();
chitti.setFireResistance(false);
deployChitti(chitti); //A function that deploys chitti somewhere
Suppose this client implementation was written by Dr Bohra(That evil guy) whose sole purpose in life is to mess up chitti. So he changes the implementation as follows.
Chitti chitti = new Chitti();
deployChitti(chitti); //A function that deploys chitti somewhere
chitti.setFireResistance(false); //Oh no we deployed a fire resistant chitti instead of fire susceptible one!!
As we can see the state of the object is now dependent on the implementation of the client and is not guaranteed to be consistent with the requirements.
Because construction is split across multiple calls, a JavaBean may be in an inconsistent state partway through its construction. The class does not have the option of enforcing consistency merely by checking the validity of the constructor parameters. Attempting to use an object when it’s in an inconsistent state may cause failures that are far removed from the code containing the bug and hence difficult to debug.
- Another disadvantage of this pattern is that we cannot make a class immutable using this pattern. If we declare our fields final, we would have no means of setting their values using setters.
THE BUILDER PATTERN
Builder pattern combines the best of both telescoping and java beans pattern. (The method described here is a form of a builder pattern).
To design our Chitti class using this pattern, we need to follow the following three steps:
- Make a builder class that receives all the required parameters from the client implementation. The client will call the constructor of this builder. The client will receive a builder object.
- The client will use the setter methods to set the optional fields as described in the builder class using the builder object returned in the first step.
- The client will then call a parameterless build method in the builder class to create the object of the original class.
This builder class is generally a static member of the original class.
Our Chitti class implemented this way would be as follows:
class Chitti {
public static enum RELATION {TOYFRIEND,BOYFRIEND};
//Required parameters.
private final int speedInThz;
private final int memoryInZettabyte;
// Optional parameters.
private final RELATION relationWithSana;
private final boolean fireResistance;
private final boolean waterResistance;
private final boolean handGun;
//constructor for object creation.
private Chitti(ChittiBuilder chittiBuilder) {
this.speedInThz = chittiBuilder.speedInThz;
this.memoryInZettabyte = chittiBuilder.memoryInZettabyte;
this.relationWithSana = chittiBuilder.relationWithSana;
this.fireResistance = chittiBuilder.fireResistance;
this.waterResistance = chittiBuilder.waterResistance;
this.handGun = chittiBuilder.handGun;
}
public static class ChittiBuilder {
//Required parameters.
private final int speedInThz;
private final int memoryInZettabyte;
// Optional parameters.
private RELATION relationWithSana = RELATION.TOYFRIEND;
private boolean fireResistance = false;
private boolean waterResistance = false;
private boolean handGun = false;
public ChittiBuilder(int speedInThz,int memoryInZettabyte) {
this.speedInThz = speedInThz;
this.memoryInZettabyte = memoryInZettabyte;
}
//Setters for optional fields.
public ChittiBuilder setFireResistance(boolean fireResistance) {
this.fireResistance = fireResistance;
return self();
}
public ChittiBuilder setWaterResistance(boolean waterResistance) {
this.waterResistance = waterResistance;
return self();
}
public ChittiBuilder setHandGun(boolean handGun) {
this.handGun = handGun;
return self();
}
public ChittiBuilder setRelationWithSana(RELATION relationWithSana) {
this.relationWithSana = relationWithSana;
return self();
}
protected ChittiBuilder self() {
return this;
}
//Build function to finally instantiate the object of class chitti;
public Chitti build() {
return new Chitti(this);
}
}
//getters for demo
public int getspeedInThz() {
return this.speedInThz;
}
public int getmemoryInZettabyte() {
return this.memoryInZettabyte;
}
public String toString() {
return "Hi, I am Chitti!! Chitti the Robot. Speed: "+this.getspeedInThz()
+" tera hertz. Memory: "+this.getmemoryInZettabyte()+" Zettabyte.";
}
}
This method has many advantages.
- Suppose we have a to create a Chitti robot which is fire and water resistant and has a hand gun. The client implementation for the same would be as follows:
Chitti chitti = new Chitti.ChittiBuilder(1,1).setFireResistance(true).setHandGun(true).setWaterResistance(true).build();
If we call the toString method in the Chitti class, we get the following output: Hi, I am Chitti!! Chitti the Robot. Speed: 1 tera hertz. Memory: 1 Zettabyte.
The code is both easy to write and read. The client has a cleaner implementation now.
Another advantage of builder pattern is that it is well suited for class hierarchies. This is achieved using recursive type bounds in generics. This is an extensive topic and deserves a wiki page of it's own!! In the meanwhile you can read this article to get an understanding of such a builder.
A builder class can create immutable objects. (The chitti we created is immutable!!)
A builder class also has the ability to house multiple varargs as every field has its own setter. This is not the case with constructors. The builder pattern is flexible. For a given class it can create objects that vary from one another. (create a fire resistant chitti who is capable of housing a handgun. Or a simple chitti who is Sana's boyfriend using the same builder!!)
Disadvantages:
- This pattern incurs a cost of creation of builder before the actual creation of object can take place.
- Using builder for classes with less number of parameters is not a good choice since the implementation is long. (You have to decide how many fields is your threshold. The book suggests to use builder as soon as the number of fields exceed four or more.)