Builder Pattern(建造者模式)屬於設計模式中Creational Pattern(創建模式)。當物件(object)的建構過程比較複雜或建構的物件有多種樣貌時,可利用Builder Pattern來設計。
Builder Pattern的定義如下,「把一個複雜物件的建構與樣貌分離,如此相同的建構過程可以產生不同樣貌的物件」。
Separate the construction of a complex object from its representation so that the same construction process can create different representations.
「物件的樣貌(object's representation)」的意思是指物件目前持有屬性的狀態。
「不同樣貌的物件(different representations)」的意思是,一個類別的屬性(成員變數)有多個,而該類別的實例為各屬性不同排列組合所建構的物件。
典型的Builder Pattern包含下列角色。
- Product:最終要被建構物件的類別。
- Builder:用來定義建構物件過程中各必要步驟(方法)的介面。
- ConcreteBuilder:實作Builder介面,實際用來建構物件的類別
- Director:負責指揮ConcreteBuilder該如何建構物件。
以下是典型的Builder Pattern的範例。
Product
類別為最終要被建構的類別,裡面有多個屬性,各屬性不同的排列組合代表各樣的物件樣貌(object's presentations)。
Product
public class Product {
private String attr1;
private String attr2;
private String attr3;
private String attr4;
private String attr5;
// getters and setters...
@Override
public String toString() {
return this.getClass().getSimpleName()
+ ":{"
+ "attr1:" + attr1 + ","
+ "attr2:" + attr2 + ","
+ "attr3:" + attr3 + ","
+ "attr4:" + attr4 + ","
+ "attr5:" + attr5
+ "}";
}
}
Builder Pattern將物件的建構過程(construction of the object)抽離成為以下的Builder
,ConcreteBuilder
及Director
。
Builder
定義建構物件的各必要步驟(方法)的介面,由ConcreteBuilder
實作。
Builder
public interface Builder {
public Builder buildAttr1();
public Builder buildAttr2();
public Builder buildAttr3();
public Product build();
}
ConcreteBuilder
實作Builder
介面,為物件的建構者。
Builder的各建造方法習慣上會返回自己,這樣的設計方式又稱為Fluent interface(流暢介面)。
ConcreteBuilder
public class ConcreteBuilder implements Builder {
private Product product;
public ConcreteBuilder() {
product = new Product();
}
@Override
public Builder buildAttr1() {
String attr1 = "A"; // 一些複雜的邏輯
product.setAttr1(attr1);
return this;
}
@Override
public Builder buildAttr2() {
String attr2 = "B"; // 一些複雜的邏輯
product.setAttr2(attr2);
return this;
}
@Override
public Builder buildAttr3() {
String attr3 = "C"; // 一些複雜的邏輯
product.setAttr3(attr3);
return this;
}
@Override
public Product build() {
System.out.println("build product...");
return product;
}
}
Director
負責決定Builder
建構步驟的順序。
Director
public class Director {
private Builder builder;
public Director(Builder builder) {
this.builder = builder;
}
public Product construct() {
return builder.buildAttr1()
.buildAttr2()
.buildAttr3()
.build();
}
}
在main()
方法,也就是client中使用以上定義的Builder Pattern的各角色來產生物件。
Main
public class Main {
public static void main(String[] args) {
Builder concreteBuilder = new ConcreteBuilder();
Director director = new Director(concreteBuilder);
Product product = director.construct();
System.out.println(product);
}
}
執行後印出以下結果
build product...
Product:{attr1:A,attr2:B,attr3:C,attr4:null,attr5:null}
以類別圖來表示Build Pattern各角色間的關係。
由於Builder
,ConcreteBuilder
,Director
都僅是用於建構Product
的類別,所以通常會直接以內部類別的形式定義在Product
類別下。
Product
public class Product {
private Product() {}
private String attr1;
private String attr2;
private String attr3;
private String attr4;
private String attr5;
// getters and setters...
public static Builder getConcreteBuilder() {
return new ConcreteBuilder();
}
public static Director getDirector(Builder builder) {
return new Director(builder);
}
public interface Builder {
public Builder buildAttr1();
public Builder buildAttr2();
public Builder buildAttr3();
public Product build();
}
private static final class ConcreteBuilder implements Builder {
private Product product;
public ConcreteBuilder() {
product = new Product();
}
@Override
public Builder buildAttr1() {
String attr1 = "A";
product.setAttr1(attr1);
return this;
}
@Override
public Builder buildAttr2() {
String attr2 = "B";
product.setAttr2(attr2);
return this;
}
@Override
public Builder buildAttr3() {
String attr3 = "C";
product.setAttr3(attr3);
return this;
}
@Override
public Product build() {
System.out.println("build product...");
return product;
}
}
public static final class Director {
private Builder builder;
public Director(Builder builder) {
this.builder = builder;
}
public Product construct() {
return builder.buildAttr1()
.buildAttr2()
.buildAttr3()
.build();
}
}
@Override
public String toString() {
return this.getClass().getSimpleName()
+ ":{"
+ "attr1:" + attr1 + ","
+ "attr2:" + attr2 + ","
+ "attr3:" + attr3 + ","
+ "attr4:" + attr4 + ","
+ "attr5:" + attr5
+ "}";
}
}
Main
public class Main {
public static void main(String[] args) {
Product.Builder concreteBuilder = Product.getConcreteBuilder();
Product.Director director = Product.getDirector(concreteBuilder);
Product product = director.construct();
System.out.println(product);
}
}
如果建構時的參數來自外部,可修改如下。
Product
public class Product {
private Product() {}
private String attr1;
private String attr2;
private String attr3;
private String attr4;
private String attr5;
// getters and setters...
private Product(Builder builder) {
this.attr1 = builder.getAttr1();
this.attr2 = builder.getAttr2();
this.attr3 = builder.getAttr3();
}
public static Builder getConcreteBuilder() {
return new ConcreteBuilder();
}
public static Director getDirector(Builder builder) {
return new Director(builder);
}
public interface Builder {
public Builder setAttr1(String attr1);
public Builder setAttr2(String attr2);
public Builder setAttr3(String attr3);
public String getAttr1();
public String getAttr2();
public String getAttr3();
public Product build();
}
private static final class ConcreteBuilder implements Builder {
private String attr1;
private String attr2;
private String attr3;
public ConcreteBuilder() {
}
@Override
public Builder setAttr1(String attr1) {
this.attr1 = attr1;
return this;
}
@Override
public Builder setAttr2(String attr2) {
this.attr2 = attr2;
return this;
}
@Override
public Builder setAttr3(String attr3) {
this.attr3 = attr3;
return this;
}
@Override
public String getAttr1() {
return attr1;
}
@Override
public String getAttr2() {
return attr2;
}
@Override
public String getAttr3() {
return attr3;
}
@Override
public Product build() {
System.out.println("build product...");
return new Product(this);
}
}
public static final class Director {
private Builder builder;
public Director(Builder builder) {
this.builder = builder;
}
public Product construct(String attr1, String attr2, String attr3) {
return builder.setAttr1(attr1)
.setAttr2(attr2)
.setAttr3(attr3)
.build();
}
}
@Override
public String toString() {
return this.getClass().getSimpleName()
+ ":{"
+ "attr1:" + attr1 + ","
+ "attr2:" + attr2 + ","
+ "attr3:" + attr3 + ","
+ "attr4:" + attr4 + ","
+ "attr5:" + attr5
+ "}";
}
}
Main
public class Main {
public static void main(String[] args) {
Product.Builder concreteBuilder = Product.getConcreteBuilder();
Product.Director director = Product.getDirector(concreteBuilder);
Product product = director.construct("A", "B", "C");
System.out.println(product);
}
}
不過我在工作中,看到大都是簡化的版本,控制Builder建構步驟通常直接在client直接操作,所以省去了Director,而且通常不會有多個Builder實作,所以變成下面這這樣。
Product
public class Product {
private String attr1;
private String attr2;
private String attr3;
private String attr4;
private String attr5;
// getters and setters...
public Product (Builder builder) {
this.attr1 = builder.attr1;
this.attr2 = builder.attr2;
this.attr3 = builder.attr3;
}
public static Builder getBuilder() {
return new Builder();
}
public static final class Builder {
private String attr1;
private String attr2;
private String attr3;
public Builder setAttr1(String attr1) {
this.attr1 = attr1;
return this;
}
public Builder setAttr2(String attr2) {
this.attr2 = attr2;
return this;
}
public Builder setAttr3(String attr3) {
this.attr3 = attr3;
return this;
}
public Product build() {
System.out.println("build product...");
return new Product(this);
}
}
@Override
public String toString() {
return this.getClass().getSimpleName()
+ ":{"
+ "attr1:" + attr1 + ","
+ "attr2:" + attr2 + ","
+ "attr3:" + attr3 + ","
+ "attr4:" + attr4 + ","
+ "attr5:" + attr5
+ "}";
}
}
Main
public class Main {
public static void main(String[] args) {
Product product = Product.getBuilder()
.setAttr1("A")
.setAttr2("B")
.setAttr3("C")
.build();
System.out.println(product);
}
}
那時麼時候會需要用到Builder Pattern呢?
當一個類別擁有多個屬性,有的屬性是必要,而有的屬性是選擇性的時候,或看到某類別有很多個建構式,或建構式有很多參數,而且在呼叫建構式時常傳一些null
進去的情況,就很適合用Builder Pattern來重構。
以建構Product為客戶Customer
來舉例,其擁有多個建構時就必須決定的屬性如下,有的屬性為必要,有的屬性為選擇性。
Customer
public class Customer {
private final String id; // 必要
private final String name; // 必要
private final String email; // 選填
private final String phone; // 選填
private final Integer age; // 選填
public Customer(String id, String name) {
this(id, name, null, null, null);
}
public Customer(String id, String name, String email, String phone, Integer age) {
this.id = id;
this.name = name;
this.email = email;
this.phone = phone;
this.age = age;
}
// getters...
public String toString() {
return this.getClass().getSimpleName() + ":{ "
+ "id:" + id + ", "
+ "name:" + name + ", "
+ "email:" + email + ", "
+ "phone:" + phone + ", "
+ "age:" + age
+ " }";
}
}
在client建構並執行。
Main
public class Main {
public static void main(String[] args) {
Customer customer1 = new Customer("A0001", "Amber Wang");
Customer customer2 = new Customer("A0002", "May li", "may-li@abc.com", null, null);
Customer customer3 = new Customer("A0003", "Ken Liu", "ken-liu@abc.com", "0917111591", null);
Customer customer4 = new Customer("A0004", "Ray Chen", "ray-chen@abc.com", "0917006740", 22);
Customer customer5 = new Customer("A0005", "Dan Hu", null, null, 33);
System.out.println(customer1);
System.out.println(customer2);
System.out.println(customer3);
System.out.println(customer4);
System.out.println(customer5);
}
}
印出結果如下。
Customer:{ id:A0001, name:Amber Wang, email:null, phone:null, age:null }
Customer:{ id:A0002, name:May li, email:may-li@abc.com, phone:null, age:null }
Customer:{ id:A0003, name:Ken Liu, email:ken-liu@abc.com, phone:0917111591, age:null }
Customer:{ id:A0004, name:Ray Chen, email:ray-chen@abc.com, phone:0917006740, age:22 }
Customer:{ id:A0005, name:Dan Hu, email:null, phone:null, age:33 }
但這樣出現了幾個問題,在client建構Customer
時,無從得知哪些屬性為必填,哪些為選填,開發者勢必要進去看Customer
的建構子程式邏輯才能知道。還有可能因為太多建構參數導致填錯位置,通常今天是5個以上的參數就會對到眼花了。
因此常看到的寫法是建構子只帶必要屬性的參數,其餘參數才在client中以setter方法來設定Customer
的各項參數。
Customer
public class Customer {
private final String id; // 必要
private final String name; // 必要
private String email; // 選填
private String phone; // 選填
private Integer age; // 選填
public Customer(String id, String name) {
this.id = id;
this.name = name;
}
// getters and setters...
public String toString() {
return this.getClass().getSimpleName() + ":{ "
+ "id:" + id + ", "
+ "name:" + name + ", "
+ "email:" + email + ", "
+ "phone:" + phone + ", "
+ "age:" + age
+ " }";
}
}
在client改成如下方式來建構Customer
。
Main
public class Main {
public static void main(String[] args) {
Customer customer1 = new Customer("A0001", "Amber Wang");
Customer customer2 = new Customer("A0002", "May li");
customer2.setEmail("may-li@abc.com");
Customer customer3 = new Customer("A0003", "Ken Liu");
customer3.setEmail("ken-liu@abc.com");
customer3.setPhone("0917111591");
Customer customer4 = new Customer("A0004", "Ray Chen");
customer4.setEmail("ray-chen@abc.com");
customer4.setPhone("0917006740");
customer4.setAge(22);
Customer customer5 = new Customer("A0005", "Dan Hu");
customer5.setAge(33);
System.out.println(customer1);
System.out.println(customer2);
System.out.println(customer3);
System.out.println(customer4);
System.out.println(customer5);
}
}
這樣建構Customer
物件時就變得非常彈性了,也提高了程式的可讀性。但缺點是選填屬性必須改為可變的(mutable),也就是必須移除final
修飾子,這樣才能呼叫setter方法去設值,如此導致無法確保執行緒安全(thread-safe)的問題。
所以此時就可以利用Builder Pattern來重新設計建構Customer
的方式。
Customer
public class Customer {
private final String id; // 必要
private final String name; // 必要
private final String email; // 選填
private final String phone; // 選填
private final Integer age; // 選填
public Customer(Builder builder) {
this.id = builder.id;
this.name = builder.name;
this.email = builder.email;
this.phone = builder.phone;
this.age = builder.age;
}
public static Builder getBuilder(String id, String name) {
return new Builder(id, name);
}
// getters...
public static final class Builder {
private String id;
private String name;
private String email;
private String phone;
private Integer age;
public Builder(String id, String name) {
this.id = id;
this.name = name;
}
public Builder setId(String id) {
this.id = id;
return this;
}
public Builder setName(String name) {
this.name = name;
return this;
}
public Builder setEmail(String email) {
this.email = email;
return this;
}
public Builder setPhone(String phone) {
this.phone = phone;
return this;
}
public Builder setAge(Integer age) {
this.age = age;
return this;
}
public Customer build() {
return new Customer(this);
}
}
public String toString() {
return this.getClass().getSimpleName() + ":{ "
+ "id:" + id + ", "
+ "name:" + name + ", "
+ "email:" + email + ", "
+ "phone:" + phone + ", "
+ "age:" + age
+ " }";
}
}
Main
public class Main {
public static void main(String[] args) {
Customer customer1 = Customer.getBuilder("A0001", "Amber Wang").build();
Customer customer2 = Customer.getBuilder("A0002", "May li")
.setEmail("may-li@abc.com").build();
Customer customer3 = Customer.getBuilder("A0003", "Ken Liu")
.setEmail("ken-liu@abc.com")
.setPhone("0917006740")
.build();
Customer customer4 = Customer.getBuilder("A0004", "Ray Chen")
.setEmail("ray-chen@abc.com")
.setPhone("0917006740")
.setAge(22)
.build();
Customer customer5 = Customer.getBuilder("A0005", "Dan Hu")
.setAge(33)
.build();
System.out.println(customer1);
System.out.println(customer2);
System.out.println(customer3);
System.out.println(customer4);
System.out.println(customer5);
}
}
如此不只確保了類別屬性的不可變性(immutable),也保持了建構時的彈性與可讀性。
Builder Pattern的缺點是讓程式碼變得冗長。另外不熟悉此Pattern的團隊成員會不清楚該如何建構物件,所以專案中有使用到Builder Pattern最好在程式註解,文件或教育訓練中說明。
Lombok套件的@Builder
注釋會自動產生Builder Pattern的程式碼,可減輕Builder Pattern冗長的問題。
Builder Pattern常見的例子有:
- Java的
StringBuilder
,StringBuffer
。 - Java Stream API。
- Java的
LocateDate
。 - JPA的
CriteriaBuilder
。 - Spring的
RequestEntity
。 - Mockito。
- Spring MVC Test Framework的各API。
而Builder Pattern(建造者模式)與Abstract Factory Pattern(抽象工廠模式)都是用來建構物件的設計模式,要注意兩者的區別。
參考:
沒有留言:
張貼留言