อ่าน Effective Java ตอนที่ 2 (Builder)

ก็อย่างที่เกลิ่นไปในบทความแรก Effective Java ตอนที่ 1 (Factory Method) ก็เลยอ่านไปเรื่อยๆ เขียนไปยาวๆกับหนังสือเล่มนี้ครับ ซึ่งรอบนี้มากับเรื่องที่ 2 การใช้ Builder กับ การเขียน constructor แบบหลายตัวแปร

Static Factory Method กับ Constructor นั้นมีหลายอย่างที่มีข้อจำกัดด้วยกันทั้งคู่ มันไม่สามารถขยับขยายได้ดี ถ้าเกิดมาหลายตัวแปรที่อาจจะใส่ก็ได้ ไม่ใส่ก็ได้ หรือตัวแปรเยอะ ถ้าเอาง่ายๆ เราเขียนแบบปกติ ต้องทำยังไง

// Telescoping constructor pattern - does not scale well!
public class NutritionFacts {
 private final int servingSize; // (mL) required
 private final int servings; // (per container) required
 private final int calories; // optional
 private final int fat; // (g) optional
 private final int sodium; // (mg) optional
 private final int carbohydrate; // (g) optional
 
 public NutritionFacts(int servingSize, int servings) {
  this(servingSize, servings, 0);
 }
 public NutritionFacts(int servingSize, int servings, int calories) {
  this(servingSize, servings, calories, 0);
 }

 public NutritionFacts(int servingSize, int servings, 
   int calories, int fat) {
  this(servingSize, servings, calories, fat, 0);
 }
 public NutritionFacts(int servingSize, int servings,
   int calories, int fat, int sodium) {
  this(servingSize, servings, calories, fat, sodium, 0);
 }

 public NutritionFacts(int servingSize, int servings, 
   int calories, int fat, int sodium, int carbohydrate) {
  this.servingSize = servingSize;
  this.servings = servings;
  this.calories = calories;
  this.fat = fat;
  this.sodium = sodium;
  this.carbohydrate = carbohydrate;
 }
}

ซึ่งบอกเลยว่าอู้วหูววววว ตายๆ จะเขียนแต่ละที แล้วสมมุติว่าเราจะใช้ก็ต้องเขียนเป็น

NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);

แล้วถ้าเราเพิ่มตัวแปรอีก จบเลย GG เขียนเพิ่มกระจุย เราเลยบอกได้เลยว่าวิธีนี้ไม่ดีแน่ๆ

แต่ถ้าเกิดเราไปใช้ JavaBeans Pattern หล่ะ (คือการสร้าง constructor เปล่าๆ แล้วใช้ setter เซ็ตค่าเอา) เออก็ดีนะ ง่ายดี อ่านเข้าใจด้วย เช่น

NutritionFacts cocaCola = new NutritionFacts(); 
cocaCola.setServingSize(240); 
cocaCola.setServings(8); 
cocaCola.setCalories(100); 
cocaCola.setSodium(35); 
cocaCola.setCarbohydrate(27);

แต่ข้อเสียของมันก็คือ การสร้าง instance ไม่ได้ทำพร้อมกับการเซ็ตค่า! ซึ่งมีโอกาสทำให้เกิดการ data inconsistent ได้ (โดยเฉพาะเมื่อทำ multi-thread) กล่าวคือ ไม่ thread-safe นั่นเอง

ซึ่งโชคดีที่เรามีอีกหนึ่งทางเลือก ที่รวมทั้งสองอย่าง ระหว่าง Telescoping และ JavaBean Pattern เข้าด้วยกัน นั่นคือ Builder Pattern โดยแทนที่จะสร้าง Object ตรงๆ เราก็ไปสร้างจาก Builder Object แทน แล้วเราก็ใส่ค่าเข้าไปคล้ายๆกับ setter แล้วก็ Build ทีเดียว เสร็จปิ๊ง…

ตัวอย่าง Builder Pattern code

// Builder Pattern
public class NutritionFacts {
 private final int servingSize;
 private final int servings;
 private final int calories;
 private final int fat;
 private final int sodium;
 private final int carbohydrate;
 
 public static class Builder {
  // Required parameters
  private final int servingSize;
  private final int servings;
  // Optional parameters - initialized to default values
  private int calories = 0;
  private int fat = 0;
  private int carbohydrate = 0;
  private int sodium = 0;
  
  public Builder(int servingSize, int servings) {
   this.servingSize = servingSize;
   this.servings = servings;
  }
  public Builder calories(int val){ 
   calories = val; return this; 
  }
  public Builder fat(int val){
   fat = val; return this; 
  }
  public Builder carbohydrate(int val){
   carbohydrate = val; return this; 
  }
  public Builder sodium(int val){ 
   sodium = val; return this; 
  }
  public NutritionFacts build() {
   return new NutritionFacts(this);
  }
 }
 private NutritionFacts(Builder builder) {
  servingSize = builder.servingSize;
  servings = builder.servings;
  calories = builder.calories;
  fat = builder.fat;
  sodium = builder.sodium;
  carbohydrate = builder.carbohydrate;
 }
}

ซึ่งเวลาจะใช้ก็ง่ายๆ

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
        .calories(100)
        .sodium(35)
        .carbohydrate(27)
        .build();

ซึ่งทำให้คนที่มาใช้ code นี้ใช้ก็ง่าย อ่านก็ง่าย เป็นมาจากการที่ Builder Pattern ใช้ชื่อ method มาจาก parameter ต่างๆ (ในกรณีที่เกิดความผิดพลาดจากตัวแปรต่างๆตอน build ก็ควรจะ throw IllegalStateExeption ด้วยนะ แต่ถ้ามันพลาดตั้งแต่ตอน set เข้าไปก็ให้ throw IllegalArgumentException แทน)

Builder Method นั้นมีความคล่องตัวอย่างมาก Builder ตัวเดียวสามารถสร้าง object ได้หลายตัว builder object สามารถใส่ค่าได้ และสร้างได้เรื่อยๆ เช่น object แรก ไม่มี calories แต่ object ที่สองทุกอย่างเหมือนกันกับ object แรก หลังจาก build object แรกแล้ว แล้วก็เอา builder ตัวเดิมมาเพิ่มตัวแปรแล้วก็ build ใหม่ให้ได้ object ที่สองอีกที

Builder Pattern มีข้อเสียอยู่เหมือนกัน เพราะถ้าเราจะสร้าง Object เมื่อไหร่แล้วหล่ะก็ เราต้องสร้าง Builder ก่อนเสมอทำให้เปลือง (ซึ่งจริงๆแล้วในการใช้งานจริงๆอาจจะไม่ค่อยพบอะไนแบบนี้หรอก) แต่ถ้ามันมีการใช้งานที่หนักหน่วง หรือใช้ resource มากๆแล้วหล่ะก็ Builder Pattern เนี่ยแหละใช้ทรัพยากรฟุ่มเฟือยกว่าตัวอื่นๆมาก เพราะฉะนั้น เราควรใช้ Builder Pattern ก็ต่อเมื่อมีตัวแปรประมาณ 4 ตัวหรือมากกว่านั้น ถ้าน้อยกว่านั้นก็ใช้ constructor หรือ static factory ก็ได้ แต่อย่าลืมไวว่าถ้าเกิดเราคิดว่าในอนาคตมีการเพิ่มตัวแปรแน่นอน ถ้าเราเริ่มด้วย constructor หรือ static factory แล้ว เราจะมาเปลี่ยนเป็น builder อีก มันจะรับมือยากมากๆ ตัว constructor และ static factory จะคงอยู่กับเราไปอีกนานเท่านาน มันเลยจะดีกว่าถ้าเราเริ่มจาก Builder Pattern ตั้งแต่แรก

สรุปว่า Builder Pattern นั้นเป็นตัวเลือกที่ดีถ้าเราสร้าง class ที่จะใช้ constructor หรือ static factory แล้วมีตัวแปรเยอะมากๆ ทั้งตัวแปรที่จำเป็นต้องใส่ และ ตัวแปรที่อาจจะไม่ใส่ก็ได้

Comments