Introduction
In this article, we'll be breaking down the Builder Design Pattern and showing it's application in Java.
Design Patterns are simply sets of standardized practices commonly used in the software development industry. They represent solutions, provided by the community, to common problems faced in every-day tasks regarding software development.
Knowing abstraction, inheritance, and polymorphism does not necessarily make you a good object-oriented designer out of the box. A design expert creates designs that are maintainable and flexible, but most importantly - understandable.
A good idea bounded to the inventor isn't such a good idea.
Creational Design Patterns
Creational Design Patterns focus on object creation. Object creation is a really important part in object-oriented design, and optimizing this task in high-performance and complex applications is paramount.
These patterns control the way we define and design the objects as well as how we instantiate them. Some encapsulate the creation logic away from users and handles creation (Factory and Abstract Factory), some focus on the process of building the objects themselves (Builder), some minimize the cost of creation (Prototype) and some control the number of instances on the whole JVM (Singleton).
In this article, we'll be diving into the Builder Design Pattern.
The Builder Design Pattern
Definition
The Builder Design Pattern separates the construction of a complex object from its representation. This is done via a nested static
class that assigns the required values before the instance is returned.
Another thing to note is that the Builder Pattern is often used to create immutable objects. The existence of setter methods pretty much defies immutability, and since we don't use them when we have the Builder Pattern in place, it's a lot easier to make immutable objects - without having to pass all parameters in the constructor call.
Motivation
Instantiating an object in Java is simple. We use the new
keyword, followed by the constructor and the parameters we're assigning to the object. A typical instantiation can look like:
Cookie chocolateChip = new Cookie("Chocolate Chip Cookie");
A String is passed to the constructor, and it's pretty evident without seeing the class definition that it represents the cookie type/name.
Though, if we want to instantiate a more complex class, such as a neural network, in this style, we're faced with:
SingleLayerNetwork configuration = new NeuralNetConfiguration(4256, STOCHASTIC_GRADIENT_DESCENT,
new Adam(), 1e-4, numRows*numColumns,
1000, RELU, XAVIER);
Even with just 8 parameters, the code quickly becomes unreadable and incomprehensible. Even for the developer who wrote the class definition in the first place. What happens when a new developer tries using this class?
Or better yet, imagine having to call the constructor of this class to instantiate it:
public class SmartHome {
private String name;
private int serialNumber;
private String addressName;
private String addressNumber;
private String city;
private String country;
private String postalCode;
private int light1PortNum;
private int light2PortNum;
private int door1PortNum;
private int door2PortNum;
private int microwavePortNum;
private int tvPortNum;
private int waterHeaterPortNum;
public SmartHome(String name, int serialNumber, String addressName, String addressNumber, String city, String country, String postalCode, int light1PortNum, int light2PortNum, int door1PortNum, int door2PortNum, int microwavePortNum, int tvPortNum, int waterHeaterPortNum) {
// Assigning values in the constructor call
}
// Getters and Setters
}
We face too many constructor arguments, and with low type-variety, we'll be looking at a huge constructor call with no way of knowing what's what.
Also please note that two constructors with the same parameter type, but with different variable names, are not accepted in Java.
Having these two constructors is not allowed in Java since the compiler can't tell them apart:
public SmartHome(int door1PortNum) { ... }
public SmartHome(int door2PortNum) { ... }
Even if we have one constructor with parameter type int
:
public SmartHome(int portNum) { ... }
We know that we have to set a port number, but we won't know if that number is the port for the door, light, microwave, TV, or water heater.
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
This class quickly becomes unusable in a team environment. Even if you're a one-man-show, good luck remembering the order of the parameters after a week of not instantiating the class.
This is where the Builder Pattern jumps in:
The Builder Pattern separates the construction from the representation.
What does this mean?
The construction is done in the class itself. The representation is what we see as the user of the class. Right now, both of our classes above have these two tied together - we directly call the constructor with the passed arguments.
By separating these two, we can make the representation of the class a lot simpler, neater and readable, while the constructor does its part.
Implementation
There are a few steps to take in order to implement the Builder Pattern. Continuing with our previous examples, we'll use the SmartHome
class to show these steps:
- A
static
builder class should be nested in ourSmartHome
class - The
SmartHome
constructor should beprivate
so the end-user can't call it - The builder class should have an intuitive name, like
SmartHomeBuilder
- The
SmartHomeBuilder
class will have the same fields as theSmartHome
class - The fields in the
SmartHome
class can befinal
or not, depending if you want it to be immutable or not - The
SmartHomeBuilder
class will contain methods that set the values, similar to setter methods. These methods will feature theSmartHomeBuilder
as the return type, assign the passed values to the fields of the static builder class and follow the builder naming convention. They'll typically start withwith
,in
,at
, etc. instead ofset
. - The static builder class will contain a
build()
method that injects these values intoSmartHome
and returns an instance of it.
With that being said, let's implement the Builder Pattern in our example class:
public class SmartHome {
// Fields omitted for brevity
// The same fields should be in `SmartHome` and `SmartHomeBuilder`
// Private constructor means we can't instantiate it
// by simply calling `new SmartHome()`
private SmartHome() {}
public static class SmartHomeBuilder {
private String name;
private int serialNumber;
private String addressName;
private String addressNumber;
private String city;
private String country;
private String postalCode;
private int light1PortNum;
private int light2PortNum;
private int door1PortNum;
private int door2PortNum;
private int microwavePortNum;
private int tvPortNum;
private int waterHeaterPortNum;
public SmartHomeBuilder withName(String name) {
this.name = name;
return this;
}
public SmartHomeBuilder withSerialNumber(int serialNumber) {
this.serialNumber = serialNumber;
return this;
}
public SmartHomeBuilder withAddressName(String addressName) {
this.addressName = addressName;
return this;
}
public SmartHomeBuilder inCity(String city) {
this.city = city;
return this;
}
public SmartHomeBuilder inCountry(String country) {
this.country = country;
return this;
}
// The rest of the methods are omitted for brevity
// All follow the same principle
public SmartHome build() {
SmartHome smartHome = new SmartHome();
smartHome.name = this.name;
smartHome.serialNumber = this.serialNumber;
smartHome.addressName = this.addressName;
smartHome.city = this.city;
smartHome.country = this.country;
smartHome.postalCode = this.postalCode;
smartHome.light1PortNum = this.light1PortNum;
smartHome.light2PortNum = this.light2PortNum;
smartHome.door1PortNum = this.door1PortNum;
smartHome.door2PortNum = this.door2PortNum;
smartHome.microwavePortNum = this.microwavePortNum;
smartHome.tvPortNum = this.tvPortNum;
smartHome.waterHeaterPortNum = this.waterHeaterPortNum;
return smartHome;
}
}
}
The SmartHome
class doesn't have public constructors and the only way to create a SmartHome
object is through the SmartHomeBuilder
class, like this:
SmartHome smartHomeSystem = new SmartHome
.SmartHomeBuilder()
.withName("RaspberrySmartHomeSystem")
.withSerialNumber(3627)
.withAddressName("Main Street")
.withAddressNumber("14a")
.inCity("Kumanovo")
.inCountry("Macedonia")
.withPostalCode("1300")
.withDoor1PortNum(342)
.withDoor2PortNum(343)
.withLight1PortNum(211)
.withLight2PortNum(212)
.withMicrowavePortNum(11)
.withTvPortNum(12)
.withWaterHeaterPortNum(13)
.build();
System.out.println(smartHomeSystem);
While we have made the class itself more complicated by including a nested class with duplicate fields - the representation is separated from the creation.
It's evident what we're constructing when instantiating the object. It's readable, understandable, and anyone can use your classes to build objects.
Going back to the real-world neural network example, it would look a little something like this:
MultiLayerNetwork conf = new NeuralNetConfiguration.Builder()
.seed(rngSeed)
.optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT)
.updater(new Adam())
.l2(1e-4)
.list()
.layer(new DenseLayer.Builder()
.nIn(numRows * numColumns) // Number of input datapoints.
.nOut(1000) // Number of output datapoints.
.activation(Activation.RELU) // Activation function.
.weightInit(WeightInit.XAVIER) // Weight initialization.
.build())
.layer(new OutputLayer.Builder(LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD)
.nIn(1000)
.nOut(outputNum)
.activation(Activation.SOFTMAX)
.weightInit(WeightInit.XAVIER)
.build())
.pretrain(false).backprop(true)
.build()
Pros and Cons
Besides the most obvious point of using the Builder Pattern, there are a couple of other pros that might not be too obvious at first glance:
- You can change the implementation of the object any way you'd like, and simply update the methods. The end-user is faced with an abstract interface through the static builder class and doesn't concern themselves with the underlying implementation.
- It supports encapsulation by decoupling the representation of the object from the construction.
The only real disadvantage is that it increases the amount of code in the domain models. They're typically long already, though they're relatively simple (fields, getters, and setters). Though, you'd rarely tamper with these classes anyway.
Another disadvantage is that it's more difficult to construct these objects from external configurations, like a static config file. It takes more logic and overhead to implement this than it would to just pass regular parameters to a class instantiation.
In general, the pros typically outweigh the cons when it comes to the Builder Pattern, which is the reason it's generally employed in many, especially complex applications, frameworks, and libraries.
Conclusion
Design Patterns are simply sets of standardized practices used in the software development industry. They represent solutions, provided by the community, to common problems faced in every-day tasks regarding software development.
In this article, we've dove into a key creational design pattern that takes care of object construction and allows developers to build complex objects with far less human-induced errors and improves maintainability and scalability.
The Builder Design pattern offers several pros over simply instantiating classes via constructors, with a con that doesn't really compare to the amount of benefits you can get from employing it.