Builders
We need to model used cars for a resell shop. We want to rate a used car by kilometers driven, year of manufacture, make, model, accessories installed such as GPS, Air Conditioning (AC), and safety features such as air bags and anti-lock brakes (ABS). The list goes on and on.
Some of these attributes are mandatory and others are optional. For example, all cars will have the year of manufacture, kilometers driven, make and model. A car may not necessarily have GPS, AC, airbags or ABS.
We also need to validate the arguments. The kilometers are not negative, year of manufacture is something sensible (for example, not 1425 A.D.), and the make and model should match—make as Maruti and model as Corolla should be flagged as an error.
To handle all these requirements, we use the Builder pattern. Here is the UML diagram and then the code follows:
Figure 2.5: UML diagram for Builder
public class UsedCar { private String make; private String model; private int kmDriven; private int yearOfManufacturing; private boolean hasGps; private boolean hasAc; private boolean hasAirBags; private boolean hasAbs; // Setters/Getters not shown } public class UsedCarBuilder { private final UsedCar car; public UsedCarBuilder() { car = new UsedCar(); } public UsedCarBuilder hasAirBags(final boolean b) { car.setHasAirBags(b); return this; } public UsedCarBuilder hasAbs(final boolean b) { car.setHasAbs(b); return this; } public UsedCarBuilder hasAc(final boolean b) { car.setHasAc(b); return this; } public UsedCarBuilder hasGps(final boolean b) { car.setHasGps(b); return this; } public UsedCarBuilder yearOfManufacturing(final int year) { car.setYearOfManufacturing(year); return this; } public UsedCarBuilder kmDriven(final int km) { car.setKmDriven(km); return this; } public UsedCarBuilder model(final String itsModel) { car.setModel(itsModel); return this; } public UsedCarBuilder make(final String itsMake) { car.setMake(itsMake); return this; } public UsedCar build() { // set sensible defaults for optional attributes - gps, ac, airbags, abs // check make and model are consistent // check year of manufacturing is sensible // check kmDriven is not negative return car; } } public class Driver { public static void main(String[] args) { // Note the method chaining UsedCar car = new UsedCarBuilder().make("Maruti").model("Alto") .kmDriven(10000).yearOfManufacturing(2006).hasGps(false) .hasAc(false).hasAbs(false).hasAirBags(false).build(); System.out.println(car); } }
We design it like this for method chaining. Note the build()
method of UsedCarBuilder
. We get one place where we can check all the parameters rigorously before we return the UsedCar object
. Making sure all fields satisfy the preconditions ensures the client code is not violating the contract.
Please see: http://www.javaworld.com/article/2074956/learn-java/icontract--design-by-contract-in-java.html for more information.
Ease of object creation
Let's say instead of using the builder pattern, could we use overloaded constructors?
Java allows us to overload constructors. And to promote reuse, we can always call other constructors using this (arg1, arg2,...,argN
) syntax:
public UsedCar(String make, String model, int kmDriven, int yearOfManufacturing, boolean hasGps, boolean hasAc, boolean hasAirBags, boolean hasAbs) { super(); this.make = make; this.model = model; this.kmDriven = kmDriven; this.yearOfManufacturing = yearOfManufacturing; this.hasGps = hasGps; this.hasAc = hasAc; this.hasAirBags = hasAirBags; this.hasAbs = hasAbs; } public UsedCar(String make, String model, int kmDriven, int yearOfManufacturing, boolean hasGps, boolean hasAc, boolean hasAirBags) { this(make, model, kmDriven, yearOfManufacturing, hasGps, hasAc, hasAirBags, false); // no ABS } public UsedCar(String make, String model, int kmDriven, int yearOfManufacturing, boolean hasGps, boolean hasAc) { this(make, model, kmDriven, yearOfManufacturing, hasGps, hasAc, false); // no Air Bags, no ABS }
The overloaded constructors keep getting narrower, tapering like a telescope. The fancy term for them is telescopic constructors:
Figure 2.6: Telescopic constructors
Anytime you want to create a UserCar
object, you need to look up the list and use the right one. Now if you add some more optional parameters, the number of constructors blows up this is hard to maintain. If you wanted any combination of optional parameters, the number of constructors would exponentially increase.
One should be able to pick and choose any of the optional parameters in any order. The builder pattern makes this possible.
The other alternative is providing setters. Not that appealing as the onus is on the calling code to invoke the correct setters and to call the validate method to make sure the object state is as expected. Someone can forget calling the validate method.
On the other hand, the builder's method chaining style makes our code more readable and saves us from looking up the constructor args list by naming the arguments. It is a small language (almost)—helping us with complex object creation—called a Domain Specific Language (DSL).
Scala shines again
It is pretty easy (again) in Scala. Made easy by Scala's support for named arguments and the wonderful case classes:
object Builder extends App { case class UsedCar(make: String, // 1 model: String, kmDriven: Int, yearOfManufacturing: Int, hasGps: Boolean = false, hasAc: Boolean = false, hasAirBags: Boolean = false, hasAbs: Boolean = false) { require(yearOfManufacturing > 1970, "Incorrect year") // 2 require(checkMakeAndModel(), "Incorrect make and model") def checkMakeAndModel() = if (make == "Maruti") { model == "alto" } else if (make == "Toyota") { model == "Corolla" } else { true } } val usedMaruti = UsedCar(model = "alto", make = "Maruti", kmDriven = 10000, yearOfManufacturing = 1980, hasAbs = true, hasAirBags = true) // 3 println(usedMaruti) val usedCorolla = usedMaruti.copy(make = "Toyota", model = "Corolla") // 4 println(usedCorolla) // val wrongModel = usedCorolla.copy(model = "alto") // throws - Incorrect make and model }
The salient points for the preceding code are as follows:
- The case class creates immutable fields of the same name.
- We check the preconditions with require—require is defined in
Predef
—and we use it to check the preconditions. - Parameters can be named—so we don't need to look up the order. In fact, IDEs can autocomplete parameters for you—this is quite convenient.
- We can create another, almost identical, object with a few changes with the
copy
method.
The Predef
is an object that provides many helpful goodies. It is imported automatically. Predef
provides implicit conversion, which makes the following snippet work:
scala> "Hello World & Good Morning!".partition(x => x.isUpper || x.isLower) res0: (String, String) = (HelloWorldGoodMorning," & !")
Predef
also provides the println
method we are using.