《重學Java設計模式》筆記——建造者模式

1. 建造者模式可以解決什麼問題

我家裏有各種形狀的瓷器,盤子或者碗。雖然形狀不同,但是所用的材料基本上是一樣的,比如土、水、釉、彩這些基本的東西。

但是做不同款式的瓷器,方法是不同的。假如說我現在已經寫好了一段代碼來生成白瓷碗和白瓷盤,正常運行起來了,這倒也沒什麼,可是,如果我現在要增加一個工作,製作彩繪馬克杯,這樣我就得修改已有的代碼了。

最簡單的修改方式就是加if-else。就像是這樣:

if (type = "白瓷碗") {
   makeWhiteBowl(...);

} else if (type = "白瓷盤") {
   makeWhitePlate(...);

} else if (type = "彩繪馬克杯") {
   makeColorCup(...);
}
...

Well,看起來還不很嚴重,那麼如果你的業務越來越複雜呢?每次新添加一種type,首先就要去改代碼,也許還不止這一處,這是不可忍受的,是很爛的設計。

建造者模式,就是爲了解決這種問題出現的一種設計模式。

在GoF的《設計模式》一書中,明確的提及了建造者模式的意圖:

將一個複雜對象的構建與它的表示分離,使得同樣的構建過程可以創建不同的表示。

這一論斷說明了這麼幾點:

  1. 構建過程和對象相分離,也就是說不要直接去new這個對象;
  2. 構建過程抽象出來到一個高層次上。

回到我上面的例子,就是製造瓷器的人和瓷器相分離。客戶交給匠人材料和訂單,匠人自行製造交付就可以了。

2. 建造者模式的理論

建造者模式中會存在下面幾種角色:

  • 建造者(Builder):接口,用於創建產品的各個部分的抽象;
  • ConcreteBuilder:實現Builder接口,是具體的建造者;
  • Director:這個名字看着像“導演”,其實是用於構造Builder對象的;
  • Product:各類產品,最終被Builder生產出來的東西。

下圖是GoF給出的類圖:

知道了建造者模式的基本類圖結構以後,還需要知道這個模式是如何使用的,如下時序圖:

3. 具體編碼實現

在《重學Java設計模式》中的建造者模式章節提到了一個裝修的例子,但是從這個例子上來看,和GoF的描述還是有一些區別的。

還是以裝修爲例,書中提到了集中裝修風格,其實就對應了幾種ConcreteBuilder,例如田園奢華歐式、輕奢田園和現代簡約。而這些建造者們都有相同的幾種方法:

  • 吊頂
  • 塗牆
  • 鋪地板或者地磚

另外還有物料,比如各種品牌的裝修材料等等。下面是我根據GoF的理論,對《重學Java設計模式》的建造者代碼的改造。

首先是物料,可以抽象出一個Matter接口,所有的地板,塗料都是Matter。

/**
 * 物料的五種屬性
 */
public interface Matter {

    //類型,比如地板
    String scene();

    //品牌
    String brand();

    //型號
    String model();

    //價格
    BigDecimal price();

    String desc();
}

物料並非建造者模式中的核心元素,接下來就可以抽象建造者了。

package com.example.pattern.builder.iface;

public interface IBuilder {
    //裝吊頂
    IBuilder appendCeiling(Matter ceiling);
    //塗牆
    IBuilder appendCoat(Matter coat);
    //鋪地板
    IBuilder appendFloor(Matter floor);
    //鋪地磚
    IBuilder appendTitle(Matter title);

    //得到裝修報價單
    String getResult();
}

建造者要做的工作就是來料加工,也就是一個裝修工人的手藝了。

下面是對Director的定義,可以認爲Director是監工吧,業主只和監工或者工頭直接聯繫,不會給工人直接下達任務:

package com.example.pattern.builder.builder;

import com.example.pattern.builder.iface.IBuilder;
import com.example.pattern.builder.iface.Matter;
import lombok.Data;

@Data
public class Director {
    private IBuilder builder;

    private Matter ceiling;
    private Matter coat;
    private Matter floor;
    private Matter title;

    public Director(IBuilder builder) {
        this.builder = builder;
    }

    public String construct() {
        return builder.appendCeiling(ceiling).appendCoat(coat)
                .appendFloor(floor).appendTitle(title).getResult();
    }

}

其實Director也不知道自己的物料是什麼。從時序圖上來看,不管是具體的建造者還是Director都是使用者(就像業主)提供的,因此使用者的代碼如下:

public class BuilderApp {
    public static void main(String[] args) {
        IBuilder europeBuilder = new EuropeBuilder(new BigDecimal(160));
        Director director = new Director(europeBuilder);
        director.setCeiling(new LevelTwoCeiling());
        director.setFloor(new MarcoPoloTitle());
        director.setCoat(new NipponCoat());
        String result = director.construct();
        System.out.println(result);
    }
}

這個結果就是計算一下裝修的賬單,很簡單的一個示例,最終的打印是總報價。

4. 進一步的思考

在結城浩的《圖解設計模式》一書中提到了“誰知道什麼”的概念。在上面的代碼中,這個概念體現了兩次:

  • BuilderApp類並沒有直接調用Builder,BuilderApp並不知道Builder能做什麼;
  • Director也不知道自己到底調用的是哪個Builder,它使用的是一個接口。

OOP一定是面向接口最好的,因爲可以隨時替換掉具體的實現,不同的類之間耦合度不高。

在新增一個產品的時候,只需要添加一個具體的Builder就可以了,不需要修改現有代碼。

學習一種設計模式總是寫這種簡單的例子也感覺沒什麼意思。我在之前的工作中因爲一些原因不能用現成的第三方包,就只能寫了一個Map的生成器來做事情,我覺得這個倒是挺有建造者模式的味道的。

首先我不需要定義Map了,因爲Java.util已經提供了足夠多的,而且是接口和實現分離的。

下面需要定義一個Builder:

package com.example.pattern.builder.maphelper;

import java.util.Map;

public class MapBuilder<K, V> {
    private Map<K, V> map;

    public MapBuilder(Map<K, V> map) {
        this.map = map;
    }

    public MapBuilder<K, V> put(K k, V v) {
        map.put(k, v);
        return this;
    }

    public Map<K, V> build() {
        return map;
    }
}

這個Builder更好的地方就是自己都不知道自己製造什麼產品。接下來時Director的角色:

package com.example.pattern.builder.maphelper;


import java.util.Map;

public class MapHelper {

    public static <K, V> MapBuilder<K, V> builder(Map<K, V> map) {
        return new MapBuilder<>(map);
    }

}

這簡直太讓人舒適了,因爲調用者的代碼是鏈式的:

Map<String, String> map
                = MapHelper.builder(new HashMap<String, String>()).put("foo", "bar")
                .build();

這個建造者和GoF的理論有一點差異,就是MapHelper並沒有持有任何對象,沒有構造方法。其實如果一定要改造也是可以的。

Map本身是一個比較複雜的構建,因爲要不停的put,代碼也顯得不夠漂亮,鏈式put則顯得優雅一些。而且這個Builder本身也實現了構建和表示分離的原則。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章