designPattern

Design Pattern(10) - Builder

一個constructor參數很多的class

今天我們有一個class Employee 他有很多attribute

public class Employee{
    String name;
    String job;
    String birthday;
    int age;
    int salary;
    String address;
    String phone;
    String email;
    public Employee(String name, String job, String birthday, int age, int salary, String address, String phone, String email){
    	this -> name = name;
    	this -> job = job;
    	this -> birthday = birthday;
    	this -> age = age;
    	this -> salary = salary;
    	this -> address = address;
    	this -> phone = phone;
    	this -> email = email;
    }
}

Alt text

即使你覺得這麼長的constructor沒有問題 可是在創一個物件的時候 有些attribute是必須的 有些是不一定要有的(比如說age跟birthday跟address不一定要提供) 那怎麼辦呢?

寫過C的都知道 很簡單的default parameter搞定

public Employee(String name, String job, String birthday = "N/A", int age = 0, int salary, String address = "N/A", String phone, String email){
    	this -> name = name;
    	this -> job = job;
    	this -> birthday = birthday;
    	this -> age = age;
    	this -> salary = salary;
    	this -> address = address;
    	this -> phone = phone;
    	this -> email = email;
    }
}

輕鬆 所有optional的值我都先寫好 只需要一個constructor

但對不起 java有java的玩法 java沒有default parameter

那怎麼辦呢 不好意思 你必須overload你的constructor

題外話

上面那個C的程式不能那樣寫 所有的optional的參數應該擺在參數的最後面 不可以想擺哪就擺哪

理由是如果今天你擺中間

int add(int x = 5, int y){
    return x + y;
}

add(7)//?????

Compiler不知道你要assign 7給誰

回到overload

所以你只能這樣 Alt text

所有的constructor都要call最長的那個constructor

還必須得照重要性排 你無法創一個只有address但沒有birthday跟age的Employee

(你不能多一個只有address沒有birthday和age的constructor 因為他跟只有birthday沒有address和age的constructor有一樣的signature)

福無雙至 禍不單行 缺點還沒說完 即使是上面的實作 還是有缺點 今天如果想改address的default value變成”Not Applicable” 你必須改所有constructor的code

要怎麼解決constructor code重複的問題呢 給你一分鐘






Alt text













解答叫做 telescoping constructor

5個參數的constructor 多增加一個參數 call 6個參數的constructor 6個參數的constructor 多增加一個參數 call 7個參數的constructor 7個參數的constructor 多增加一個參數 call 8個參數的constructor

Alt text 是否覺得有點神似 Alt text

這樣子你某個optional的argument如果要改 就只要改一個地方

回到正題

這實在醜 而且client每次要call都要看一下到底參數順序要怎麼給 而且重要性的問題也還沒解答 你無法創一個只有address但沒有birthday跟age的Employee

黑暗盡頭 主角登場

我們在Employee裡面加一個inner class, Builder

public class Employee{
    private String name;
    private String job;
    private String birthday;
    private int age;
    private int salary;
    private String address;
    private String phone;
    private String email;
   
    public static class Builder{
	private String name;
	private String job;
    	private int salary;
    	private String phone;
    	private String email;
	private String birthday = "N/A";
    	private int age = 0;
    	private String address = "N/A";
	public Builder(){}
	public Builder setName(String name){
	    this -> name = name;
	    return this;
	}
	public Builder setJob(String job){
	    this -> job = job;
	    return this;
	}
	public Builder setSalary(int salary){
	    this -> salary = salary;
	    return this;
	}
	public Builder setPhone(String phone){
	    this -> phone = phone;
	    return this;
	}
	public Builder setEmail(String email){
	    this -> email = email;
	    return this;
	}
	public Builder setBirthday(String birthday){
	    this -> birthday = birthday;
	    return this;
	}
	public Builder setAge(int age){
	    this -> age = age;
	    return this;
	}
	public Builder setAddress(String address){
	    this -> address = address;
	    return this;
	}
	public Employee build(){
	    return new Employee(this);
	} 

    }
    private Employee(Builder builder){
    	this -> name = builder.name;
    	this -> job = builder.job;
    	this -> salary = builder.salary;
    	this -> phone = builder.phone;
    	this -> email = builder.email;
    	this -> birthday = builder.birthday;
    	this -> age = builder.age;
    	this -> address = builder.address;
    }
}

client怎麼建一個Employee?

Employee e = new Employee.Builder().setName("Alex").setJob("Engineer").setSalary(100).setPhone("123").setEmail("a@b").setAddress("ABC").build();

對client來說code非常好懂 也解決了之前的問題 我們可以只給address不用給birthday跟age

除此之外 還有什麼好處呢?

  1. 對於不合法的輸入及早發現及早治療: 你可以在每一個setter都做好error handling 之前的例子可能你constructor都跑一半了才發現user給你髒東西 這裡的話setter直接噴錯就可以 不用等到build()被call

  2. 如果你要給的某個參數是array of something 你需要用到varargs 那你的constructor就無法支援其中兩個參數是array of something的情況 不過這點其實用List也可以解決

  3. 如果參數的順序有強制的話 比如給參數A之前 參數B必須已經有了的情況 constructor做不到 可是builder可以輕易做到

問問題時間

Q: 之前說好分離一個物件的創造和使用 factory就是另一個class專門負責創造 為什麼builder需要是inner class?

A: 因為constructor是private 我們不想讓其他人去call這個constructor(Builder應該要是唯一可以call constructor的人) 所以Builder是static inner class

Q: 為什麼不能就讓setXXX當成Employee的member function 還特別需要一個Builder? 這樣的話我呼叫完constructor完之後再慢慢set不是也一樣效果嗎?

Employee e = new Employee() .setName("Alex").setJob("Engineer").setSalary(100).setPhone("123").setEmail("a@b").setAddress("ABC");

A: 因為可能會有一些invalid state 或是參數之間有dependency的話 這個做法就不好 另一個缺點是這樣我們無法建造一個無法被改變狀態的class 因為setXXX function都必須要是public

以上

以上是effective java中Chap2 Item2裡面對於builder的介紹 如果你是第一次看builder 那你可以不需要往下看 我認為effective java已經把使用時機跟優缺點都講得非常清楚 你已經知道怎麼用Builder跟什麼時機用Builder 我認為已經很夠了

但如果你是看了Gof以後覺得Builer跟Factory沒有什麼區別 想真的分得很清楚 那麼你可以往下看下去 因為我第一次讀builder的時候也有一樣的問題

Prerequisites

  1. 很不幸的 你很想了解Gof裡面的結構部分 想知道到底Director是什麼

  2. 你已經知道並了解什麼是工廠模式抽象工廠

  3. 你有十分鐘以上的連續時間 只看到一半的話很有可能你會跟工廠模式混淆

以上條件都達成的話歡迎往下繼續看 否則走火入魔後果自行負責 Alt text

先來看看GOF對於Builder的定義

建造者模式讓你可以一步步創建一個複雜對象 所以可以用同一套程序生成屬性不同的物件

結構

Alt text

麥當勞

讀Design Pattern不實作就如紙上談兵 我們照著結構圖 一個一個實作

Product就是Meal 一個麥當勞餐包含了主食 副食和飲料

public class Meal{
    public Food food;
    public Drink drink;
    public Sides sides;
    public Meal setFood(Food f){
	this.food = f;
	return this;
    }
    public Meal setDrink(Drink d){
	this.drink = d;
	return this;
    }
    public Meal setSides(Sides s){
	this.sides = s;
	return this;
    }
    public showMeal(){
	//隨便你要怎麼show
    }
} 

記得先建好抽象的Builder

public abstract class MealBuilder{
    public abstract Food buildFood();
    public abstract Drink builDrink();
    public abstract Sides buildSides();
} 

我們的ConcreteBuilder是大麥克建造者

public class BigMacBuilder extends MealBuilder{
    public Food buildFood(){
	return new BigMac();
    }
    public Drink builDrink(){
	return new Coke();
    }
    public Sides buildSides(){
	return new Fries();
    }  
} 

Director是我們的Waiter

Waiter的constructor吃一個builder 你叫我build的時候我就乖乖的去拿大麥克套餐的主食 大麥克套餐的副食 大麥克套餐的飲料

public class Waiter{
    private MealBuilder builder;
    public Waiter(MealBuilder builder){
	this.builder = builder;
    }
    public Meal build(){
	Meal m = new Meal();
	m.setFood(builder.buildFood())
	    .setDrink(builder.builDrink())
	    .setSides(builder.buildSides());
	return m;
    }
}

Client的用法也相當直觀

    BigMacBuilder bigMacBuilder = new BigMacBuilder();
    Waiter waiter = new Waiter(bigMacBuilder);
    Meal returnedMeal = waiter.build();

很好 我們照著教科書一步步實做起來 你有興趣的話我把code放在我的github上 你可以直接在IDE上跑起來

但你有沒有發現 其實我們根本不需要這個Waiter

Waiter做的事就只是照著他的member variable: builder裡面的東西一個個把東西建出來 那這件事其實ConcreteBuilder自己就做得到

要做的修改就是把build()移到ConcreteBuilder

public class BigMacBuilder extends MealBuilder{
    public Food buildFood(){
        return new BigMac();
    }   
    public Drink builDrink(){
        return new Coke();
    }
    public Sides buildSides(){
        return new Fries();
    }
    public Meal build(){
      Meal m = new Meal();
      m.setFood(buildFood())
          .setDrink(builDrink())
          .setSides(buildSides());
      return m;
    }
} 

搞定 client用法變這樣

    BigMacBuilder bigMacBuilder = new BigMacBuilder();
    Meal returnedMeal = bigMacBuilder.build();

有沒有覺得這個好像似曾相似 你可以看仔細一點

Alt text

想不起來嗎 你可以再看仔細一點 Alt text

Alt text

恭喜你 原來Builder根本就是抽象工廠的加工

BigMac, Fries, Coke就屬於同一個產品族

每一個不同的Concrete Builder就是不同的具體工廠(BigMacBuilder, McNuggetBuilder, CheeseburgerBuilder)

MealBuilder就是抽象工廠

原來是個用菜頭排骨酥做成的味增湯啊

融會貫通

那這兩種模式的差別又在哪裡 為什麼會這麼像呢? 再給你一圈






Alt text













這兩個模式最主要的癥結是ConcreteBuilder的數量 如果ConcreteBuilder數量多於一 那根本就是抽象工廠

因為超過一個Builder代表說Product的某部分不一樣(比如說大麥克餐跟麥克雞塊餐的主食不一樣) 那就需要用到抽象工廠產品族的概念

可是這樣就失去了Builder的本意

我認為Builder就是應該要在build之前把需要的東西都設定好 然後再開始build

所以剛剛麥當勞的例子根本就可以用唯一的Builer去setFood, setDrink等等 然後call build把餐點生出來

既然ConcreteBuilder只需要一個 我們可以省略抽象的Builder 只需要一個Builder class 當然沒用的Director也可以完全不要

最後整個結構圖簡化成這樣

Alt text

這就是我們文章前半段在講的東西 繞了一圈 原來之前講的就很夠了啊

難得糊塗 荒唐荒唐 醉過醉過

如果天黑之前來得及

我要忘了你的眼睛

窮極一生

做不完一場夢

大夢初醒荒唐了一生

                                                             南山南




Alt text

雖然到頭來多走了一圈冤枉路

Effective Java的builder講的就已經很夠了

但回頭來看 還是不枉瀟灑走一回 痛快

原來

抽象工廠就是參數寫死的Builder

Builder就是個更有彈性的constructor

使用時機就是constructor參數很多 或是sub物件的生成順序有dependency