AdSense

網頁

2019/9/12

Java 設計模式 建造者模式 Builder Pattern

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)抽離成為以下的BuilderConcreteBuilderDirector

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各角色間的關係。





由於BuilderConcreteBuilderDirector都僅是用於建構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常見的例子有:


而Builder Pattern(建造者模式)與Abstract Factory Pattern(抽象工廠模式)都是用來建構物件的設計模式,要注意兩者的區別。


參考:

沒有留言:

AdSense