建造者模式就是這麼簡單

TimberLiu發表於2019-05-12

上一篇文章中學習了三種工廠模式,這篇文章來學習一下另一種應用比較多的建立型模式,它適合用來建立擁有非常多的屬性的物件。同樣地,也是先從一個例子開始。

建造者模式

相信小夥伴們在假期出去遊玩時,都會制定一套度假計劃。一般來說,需要包括如下幾個部分,VacationPlanner 類可以如下定義:

public class VacationPlanner {

    private int day; // 天數
    private String city; // 旅遊城市
    private int ticket; // 車票
    private String hotel; // 住宿旅館
    private String tourist; // 旅遊景點
    private int personNum; // 人數
    private String other; // 其他
    
    // setter/getter/toString   
}
複製程式碼

如此,我們建立計劃的方法 createPlanner 可以按照如下寫:

public class Vacation {

    public VacationPlanner createPlanner(int day, String city, String hotel, int personNum, 
                              int ticket, String tourist, String other) {
        VacationPlanner planner = new VacationPlanner();
        planner.setDay(day);
        planner.setCity(city);
        planner.setHotel(hotel);
        planner.setPersonNum(personNum);
        planner.setTicket(ticket);
        planner.setTourist(tourist);
        planner.setOther(other);

        return planner;
    }
}
複製程式碼

很簡單吧。但是如果很多人都想要製作一個度假計劃,而每個人的計劃又不太一樣,所以,建立的度假計劃及其建立步驟也就不同。例如,有的人就是本地人,不需要旅館;有的人需要其他活動。

另外,其實我們並不需要知道度假計劃的內部實現。那我們該如何做一個有彈性的設計,去適應各種變化呢?其實,我們可以建立一個單獨的建立者,將度假計劃的表示和建立計劃的步驟分開來。

但在這之前,我們應該定義一個抽象建造者,也就是要符合之前我們所說的針對介面程式設計,而不是具體的類:

public abstract class AbstractVacationPlannerBuilder {

    public abstract void buildDay();
    public abstract void buildTicket();
    public abstract void buildHotel();
    public abstract void buildTourist();
    public abstract void buildPersonNum();
    public abstract void buildOther();
    public abstract VacationPlanner getVacationPlanner();
}
複製程式碼

然後,再來建立一個具體的建造者:

public class VacationPlannerBuilder extends AbstractVacationPlannerBuilder {

    private VacationPlanner vacationPlanner = new VacationPlanner();

    @Override
    public void buildDay(int day) {
        vacationPlanner.setDay(day);
    }
    @Override
    public void buildTicket(int ticket) {
        vacationPlanner.setTicket(ticket);
    }
    @Override
    public void buildHotel(String hotel) {
        vacationPlanner.setHotel(hotel);
    }
    @Override
    public void buildTourist(String tourist) {
        vacationPlanner.setTourist(tourist);
    }
    @Override
    public void buildPersonNum(int personNum) {
        vacationPlanner.setPersonNum(personNum);
    }   
    @Override
    public void buildOther(String other) {
        vacationPlanner.setOther(other);
    }

    @Override
    public VacationPlanner getVacationPlanner() {
        return vacationPlanner;
    }
}
複製程式碼

現在 Vacation 就可以按照如下的方式寫了:

public class Vacation {

    private AbstractVacationPlannerBuilder builder;
    
    public void setVacationPlannerBuilder(AbstractVacationPlannerBuilder builder) {
        this.builder = builder;
    }

    public AbstractVacationPlannerBuilder createPlanner(int day, String city, String hotel, int personNum,
                                         int ticket, String tourist, String meal, String other) {
        builder.buildDay(day);
        builder.buildCity(city);
        builder.buildHotel(hotel);
        builder.buildPersonNum(personNum);
        builder.buildTicket(ticket);
        builder.buildTourist(tourist);
        builder.buildOther(other);
        
        return builder;
    }
}
複製程式碼

上述模式就是建造者模式,它是將一個複雜物件的表示和建立分離開來,允許通過多個步驟來建立物件,並且可以改變其過程。

它的 UML 圖如下:

建造者模式就是這麼簡單

下面來總結一下建造者模式的優點:

  • 建造者模式將一個複雜物件的建立過程封裝起來;
  • 向客戶端隱藏了產品內部的實現,將產品本身與產品的建立過程分離開來;
  • 可以更加精確地控制產品的建立過程,將複雜產品的建立步驟分解在不同的方法中;

缺點:

  • 在建造者模式中,如果產品之間的差異性太大,則不適合使用建造者模式。
  • 如果產品的內部變化複雜,可能需要定義很多建造者類來實現這些變化,導致系統變得很龐大。

靜態內部類方式

另外,建造者模式也可以使用靜態內部類的形式,它是將實體類對應的 Builder 放置到實體類的內部,看一下它的程式碼:

public class VacationPlanner {

    private int day; // 天數
    private String city; // 旅遊城市
    private int ticket; // 車票
    private String hotel; // 住宿旅館
    private String tourist; // 旅遊景點
    private int personNum; // 人數
    private String other; // 其他
    
    // 全屬性的構造方法
    public VacationPlanner(VacationPlannerBuilder builder) {
        this.day = builder.day;
        this.city = builder.city;
        this.ticket = builder.ticket;
        this.hotel = builder.hotel;
        this.tourist = builder.tourist;
        this.personNum = builder.personNum;
        this.other = builder.other;
    }

    public static class VacationPlannerBuilder {

        private int day; // 天數
        private String city; // 旅遊城市
        private int ticket; // 車票
        private String hotel; // 住宿旅館
        private String tourist; // 旅遊景點
        private int personNum; // 人數
        private String other; // 其他

        // 與之前不同,這裡返回 VacationPlannerBuilder
        public VacationPlannerBuilder buildDay(int day) {
            this.day = day;
            return this;
        }
        public VacationPlannerBuilder buildCity(String city) {
            this.city = city;
            return this;
        }
        public VacationPlannerBuilder buildTicket(int ticket) {
            this.ticket = ticket;
            return this;
        }
        public VacationPlannerBuilder buildHotel(String hotel) {
            this.hotel = hotel;
            return this;
        }
        public VacationPlannerBuilder buildTourist(String tourist) {
            this.tourist = tourist;
            return this;
        }
        public VacationPlannerBuilder buildPersonNum(int personNum) {
            this.personNum = personNum;
            return this;
        }
        public VacationPlannerBuilder buildOther(String other) {
            this.other = other;
            return this;
        }
        
        public VacationPlanner build() {
            return new VacationPlanner(this);
        }
    }
}
複製程式碼

下面來寫個測試類進行測試:

public class Test {

    public static void main(String[] args) {
        VacationPlanner planner = new VacationPlanner.VacationPlannerBuilder()
                .buildDay(3)
                .buildCity("北京")
                .buildHotel("7天")
                .buildPersonNum(2)
                .buildTicket(999)
                .buildTourist("故宮")
                .buildOther("其他")
                .build();
        System.out.println(planner);
    }
}
複製程式碼

建造者模式的具體實踐

JDK#StringBuilder

AbstractStringBuilder

可以將 StringBuilder 看成 String 的建造者,而 AbstractStringBuilder 是建造者的抽象類。它的部分原始碼如下:

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    // 儲存 char 字元的陣列
    char[] value;
    int count;
    
    public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len); // 確保容量
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }
    public AbstractStringBuilder append(int i) {
        if (i == Integer.MIN_VALUE) {
            append("-2147483648");
            return this;
        }
        int appendedLength = (i < 0) ? Integer.stringSize(-i) + 1
                                     : Integer.stringSize(i);
        int spaceNeeded = count + appendedLength;
        ensureCapacityInternal(spaceNeeded);
        Integer.getChars(i, spaceNeeded, value);
        count = spaceNeeded;
        return this;
    }
    
    @Override
    public abstract String toString();
    
}
複製程式碼

StringBuilder

StringBuilder 的部分原始碼如下,這裡使用的建造者模式是我們之前看到的第一種模式:

public final class StringBuilder
        extends AbstractStringBuilder
        implements java.io.Serializable, CharSequence {
        
    @Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }
    @Override
    public StringBuilder append(int i) {
        super.append(i);
        return this;
    }
    
    @Override
    public String toString() {
        // 建立了一個新的 String
        return new String(value, 0, count);
    }
}
複製程式碼

Guava#ImmutableSet

ImmutableSet 的建造者使用的是靜態內部類方式:

public abstract class ImmutableSet<E> extends ImmutableCollection<E> implements Set<E> {

    public static <E> Builder<E> builder() {
        return new Builder<E>();
    }

    public static class Builder<E> extends ImmutableCollection.ArrayBasedBuilder<E> {
        
        @Override
        public Builder<E> add(E element) {
          super.add(element);
          return this;
        }
        @Override
        public Builder<E> addAll(Iterable<? extends E> elements) {
          super.addAll(elements);
          return this;
        }
        @Override
        public ImmutableSet<E> build() {
            ImmutableSet<E> result = construct(size, contents);
            size = result.size();
            return result;
        }
    }   
    
    private static <E> ImmutableSet<E> construct(int n, Object... elements) {
        // 返回不同的 ImmutableSet 實現
    }
}
複製程式碼

一般它有兩種建立方法:

ImmutableSet<Object> set1 = new ImmutableSet.Builder<>().build();

ImmutableSet<Object> set2 = ImmutableSet.builder().build();
複製程式碼

參考資料

  • 《Head First 設計模式》

相關文章