java設計模式-Build模式

在日常開發中,時常會遇到一個類中包含了很多成員變量的情況,並且這些成員變量有些是必須的,有些則是可選域。
那麼對於這樣的類,應該採用哪種方法或者構造器來編寫呢?

場景假設

假設 我們需要創建一個手機類,而手機含有很多必須屬性,也有很多可選的屬性。例如下面這個類。

public class Phone {
    // 品牌
    private String brand;
    // 型號
    private String model;
    // 上市時間
    private String marketDate;
    // 運存
    private int ram;
    // 內存
    private int rom;
    // 價格
    private int price;
    // 電池容量
    private int batteryCapacity;
    // 顏色
    private String colour;
    // 長 mm
    private double length;
    // 寬 mm
    private double width;
    // 高 mm
    private double height;
    // 重量 g
    private int weight;
    // 屏幕尺寸 inches
    private double screenSize;
    // 處理器型號
    private String cpu;
    //===========================以下爲可選
    // 揚聲器個數
    private int speakerNum;
    // 前置攝像頭個數
    private int preCameraNum;
    // 後置攝像頭個數
    private int backCameraNum;
    // 防水功能
    private boolean waterproof;
    // 3.5mm耳機孔
    private boolean headsetHole;
    // NFC功能
    private boolean nfc;
    // 紅外功能
    private boolean infrared;
    // 無線充電
    private boolean wirelessCharge;
    // 反向無線充電
    private boolean reWirelessCharge;
}

重疊構造器模式

程序員一貫採用重疊構造器(telescoping constructor)模式,在這種模式下,你提供第一個只有必要參數的構造器,第二個構造器有一個可選參數,第三個構造器有兩個可選參數,以此類推。最後一個構造器擁有全部參數。

//我們需要創建很多構造器,以便調用者可以直接創建出可以使用的實例,所以有了如下構造器
    //構造器1
    public Phone(String brand, String model, String marketDate, int ram, int rom, int price, int batteryCapacity,
            String colour, double length, double width, double height, int weight, double screenSize, String cpu,
            int speakerNum, int preCameraNum, int backCameraNum, boolean waterproof, boolean headsetHole, boolean nfc,
            boolean infrared, boolean wirelessCharge, boolean reWirelessCharge) {
        super();
        this.brand = brand;
        this.model = model;
        this.marketDate = marketDate;
        this.ram = ram;
        this.rom = rom;
        this.price = price;
        this.batteryCapacity = batteryCapacity;
        this.colour = colour;
        this.length = length;
        this.width = width;
        this.height = height;
        this.weight = weight;
        this.screenSize = screenSize;
        this.cpu = cpu;
        this.speakerNum = speakerNum;
        this.preCameraNum = preCameraNum;
        this.backCameraNum = backCameraNum;
        this.waterproof = waterproof;
        this.headsetHole = headsetHole;
        this.nfc = nfc;
        this.infrared = infrared;
        this.wirelessCharge = wirelessCharge;
        this.reWirelessCharge = reWirelessCharge;
    }
	//構造器2
    public Phone(String brand, String model, String marketDate, int ram, int rom, int price, int batteryCapacity,
            String colour, double length, double width, double height, int weight, double screenSize, String cpu,
            int speakerNum, int preCameraNum, int backCameraNum, boolean waterproof, boolean headsetHole, boolean nfc,
            boolean infrared) {
        super();
        this.brand = brand;
        this.model = model;
        this.marketDate = marketDate;
        this.ram = ram;
        this.rom = rom;
        this.price = price;
        this.batteryCapacity = batteryCapacity;
        this.colour = colour;
        this.length = length;
        this.width = width;
        this.height = height;
        this.weight = weight;
        this.screenSize = screenSize;
        this.cpu = cpu;
        this.speakerNum = speakerNum;
        this.preCameraNum = preCameraNum;
        this.backCameraNum = backCameraNum;
        this.waterproof = waterproof;
        this.headsetHole = headsetHole;
        this.nfc = nfc;
        this.infrared = infrared;
    }
	//構造器3
    public Phone(String brand, String model, String marketDate, int ram, int rom, int price, int batteryCapacity,
            String colour, double length, double width, double height, int weight, double screenSize, String cpu,
            int speakerNum, int preCameraNum, int backCameraNum, boolean waterproof, boolean headsetHole) {
        super();
        this.brand = brand;
        this.model = model;
        this.marketDate = marketDate;
        this.ram = ram;
        this.rom = rom;
        this.price = price;
        this.batteryCapacity = batteryCapacity;
        this.colour = colour;
        this.length = length;
        this.width = width;
        this.height = height;
        this.weight = weight;
        this.screenSize = screenSize;
        this.cpu = cpu;
        this.speakerNum = speakerNum;
        this.preCameraNum = preCameraNum;
        this.backCameraNum = backCameraNum;
        this.waterproof = waterproof;
        this.headsetHole = headsetHole;
    }
	//構造器4
    public Phone(String brand, String model, String marketDate, int ram, int rom, int price, int batteryCapacity,
            String colour, double length, double width, double height, int weight, double screenSize, String cpu,
            int speakerNum, int preCameraNum, int backCameraNum, boolean waterproof) {
        super();
        this.brand = brand;
        this.model = model;
        this.marketDate = marketDate;
        this.ram = ram;
        this.rom = rom;
        this.price = price;
        this.batteryCapacity = batteryCapacity;
        this.colour = colour;
        this.length = length;
        this.width = width;
        this.height = height;
        this.weight = weight;
        this.screenSize = screenSize;
        this.cpu = cpu;
        this.speakerNum = speakerNum;
        this.preCameraNum = preCameraNum;
        this.backCameraNum = backCameraNum;
        this.waterproof = waterproof;
    }

當你需要創建實例的時候,就要利用參數列表最短的構造器,但是該列表包含了要設置的所有參數。

 Phone phone = new Phone(
                "華爲", "p40 pro", "2020年3月", 8,256,6488, 4100, "冰霜銀", 
                72.6, 8.95, 158.2, 209, 6.58, "麒麟990 5G", 1, 
                2, 4, true, false, true, 
                true, true, true);

當有多個構造器時,調用者很容易暈頭轉向。
在這裏插入圖片描述
這個構造器通常會需要許多你本不想設置的參數,但是你還是不得不爲他們傳遞值。如果僅僅是幾個參數還不算太糟糕,問題是隨着參數數目的增加,它很快會失去控制。
總結
重疊構造器模式可行,但是黨有許多參數的時候,客戶端代碼會很難編寫,並且仍然較難閱讀。如果讀者想知道哪些值是什麼意思,必須很仔細的數着這些參數來一探究竟。一段長類型的相同的參數會導致一些微妙的錯誤。如果客戶端不小心顛倒了兩個參數的順序,編譯器也不會報錯,但是在程序運行間就會出現錯誤行爲。

JavaBean模式

遇到許多構造參數的時候,還有第二種方法代替,即JavaBean模式,在這種模式下,調用一個無參構造器來創建對象,然後調用一個無參構造來創建對象,然後調用setter方法來設置每個必要的參數,以及每個相關的可選參數。

//我們爲所有屬性提供get、set方法,並提供無參構造器,通過set方法來進行賦值
    public Phone() {
        super();
    }


    public String getBrand() {
        return brand;
    }


    public void setBrand(String brand) {
        this.brand = brand;
    }


    public String getModel() {
        return model;
    }


    public void setModel(String model) {
        this.model = model;
    }


    public String getMarketDate() {
        return marketDate;
    }


    public void setMarketDate(String marketDate) {
        this.marketDate = marketDate;
    }


    public int getRam() {
        return ram;
    }


    public void setRam(int ram) {
        this.ram = ram;
    }


    public int getRom() {
        return rom;
    }


    public void setRom(int rom) {
        this.rom = rom;
    }


    public int getPrice() {
        return price;
    }


    public void setPrice(int price) {
        this.price = price;
    }


    public int getBatteryCapacity() {
        return batteryCapacity;
    }


    public void setBatteryCapacity(int batteryCapacity) {
        this.batteryCapacity = batteryCapacity;
    }


    public String getColour() {
        return colour;
    }


    public void setColour(String colour) {
        this.colour = colour;
    }


    public double getLength() {
        return length;
    }


    public void setLength(double length) {
        this.length = length;
    }


    public double getWidth() {
        return width;
    }


    public void setWidth(double width) {
        this.width = width;
    }


    public double getHeight() {
        return height;
    }


    public void setHeight(double height) {
        this.height = height;
    }


    public int getWeight() {
        return weight;
    }


    public void setWeight(int weight) {
        this.weight = weight;
    }


    public double getScreenSize() {
        return screenSize;
    }


    public void setScreenSize(double screenSize) {
        this.screenSize = screenSize;
    }


    public String getCpu() {
        return cpu;
    }


    public void setCpu(String cpu) {
        this.cpu = cpu;
    }


    public int getSpeakerNum() {
        return speakerNum;
    }


    public void setSpeakerNum(int speakerNum) {
        this.speakerNum = speakerNum;
    }


    public int getPreCameraNum() {
        return preCameraNum;
    }


    public void setPreCameraNum(int preCameraNum) {
        this.preCameraNum = preCameraNum;
    }


    public int getBackCameraNum() {
        return backCameraNum;
    }


    public void setBackCameraNum(int backCameraNum) {
        this.backCameraNum = backCameraNum;
    }


    public boolean isWaterproof() {
        return waterproof;
    }


    public void setWaterproof(boolean waterproof) {
        this.waterproof = waterproof;
    }


    public boolean isHeadsetHole() {
        return headsetHole;
    }


    public void setHeadsetHole(boolean headsetHole) {
        this.headsetHole = headsetHole;
    }


    public boolean isNfc() {
        return nfc;
    }


    public void setNfc(boolean nfc) {
        this.nfc = nfc;
    }


    public boolean isInfrared() {
        return infrared;
    }


    public void setInfrared(boolean infrared) {
        this.infrared = infrared;
    }


    public boolean isWirelessCharge() {
        return wirelessCharge;
    }


    public void setWirelessCharge(boolean wirelessCharge) {
        this.wirelessCharge = wirelessCharge;
    }


    public boolean isReWirelessCharge() {
        return reWirelessCharge;
    }


    public void setReWirelessCharge(boolean reWirelessCharge) {
        this.reWirelessCharge = reWirelessCharge;
    }

這種方式彌補了重疊構造器的不足。說得明白一點就是讀起來容易,用起來也容易。

        Phone phone = new Phone();
        phone.setBrand("華爲");
        phone.setModel("p40 pro");
        phone.setMarketDate("2020年3月");
        phone.setRam(4);
        phone.setRom(256);
        phone.setPrice(6488);
        phone.setBatteryCapacity(4100);
        phone.setColour("冰霜銀");
        phone.setLength(158.2);
        phone.setWidth(72.6);
        phone.setHeight(8.95);
        phone.setWeight(209);
        phone.setCpu("麒麟990 5G");
        phone.setSpeakerNum(1);
        phone.setPreCameraNum(2);
        phone.setBackCameraNum(4);
        phone.setWaterproof(true);
        phone.setHeadsetHole(false);
        phone.setNfc(true);
        phone.setInfrared(true);
        phone.setWirelessCharge(true);
        phone.setReWirelessCharge(true);

遺憾的是,JavaBean 模式自身有着很嚴重的缺點。

  • 因爲構造過程被分到了幾個調用中,在構造過程中javabean可能處於不一致的狀態。類無法僅僅通過檢驗構造器的參數的有效性來保持一致性。試圖使用不一致狀態的對象,將會導致失敗,這種失敗與包含錯誤的代碼大相徑庭,因此它調試起來十分困難。
  • 與此相關的另一點不足在於,JavaBean模式阻止了把類做成不可變的可能,這就需要程序員付出額外的努力來保證它的線程安全。

Build模式

幸運的是,還有第三種替代方法,既能保證像重疊構造器那樣的安全性,也能保證像JavaBean模式那麼好的可讀性,這就是Build模式。
不直接生成想要的對象,而是讓客戶端利用所有構造的參數調用構造器(或靜態工廠),得到一個builder對象。然後客戶端在builder對象上調用類似於setter的方法,來設置每個相關的可選參數。最後客戶端調用無參的build方法來生成不可變的對象。這個builder是它構建的類的靜態成員類。

@SuppressWarnings("unused")
public class Phone {
    // 品牌
    private final String brand;
    // 型號
    private final String model;
    // 上市時間
    private final String marketDate;
    // 運存
    private final int ram;
    // 內存
    private final int rom;
    // 價格
    private final int price;
    // 電池容量
    private final int batteryCapacity;
    // 顏色
    private final String colour;
    // 長 mm
    private final double length;
    // 寬 mm
    private final double width;
    // 高 mm
    private final double height;
    // 重量 g
    private final int weight;
    // 屏幕尺寸 inches
    private final double screenSize;
    // 處理器型號
    private final String cpu;
    // 揚聲器個數
    private final int speakerNum;
    // 前置攝像頭個數
    private final int preCameraNum;
    // 後置攝像頭個數
    private final int backCameraNum;
    // 防水功能
    private final boolean waterproof;
    // 3.5mm耳機孔
    private final boolean headsetHole;
    // NFC功能
    private final boolean nfc;
    // 紅外功能
    private final boolean infrared;
    // 無線充電
    private final boolean wirelessCharge;
    // 反向無線充電
    private final boolean reWirelessCharge;

    public static class Builder {
        // ==================必須參數==================
        // 品牌
        private final String brand;
        // 型號
        private final String model;
        // 上市時間
        private final String marketDate;
        // 運存
        private final int ram;
        // 內存
        private final int rom;
        // 價格
        private final int price;
        // 電池容量
        private final int batteryCapacity;
        // 顏色
        private final String colour;
        // 長 mm
        private final double length;
        // 寬 mm
        private final double width;
        // 高 mm
        private final double height;
        // 重量 g
        private final int weight;
        // 屏幕尺寸 inches
        private final double screenSize;
        // 處理器型號
        private final String cpu;
        // ==================不必須參數,給予默認值==================
        // 揚聲器個數
        private int speakerNum = 1;
        // 前置攝像頭個數
        private int preCameraNum = 0;
        // 後置攝像頭個數
        private int backCameraNum = 1;
        // 防水功能
        private boolean waterproof = false;
        // 3.5mm耳機孔
        private boolean headsetHole = false;
        // NFC功能
        private boolean nfc = false;
        // 紅外功能
        private boolean infrared = false;
        // 無線充電
        private boolean wirelessCharge = false;
        // 反向無線充電
        private boolean reWirelessCharge = false;

        public Builder(String brand, String model, String marketDate, int ram, int rom, int price, int batteryCapacity,
                String colour, double length, double width, double height, int weight, double screenSize, String cpu) {
            super();
            this.brand = brand;
            this.model = model;
            this.marketDate = marketDate;
            this.ram = ram;
            this.rom = rom;
            this.price = price;
            this.batteryCapacity = batteryCapacity;
            this.colour = colour;
            this.length = length;
            this.width = width;
            this.height = height;
            this.weight = weight;
            this.screenSize = screenSize;
            this.cpu = cpu;
        }

        public Builder speakerNum(int val) {
            speakerNum = val;
            return this;
        }

        public Builder preCameraNum(int val) {
            preCameraNum = val;
            return this;
        }

        public Builder backCameraNum(int val) {
            backCameraNum = val;
            return this;
        }

        public Builder waterproof(boolean val) {
            waterproof = val;
            return this;
        }

        public Builder headsetHole(boolean val) {
            headsetHole = val;
            return this;
        }

        public Builder nfc(boolean val) {
            nfc = val;
            return this;
        }

        public Builder infrared(boolean val) {
            infrared = val;
            return this;
        }

        public Builder wirelessCharge(boolean val) {
            wirelessCharge = val;
            return this;
        }

        public Builder reWirelessCharge(boolean val) {
            reWirelessCharge = val;
            return this;
        }

        public Phone builder() {
            return new Phone(this);
        }

    }

    private Phone(Builder builder) {
        brand = builder.brand;
        model = builder.model;
        marketDate = builder.marketDate;
        ram = builder.ram;
        rom = builder.rom;
        price = builder.price;
        batteryCapacity = builder.batteryCapacity;
        colour = builder.colour;
        length = builder.length;
        width = builder.width;
        height = builder.height;
        weight = builder.weight;
        screenSize = builder.screenSize;
        cpu = builder.cpu;
        speakerNum = builder.speakerNum;
        preCameraNum = builder.preCameraNum;
        backCameraNum = builder.backCameraNum;
        waterproof = builder.waterproof;
        headsetHole = builder.headsetHole;
        nfc = builder.nfc;
        infrared = builder.infrared;
        wirelessCharge = builder.wirelessCharge;
        reWirelessCharge = builder.reWirelessCharge;
    }

}

客戶端代碼,這樣的客戶端代碼更容易編寫,更爲重要的是,易於閱讀。builder模式模擬了具體名的可選參數,就像Ada和Python中的一樣。

Phone phone = new Phone.Builder("華爲", "p40 pro", "2020年3月", 8, 256, 6488, 4100, "冰霜銀", 158.2, 72.6, 8.95, 209,
        6.58, "麒麟990 5G").speakerNum(1).preCameraNum(2).backCameraNum(4).nfc(true).waterproof(true)
                .headsetHole(false).infrared(true).wirelessCharge(true).reWirelessCharge(true).builder();

與構造器相比,builder的微略優勢在於,builder可以有多個可變的(varargs)參數。構造器就像方法一樣,只能有一個可變參數。因爲builder利用單獨的方法來設置每個參數,你想要多少個可變參數,他就可以有多少個,直到每個setter都有一個可變參數。
Builder模式也有自身的不足,爲了創建對象,必須先創建他的構建器。雖然創建構建器的開銷在實踐中可能不那麼明顯,但是在某些十分注重性能的情況下,可能就問題了。
Builder 模式還比重疊構造器模式更加冗長,因此它只在有很多參數的時候才使用,比如4個或更多參數,但請記住,將來你可能需要添加參數。如果一開始就是用構造器或者靜態工廠,等到類需要多個參數時才添加構建器,就會無法控制,那些過時的構造器或者靜態工廠顯得十分不協調。因此,通常最好一開始就使用構建器。

總結

何時使用 Builer 模式

  • 類的構造器或靜態工廠中有多個參數
  • 類的參數大於4個時
  • 大部分參數爲可選時
  • 對性能要求不是特別極致的
  • 要求高的代碼可讀性時
  • 需要保證線程安全時
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章