官术网_书友最值得收藏!

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.

主站蜘蛛池模板: 丰宁| 葵青区| 南平市| 达尔| 库伦旗| 保山市| 南汇区| 邓州市| 彭州市| 吉首市| 台北县| 台东县| 开江县| 定西市| 普定县| 都江堰市| 甘南县| 宁阳县| 绿春县| 平利县| 信阳市| 南安市| 敦化市| 合阳县| 勐海县| 永州市| 安阳县| 无为县| 泽普县| 绵竹市| 招远市| 海宁市| 深泽县| 尼玛县| 常熟市| 屯昌县| 成安县| 抚州市| 曲周县| 廉江市| 秦皇岛市|