Simple programs do not require an extensive design process because they focus on limited use of the solution, using only a few classes. Large programs focus on an extensive design that leverages re-usability more than any other properties of a good design paradigm. The grand idea is not only providing a solution to the present problem, but also to create a design that forms a building block for future changes. Complex programs require thousands of lines of code with numerous interactions between objects and users. These types of solutions are commonly found in Air Traffic Control Systems and Banking Systems which are operated with thousands of Teller Machines. Here, we explore a design pattern called the Builder Pattern and implement it with Java code examples.
Overview
Effective software designs not only cater to present requirements but also form the basis for future changes and development. This is easier said than done under practical application. But, design patterns definitely lessen the burden of code design to a great extent. Patterns are the proven architecture for constructing flexible and maintainable software. It substantially reduces complexity of code with a standard set of norms and practices.
There are numerous design patterns available and developers can pick one according to the best expression of the code flow. It is almost impossible to not pick one that fits your need. In fact, design patterns are a proof that someone has already encountered the problem and designed a best practice to obtain the solution. However, they are not providence in any way; a better idea can always replace them. A rebellion under peaceful circumstance is suicidal. Although one can rebel out of the situation and do one’s own thing, it is helpful to follow some sort of design patterns under most circumstances.
The Builder Pattern
Design patterns are named according to their characteristics. For example, the builder pattern lays out the norms of building a class structure. It is particularly helpful in instantiating a class in object-oriented programming. The idea is to decouple the construction of complex objects from its representation. It leverages flexibility in designing objects in a language like Java. The convenience of this design pattern is easily felt as we start to code.
Using the Builder Pattern
This pattern is particularly useful to create an instance of a class that has many fields or properties. As should be obvious, a constructor is quite cumbersome in such a case. For example, in a class like this:
public class Person { private final long id; private final String firstName; private final String middleName; //optional private final String lastName; //optional private final Date birthDate; private final String phone; private final String email; private final String street; //optional private final String city; //optional private final String province; private final String zip; // ...
To create an instance of this class, we can:
- Use a single constructor to initialize the fields with values
- Use multiple constructors
- Use setter methods after instantiating an object with a no-argument constructor
Although these are all syntactically valid techniques, they are quite cumbersome in practice. As the number of fields increases, it soon becomes hard to manage and understand. Using a single constructor is a bad idea, firstly because it is a bad design to initialize a number of fields with a huge parameterized constructor. Secondly, there is a little choice to do away with the optional fields. Using multiple constructors is not a good idea because, if the number of fields increases in the future, it soon becomes unmanageable.
A third approach would be not to use any constructor at all, instead removing the final modifier from the fields and using setter methods to initialize. The problem with the technique is that we can create invalid objects of this class by using the setter method. For example, the following is a semantically invalid instance of the class although it is syntactically valid.
Person person = new Person(); person.setCity("Mumbai");
Note that the definition of a person object is not only the valid initialization of the city field but the correct initialization of at least the non-optional fields. This is the real problem with setter method initialization.
Implementing a Builder Pattern
We can use a builder patter to overcome all the problems discussed above. Here, in this technique we create a companion object called a builder. This companion object is used to construct a legal domain object. This not only enhances code clarity but also make it simple to construct.
public class Person { public static class Builder { private long id; private String firstName; private String middleName; //optional private String lastName; //optional private Date birthDate; private String phone; private String email; private String street; //optional private String city; //optional private String province; private String zip; public Builder id(final long id) { this.id = id; return this; } public Builder firstName(final String firstName) { this.firstName = firstName; return this; } public Builder middleName(final String middleName) { this.middleName = middleName; return this; } public Builder lastName(final String lastName) { this.lastName = lastName; return this; } public Builder birthDate(final Date birthDate) { this.birthDate = birthDate; return this; } public Builder phone(final String phone) { this.phone = phone; return this; } public Builder email(final String email) { this.email = email; return this; } public Builder street(final String street) { this.street = street; return this; } public Builder city(final String city) { this.city = city; return this; } public Builder province(final String province) { this.province = province; return this; } public Builder zip(final String zip) { this.zip = zip; return this; } public Person build(){ if(id <= 0 || firstName.isEmpty() || birthDate == null || phone.isEmpty() || email.isEmpty() || province.isEmpty() || zip.isEmpty()){ throw new IllegalStateException("Cannot create Person object."); } return new Person(id,firstName,middleName,lastName, birthDate,phone,email,street,city,province,zip); } } private final long id; private final String firstName; private final String middleName; //optional private final String lastName; //optional private final Date birthDate; private final String phone; private final String email; private final String street; //optional private final String city; //optional private final String province; private final String zip; private Person(final long id, final String firstName, final String middleName, final String lastName, final Date birthDate, final String phone, final String email, final String street, final String city, final String province, final String zip) { this.id = id; this.firstName = firstName; this.middleName = middleName; this.lastName = lastName; this.birthDate = birthDate; this.phone = phone; this.email = email; this.street = street; this.city = city; this.province = province; this.zip = zip; } }
The Builder class is a part of the Person class and is used to construct Person objects. With a constructor, parameters are ordered in a specific manner. As a result, they are passed in the same order. With a builder pattern, the ordering does not matter and the values can be passed in any order during construction. Note that the constructor is made private in this case.
@Test public void rightBuild() { final Person.Builder builder = new Person.Builder(); final Person emp = builder .id(101) .firstName("Percy") .middleName("Bysshe") .lastName("Shelley") .birthDate(new GregorianCalendar(1792, Calendar.AUGUST,4).getTime()) .phone("1234567890") .email("pbs@gmail.com") .street("123 somewhere") .province("someplace") .zip("10293847").build(); } @Test(expected = IllegalStateException.class) public void wrongBuild() { final Person.Builder builder = new Person.Builder(); final Person emp = builder .middleName("Bysshe") .lastName("Shelley") .phone("1234567890") .zip("10293847").build(); }
Observe in the test methods how we have created the objects with a call to the builder methods and a chain of method calls. Finally, the build() method is called to end the chain and finalize creation of the object. This is how we implement the builder pattern in Java code.
Conclusion
The essence is to understand the principle behind the builder patter and implement it in your own way. However, the pattern almost remains the same in all circumstances. As specified, the builder patter is particularly useful where one has to initialize a large set of fields in the class. Every class is not suitable to use this pattern. As can be seen, the lines of code increase for the sake of convenience. Use it intelligently and with discretion.