重學 Java 設計模式:實戰建造者模式

小傅哥發表於2020-05-27


作者:小傅哥
部落格:https://bugstack.cn - 原創系列專題文章

沉澱、分享、成長,讓自己和他人都能有所收穫!?

一、前言

亂碼七糟 [luàn qī bā zāo],我時常懷疑這個成語是來形容程式猿的!

無論承接什麼樣的需求,是不是身邊總有那麼幾個人程式碼寫的爛,但是卻時常有測試小姐姐過來聊天(求改bug)、有產品小夥伴送吃的(求寫需求)、有業務小妹妹陪著改程式碼(求上線),直至領導都認為他的工作很重要,而在旁邊的你只能蹭點吃的。

那你說,CRUD的程式碼還想讓我怎麼樣?

這樣的小夥伴,可能把程式碼寫的很直接,ifelse多用一點,滿足於先臨時支援一下,想著這也沒什麼的。而且這樣的業務需求要的急又都是增刪改查的內容,實在不想做設計。而如果有人提到說好好設計下,可能也會被反對不要過渡設計。

貼膏藥似的修修補補,一次比一次恐怖!

第一次完成產品需求實在是很快,但網際網路的程式碼不比傳統企業。在傳統行業可能一套程式碼能用十年,但在網際網路高速的迭代下你的工程,一年就要變動幾十次。如果從一開始就想著只要完成功能就可以,那麼隨之而來的是後續的需求難以承接,每次看著成片成片的程式碼,實在不知如何下手。

在研發流程規範下執行,才能寫出好程式!

一個專案的上線往往要經歷業務需求產品設計研發實現測試驗證上線部署正式開量,而這其中對研發非常重要的一換就是研發實現的過程,又可以包括為;架構選型功能設計設計評審程式碼實現程式碼評審單測覆蓋率檢查編寫文件提交測試。所以在一些流程規範下,其實很難讓你隨意開發程式碼。

開發程式碼的過程不是炫技,就像蓋房子如果不按照圖紙來修建,回首就在山牆上搭一個廚房衛浴!可能在現實場景中這很荒唐,但在功能開發中卻總有這樣的程式碼。

所以我們也需要一些設計模式的標準思想,去建設程式碼結構,提升全域性把控能力。

二、開發環境

  1. JDK 1.8
  2. Idea + Maven
  3. 涉及工程三個,可以通過關注公眾號bugstack蟲洞棧,回覆原始碼下載獲取(開啟獲取的連結,找到序號18)
工程 描述
itstack-demo-design-3-00 場景模擬工程,模擬裝修過程中的套餐選擇(豪華、田園、簡約)
itstack-demo-design-3-01 使用一坨程式碼實現業務需求,也是對ifelse的使用
itstack-demo-design-3-02 通過設計模式優化改造程式碼,產生對比性從而學習

三、建造者模式介紹

建造者模式,圖片來自 refactoringguru.cn

建造者模式所完成的內容就是通過將多個簡單物件通過一步步的組裝構建出一個複雜物件的過程。

那麼,哪裡有這樣的場景呢?

例如你玩王者榮耀的時的初始化介面;有三條路、有樹木、有野怪、有守衛塔等等,甚至依賴於你的網路情況會控制清晰度。而當你換一個場景進行其他不同模式的選擇時,同樣會建設道路、樹木、野怪等等,但是他們的擺放和大小都有不同。這裡就可以用到建造者模式來初始化遊戲元素。

而這樣的根據相同的物料,不同的組裝所產生出的具體的內容,就是建造者模式的最終意圖,也就是;將一個複雜的構建與其表示相分離,使得同樣的構建過程可以建立不同的表示。

四、案例場景模擬

場景模擬;裝修套餐選擇(豪華、田園、簡約)

這裡我們模擬裝修公司對於設計出一些套餐裝修服務的場景。

很多裝修公司都會給出自家的套餐服務,一般有;歐式豪華、輕奢田園、現代簡約等等,而這些套餐的後面是不同的商品的組合。例如;一級&二級吊頂、多樂士塗料、聖象地板、馬可波羅地磚等等,按照不同的套餐的價格選取不同的品牌組合,最終再按照裝修面積給出一個整體的報價。

這裡我們就模擬裝修公司想推出一些套餐裝修服務,按照不同的價格設定品牌選擇組合,以達到使用建造者模式的過程。

1. 場景模擬工程

itstack-demo-design-3-00
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                ├── ceilling
                │   ├── LevelOneCeiling.java
                │   └── LevelTwoCeiling.java
                ├── coat
                │   ├── DuluxCoat.java
                │   └── LiBangCoat.java
                │   └── LevelTwoCeiling.java
                ├── floor
                │   ├── DerFloor.java
                │   └── ShengXiangFloor.java
                ├── tile
                │   ├── DongPengTile.java
                │   └── MarcoPoloTile.java
                └── Matter.java

在模擬工程中提供了裝修中所需要的物料;ceilling(吊頂)coat(塗料)floor(地板)tile(地磚),這麼四項內容。(實際的裝修物料要比這個多的多

2. 場景簡述

2.1 物料介面

public interface Matter {

    String scene();      // 場景;地板、地磚、塗料、吊頂

    String brand();      // 品牌

    String model();      // 型號

    BigDecimal price();  // 價格

    String desc();       // 描述

}
  • 物料介面提供了基本的資訊,以保證所有的裝修材料都可以按照統一標準進行獲取。

2.2 吊頂(ceiling)

一級頂

public class LevelOneCeiling implements Matter {

    public String scene() {
        return "吊頂";
    }

    public String brand() {
        return "裝修公司自帶";
    }

    public String model() {
        return "一級頂";
    }

    public BigDecimal price() {
        return new BigDecimal(260);
    }

    public String desc() {
        return "造型只做低一級,只有一個層次的吊頂,一般離頂120-150mm";
    }

}

二級頂

public class LevelTwoCeiling  implements Matter {

    public String scene() {
        return "吊頂";
    }

    public String brand() {
        return "裝修公司自帶";
    }

    public String model() {
        return "二級頂";
    }

    public BigDecimal price() {
        return new BigDecimal(850);
    }

    public String desc() {
        return "兩個層次的吊頂,二級吊頂高度一般就往下吊20cm,要是層高很高,也可增加每級的厚度";
    }
    
}

2.3 塗料(coat)

多樂士

public class DuluxCoat  implements Matter {

    public String scene() {
        return "塗料";
    }

    public String brand() {
        return "多樂士(Dulux)";
    }

    public String model() {
        return "第二代";
    }

    public BigDecimal price() {
        return new BigDecimal(719);
    }

    public String desc() {
        return "多樂士是阿克蘇諾貝爾旗下的著名建築裝飾油漆品牌,產品暢銷於全球100個國家,每年全球有5000萬戶家庭使用多樂士油漆。";
    }
    
}

立邦

public class LiBangCoat implements Matter {

    public String scene() {
        return "塗料";
    }

    public String brand() {
        return "立邦";
    }

    public String model() {
        return "預設級別";
    }

    public BigDecimal price() {
        return new BigDecimal(650);
    }

    public String desc() {
        return "立邦始終以開發綠色產品、注重高科技、高品質為目標,以技術力量不斷推進科研和開發,滿足消費者需求。";
    }

}

2.4 地板(floor)

德爾

public class DerFloor implements Matter {

    public String scene() {
        return "地板";
    }

    public String brand() {
        return "德爾(Der)";
    }

    public String model() {
        return "A+";
    }

    public BigDecimal price() {
        return new BigDecimal(119);
    }

    public String desc() {
        return "DER德爾集團是全球領先的專業木地板製造商,北京2008年奧運會裝潢和公裝地板供應商";
    }
    
}

聖象

public class ShengXiangFloor implements Matter {

    public String scene() {
        return "地板";
    }

    public String brand() {
        return "聖象";
    }

    public String model() {
        return "一級";
    }

    public BigDecimal price() {
        return new BigDecimal(318);
    }

    public String desc() {
        return "聖象地板是中國地板行業著名品牌。聖象地板擁有中國馳名商標、中國名牌、國家免檢、中國環境標誌認證等多項榮譽。";
    }

}

2.5 地磚(tile)

東鵬

public class DongPengTile implements Matter {

    public String scene() {
        return "地磚";
    }

    public String brand() {
        return "東鵬瓷磚";
    }

    public String model() {
        return "10001";
    }

    public BigDecimal price() {
        return new BigDecimal(102);
    }

    public String desc() {
        return "東鵬瓷磚以品質鑄就品牌,科技推動品牌,口碑傳播品牌為宗旨,2014年品牌價值132.35億元,位列建陶行業榜首。";
    }

}

馬可波羅

public class MarcoPoloTile implements Matter {

    public String scene() {
        return "地磚";
    }

    public String brand() {
        return "馬可波羅(MARCO POLO)";
    }

    public String model() {
        return "預設";
    }

    public BigDecimal price() {
        return new BigDecimal(140);
    }

    public String desc() {
        return "“馬可波羅”品牌誕生於1996年,作為國內最早品牌化的建陶品牌,以“文化陶瓷”佔領市場,享有“仿古磚至尊”的美譽。";
    }

}
  • 以上就是本次裝修公司所提供的裝修配置單,接下我們會通過案例去使用不同的物料組合出不同的套餐服務。

五、用一坨坨程式碼實現

講道理沒有ifelse解決不了的邏輯,不行就在加一行!

每一個章節中我們都會使用這樣很直白的方式去把功能實現出來,在通過設計模式去優化完善。這樣的程式碼結構也都是非常簡單的,沒有複雜的類關係結構,都是直來直去的程式碼。除了我們經常強調的這樣的程式碼不能很好的擴充套件外,做一些例子demo工程還是可以的。

1. 工程結構

itstack-demo-design-3-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── DecorationPackageController.java

一個類幾千行的程式碼你是否見過,嚯?那今天就讓你見識一下有這樣潛質的類!

2. ifelse實現需求

public class DecorationPackageController {

    public String getMatterList(BigDecimal area, Integer level) {

        List<Matter> list = new ArrayList<Matter>(); // 裝修清單
        BigDecimal price = BigDecimal.ZERO;          // 裝修價格

        // 豪華歐式
        if (1 == level) {

            LevelTwoCeiling levelTwoCeiling = new LevelTwoCeiling(); // 吊頂,二級頂
            DuluxCoat duluxCoat = new DuluxCoat();                   // 塗料,多樂士
            ShengXiangFloor shengXiangFloor = new ShengXiangFloor(); // 地板,聖象

            list.add(levelTwoCeiling);
            list.add(duluxCoat);
            list.add(shengXiangFloor);

            price = price.add(area.multiply(new BigDecimal("0.2")).multiply(levelTwoCeiling.price()));
            price = price.add(area.multiply(new BigDecimal("1.4")).multiply(duluxCoat.price()));
            price = price.add(area.multiply(shengXiangFloor.price()));

        }

        // 輕奢田園
        if (2 == level) {

            LevelTwoCeiling levelTwoCeiling = new LevelTwoCeiling(); // 吊頂,二級頂
            LiBangCoat liBangCoat = new LiBangCoat();                // 塗料,立邦
            MarcoPoloTile marcoPoloTile = new MarcoPoloTile();       // 地磚,馬可波羅

            list.add(levelTwoCeiling);
            list.add(liBangCoat);
            list.add(marcoPoloTile);

            price = price.add(area.multiply(new BigDecimal("0.2")).multiply(levelTwoCeiling.price()));
            price = price.add(area.multiply(new BigDecimal("1.4")).multiply(liBangCoat.price()));
            price = price.add(area.multiply(marcoPoloTile.price()));

        }

        // 現代簡約
        if (3 == level) {

            LevelOneCeiling levelOneCeiling = new LevelOneCeiling();  // 吊頂,二級頂
            LiBangCoat liBangCoat = new LiBangCoat();                 // 塗料,立邦
            DongPengTile dongPengTile = new DongPengTile();           // 地磚,東鵬

            list.add(levelOneCeiling);
            list.add(liBangCoat);
            list.add(dongPengTile);

            price = price.add(area.multiply(new BigDecimal("0.2")).multiply(levelOneCeiling.price()));
            price = price.add(area.multiply(new BigDecimal("1.4")).multiply(liBangCoat.price()));
            price = price.add(area.multiply(dongPengTile.price()));
        }

        StringBuilder detail = new StringBuilder("\r\n-------------------------------------------------------\r\n" +
                "裝修清單" + "\r\n" +
                "套餐等級:" + level + "\r\n" +
                "套餐價格:" + price.setScale(2, BigDecimal.ROUND_HALF_UP) + " 元\r\n" +
                "房屋面積:" + area.doubleValue() + " 平米\r\n" +
                "材料清單:\r\n");

        for (Matter matter: list) {
            detail.append(matter.scene()).append(":").append(matter.brand()).append("、").append(matter.model()).append("、平米價格:").append(matter.price()).append(" 元。\n");
        }

        return detail.toString();

    }

}
  • 首先這段程式碼所要解決的問題就是接收入參;裝修面積(area)、裝修等級(level),根據不同型別的裝修等級選擇不同的材料。
  • 其次在實現過程中可以看到每一段if塊裡,都包含著不通的材料(吊頂,二級頂、塗料,立邦、地磚,馬可波羅),最終生成裝修清單和裝修成本。
  • 最後提供獲取裝修詳細資訊的方法,返回給呼叫方,用於知道裝修清單。

3. 測試驗證

接下來我們通過junit單元測試的方式驗證介面服務,強調日常編寫好單測可以更好的提高系統的健壯度。

編寫測試類:

@Test
public void test_DecorationPackageController(){
    DecorationPackageController decoration = new DecorationPackageController();
    // 豪華歐式
    System.out.println(decoration.getMatterList(new BigDecimal("132.52"),1));
    // 輕奢田園
    System.out.println(decoration.getMatterList(new BigDecimal("98.25"),2));
    // 現代簡約
    System.out.println(decoration.getMatterList(new BigDecimal("85.43"),3));
}

結果:

-------------------------------------------------------
裝修清單
套餐等級:1
套餐價格:198064.39 元
房屋面積:132.52 平米
材料清單:
吊頂:裝修公司自帶、二級頂、平米價格:850 元。
塗料:多樂士(Dulux)、第二代、平米價格:719 元。
地板:聖象、一級、平米價格:318 元。


-------------------------------------------------------
裝修清單
套餐等級:2
套餐價格:119865.00 元
房屋面積:98.25 平米
材料清單:
吊頂:裝修公司自帶、二級頂、平米價格:850 元。
塗料:立邦、預設級別、平米價格:650 元。
地磚:馬可波羅(MARCO POLO)、預設、平米價格:140 元。


-------------------------------------------------------
裝修清單
套餐等級:3
套餐價格:90897.52 元
房屋面積:85.43 平米
材料清單:
吊頂:裝修公司自帶、一級頂、平米價格:260 元。
塗料:立邦、預設級別、平米價格:650 元。
地磚:東鵬瓷磚、10001、平米價格:102 元。


Process finished with exit code 0
  • 看到輸出的這個結果,已經很有裝修公司提供報價單的感覺了。以上這段使用ifelse方式實現的程式碼,目前已經滿足的我們的也許功能。但隨著老闆對業務的快速發展要求,會提供很多的套餐針對不同的戶型。那麼這段實現程式碼將迅速擴增到幾千行,甚至在修修改改中,已經像膏藥一樣難以維護。

六、建造者模式重構程式碼

接下來使用建造者模式來進行程式碼優化,也算是一次很小的重構。

建造者模式主要解決的問題是在軟體系統中,有時候面臨著"一個複雜物件"的建立工作,其通常由各個部分的子物件用一定的過程構成;由於需求的變化,這個複雜物件的各個部分經常面臨著重大的變化,但是將它們組合在一起的過程卻相對穩定。

這裡我們會把構建的過程交給建立者類,而建立者通過使用我們的構建工具包,去構建出不同的裝修套餐

1. 工程結構

itstack-demo-design-3-02
└── src
    ├── main
    │   └── java
    │       └── org.itstack.demo.design
    │           ├── Builder.java    
    │           ├── DecorationPackageMenu.java
    │           └── IMenu.java 
    └── test
         └── java
             └── org.itstack.demo.design.test
                 └── ApiTest.java

建造者模型結構

建造者模型結構

工程中有三個核心類和一個測試類,核心類是建造者模式的具體實現。與ifelse實現方式相比,多出來了兩個二外的類。具體功能如下;

  • Builder,建造者類具體的各種組裝由此類實現。
  • DecorationPackageMenu,是IMenu介面的實現類,主要是承載建造過程中的填充器。相當於這是一套承載物料和建立者中間銜接的內容。

,那麼接下來會分別講解幾個類的具體實現。

2. 程式碼實現

2.1 定義裝修包介面

public interface IMenu {

    IMenu appendCeiling(Matter matter); // 吊頂

    IMenu appendCoat(Matter matter);    // 塗料

    IMenu appendFloor(Matter matter);   // 地板

    IMenu appendTile(Matter matter);    // 地磚

    String getDetail();                 // 明細 

}
  • 介面類中定義了填充各項物料的方法;吊頂塗料地板地磚,以及最終提供獲取全部明細的方法。

2.2 裝修包實現

public class DecorationPackageMenu implements IMenu {

    private List<Matter> list = new ArrayList<Matter>();  // 裝修清單
    private BigDecimal price = BigDecimal.ZERO;      // 裝修價格

    private BigDecimal area;  // 面積
    private String grade;     // 裝修等級;豪華歐式、輕奢田園、現代簡約

    private DecorationPackageMenu() {
    }

    public DecorationPackageMenu(Double area, String grade) {
        this.area = new BigDecimal(area);
        this.grade = grade;
    }

    public IMenu appendCeiling(Matter matter) {
        list.add(matter);
        price = price.add(area.multiply(new BigDecimal("0.2")).multiply(matter.price()));
        return this;
    }

    public IMenu appendCoat(Matter matter) {
        list.add(matter);
        price = price.add(area.multiply(new BigDecimal("1.4")).multiply(matter.price()));
        return this;
    }

    public IMenu appendFloor(Matter matter) {
        list.add(matter);
        price = price.add(area.multiply(matter.price()));
        return this;
    }

    public IMenu appendTile(Matter matter) {
        list.add(matter);
        price = price.add(area.multiply(matter.price()));
        return this;
    }

    public String getDetail() {

        StringBuilder detail = new StringBuilder("\r\n-------------------------------------------------------\r\n" +
                "裝修清單" + "\r\n" +
                "套餐等級:" + grade + "\r\n" +
                "套餐價格:" + price.setScale(2, BigDecimal.ROUND_HALF_UP) + " 元\r\n" +
                "房屋面積:" + area.doubleValue() + " 平米\r\n" +
                "材料清單:\r\n");

        for (Matter matter: list) {
            detail.append(matter.scene()).append(":").append(matter.brand()).append("、").append(matter.model()).append("、平米價格:").append(matter.price()).append(" 元。\n");
        }

        return detail.toString();
    }

}
  • 裝修包的實現中每一個方法都會了 this,也就可以非常方便的用於連續填充各項物料。
  • 同時在填充時也會根據物料計算平米數下的報價,吊頂和塗料按照平米數適量乘以常熟計算。
  • 最後同樣提供了統一的獲取裝修清單的明細方法。

2.3 建造者方法

public class Builder {

    public IMenu levelOne(Double area) {
        return new DecorationPackageMenu(area, "豪華歐式")
                .appendCeiling(new LevelTwoCeiling())    // 吊頂,二級頂
                .appendCoat(new DuluxCoat())             // 塗料,多樂士
                .appendFloor(new ShengXiangFloor());     // 地板,聖象
    }

    public IMenu levelTwo(Double area){
        return new DecorationPackageMenu(area, "輕奢田園")
                .appendCeiling(new LevelTwoCeiling())   // 吊頂,二級頂
                .appendCoat(new LiBangCoat())           // 塗料,立邦
                .appendTile(new MarcoPoloTile());       // 地磚,馬可波羅
    }

    public IMenu levelThree(Double area){
        return new DecorationPackageMenu(area, "現代簡約")
                .appendCeiling(new LevelOneCeiling())   // 吊頂,二級頂
                .appendCoat(new LiBangCoat())           // 塗料,立邦
                .appendTile(new DongPengTile());        // 地磚,東鵬
    }

}
  • 建造者的使用中就已經非常容易了,統一的建造方式,通過不同物料填充出不同的裝修風格;豪華歐式輕奢田園現代簡約,如果將來業務擴充套件也可以將這部分內容配置到資料庫自動生成。但整體的思想還可以使用建立者模式進行搭建。

3. 測試驗證

編寫測試類:

@Test
public void test_Builder(){
    Builder builder = new Builder();
    // 豪華歐式
    System.out.println(builder.levelOne(132.52D).getDetail());
    // 輕奢田園
    System.out.println(builder.levelTwo(98.25D).getDetail());
    // 現代簡約
    System.out.println(builder.levelThree(85.43D).getDetail());
}

結果:

-------------------------------------------------------
裝修清單
套餐等級:豪華歐式
套餐價格:198064.39 元
房屋面積:132.52 平米
材料清單:
吊頂:裝修公司自帶、二級頂、平米價格:850 元。
塗料:多樂士(Dulux)、第二代、平米價格:719 元。
地板:聖象、一級、平米價格:318 元。


-------------------------------------------------------
裝修清單
套餐等級:輕奢田園
套餐價格:119865.00 元
房屋面積:98.25 平米
材料清單:
吊頂:裝修公司自帶、二級頂、平米價格:850 元。
塗料:立邦、預設級別、平米價格:650 元。
地磚:馬可波羅(MARCO POLO)、預設、平米價格:140 元。


-------------------------------------------------------
裝修清單
套餐等級:現代簡約
套餐價格:90897.52 元
房屋面積:85.43 平米
材料清單:
吊頂:裝修公司自帶、一級頂、平米價格:260 元。
塗料:立邦、預設級別、平米價格:650 元。
地磚:東鵬瓷磚、10001、平米價格:102 元。
       

Process finished with exit code 0
  • 測試結果是一樣的,呼叫方式也基本類似。但是目前的程式碼結構卻可以讓你很方便的很有調理的進行擴充套件業務開發。而不是以往一樣把所有程式碼都寫到ifelse裡面。

七、總結

  • 通過上面對建造者模式的使用,已經可以摸索出一點心得。那就是什麼時候會選擇這樣的設計模式,當:一些基本物料不會變,而其組合經常變化的時候,就可以選擇這樣的設計模式來構建程式碼。
  • 此設計模式滿足了單一職責原則以及可複用的技術、建造者獨立、易擴充套件、便於控制細節風險。但同時當出現特別多的物料以及很多的組合後,類的不斷擴充套件也會造成難以維護的問題。但這種設計結構模型可以把重複的內容抽象到資料庫中,按照需要配置。這樣就可以減少程式碼中大量的重複。
  • 設計模式能帶給你的是一些思想,但在平時的開發中怎麼樣清晰的提煉出符合此思路的建造模組,是比較難的。需要經過一些鍛鍊和不斷承接更多的專案,從而獲得這部分經驗。有的時候你的程式碼寫的好,往往是倒逼的,複雜的業務頻繁的變化,不斷的挑戰!

八、推薦閱讀

相關文章