Java設計模式14:建造者模式

  • 2019 年 10 月 3 日
  • 筆記

什麼是建造者模式

發現很多框架的源碼使用了建造者模式,看了一下覺得挺實用的,就寫篇文章學習一下,順便分享給大家。

建造者模式是什麼呢?用一句話概括就是建造者模式的目的是為了分離對象的屬性與創建過程,是的,只要記住並理解紅字的幾個部分,建造者模式你就懂了。

 

為什麼需要建造者模式

建造者模式是構造方法的一種替代方案,為什麼需要建造者模式,我們可以想,假設有一個對象裡面有20個屬性:

  • 屬性1
  • 屬性2
  • 屬性20

對開發者來說這不是瘋了,也就是說我要去使用這個對象,我得去了解每個屬性的含義,然後在構造函數或者Setter中一個一個去指定。更加複雜的場景是,這些屬性之間是有關聯的,比如屬性1=A,那麼屬性2隻能等於B/C/D,這樣對於開發者來說更是增加了學習成本,開源產品這樣的一個對象相信不會有太多開發者去使用。

為了解決以上的痛點,建造者模式應運而生,對象中屬性多,但是通常重要的只有幾個,因此建造者模式會讓開發者指定一些比較重要的屬性或者讓開發者指定某幾個對象類型,然後讓建造者去實現複雜的構建對象的過程,這就是對象的屬性與創建分離。這樣對於開發者而言隱藏了複雜的對象構建細節,降低了學習成本,同時提升了程式碼的可復用性。

雖然感覺基本說清楚了,但還是有點理論,具體往下看一下例子。

 

建造者模式程式碼示例

舉一個實際場景的例子:

大家知道一輛車是很複雜的,有發動機、變速器、輪胎、擋風玻璃、雨刮器、氣缸、方向盤等等無數的部件。    用戶買車的時候不可能一個一個去指定我要那種類型的變速器、我要一個多大的輪胎、我需要長寬高多少的車,這是不現實的    通常用戶只會和銷售談我需要什麼什麼樣的類型的車,馬力要不要強勁、空間是否寬敞,這樣銷售就會根據用戶的需要去推薦一款具體的車    這就是一個典型建造者的場景:車是複雜對象,銷售是建造者。我告訴建造者我需要什麼,建造者根據我的需求給我一個具體的對象

根據這個例子,我們定義一個簡單的汽車對象:

 1 public class Car {   2   3     // 尺寸   4     private String size;   5   6     // 方向盤   7     private String steeringWheel;   8   9     // 底座  10     private String pedestal;  11  12     // 輪胎  13     private String wheel;  14  15     // 排量  16     private String displacement;  17  18     // 最大速度  19     private String maxSpeed;  20  21     public String getSize() {  22         return size;  23     }  24  25     public void setSize(String size) {  26         this.size = size;  27     }  28  29     public String getSteeringWheel() {  30         return steeringWheel;  31     }  32  33     public void setSteeringWheel(String steeringWheel) {  34         this.steeringWheel = steeringWheel;  35     }  36  37     public String getPedestal() {  38         return pedestal;  39     }  40  41     public void setPedestal(String pedestal) {  42         this.pedestal = pedestal;  43     }  44  45     public String getWheel() {  46         return wheel;  47     }  48  49     public void setWheel(String wheel) {  50         this.wheel = wheel;  51     }  52  53     public String getDisplacement() {  54         return displacement;  55     }  56  57     public void setDisplacement(String displacement) {  58         this.displacement = displacement;  59     }  60  61     public String getMaxSpeed() {  62         return maxSpeed;  63     }  64  65     public void setMaxSpeed(String maxSpeed) {  66         this.maxSpeed = maxSpeed;  67     }  68  69     @Override  70     public String toString() {  71         return "Car [size=" + size + ", steeringWheel=" + steeringWheel + ", pedestal=" + pedestal + ", wheel=" + wheel  72             + ", displacement=" + displacement + ", maxSpeed=" + maxSpeed + "]";  73     }  74  75 }

這裡簡單定義幾個參數,然後建造者對象應運而生:

public class CarBuilder {        // 車型      private String type;        // 動力      private String power;        // 舒適性      private String comfort;        public Car build() {          Assert.assertNotNull(type);          Assert.assertNotNull(power);          Assert.assertNotNull(comfort);            return new Car(this);      }        public String getType() {          return type;      }        public CarBuilder type(String type) {          this.type = type;          return this;      }        public String getPower() {          return power;      }        public CarBuilder power(String power) {          this.power = power;          return this;      }        public String getComfort() {          return comfort;      }        public CarBuilder comfort(String comfort) {          this.comfort = comfort;          return this;      }        @Override      public String toString() {          return "CarBuilder [type=" + type + ", power=" + power + ", comfort=" + comfort + "]";      }    }

說是建造者,其實也不合適,它只是一個中間對象,用於接收來自外部的資訊,比如需要什麼樣的車型,需要什麼樣的動力啊這些。

然後大家一定注意到了build方法,這個是建造者模式好像約定俗成的方法名,代表建造,裡面把自身對象傳給Car,這個構造方法的實現我在第一段程式碼裡面是沒有貼的,這段程式碼的實現為:

public Car(CarBuilder builder) {      if ("緊湊型車".equals(builder.getType())) {          this.size = "大小--緊湊型車";      } else if ("中型車".equals(builder.getType())) {          this.size = "大小--中型車";      } else {          this.size = "大小--其他";      }        if ("很舒適".equals(builder.getComfort())) {          this.steeringWheel = "方向盤--很舒適";          this.pedestal = "底座--很舒適";      } else if ("一般舒適".equals(builder.getComfort())) {          this.steeringWheel = "方向盤--一般舒適";          this.pedestal = "底座--一般舒適";      } else {          this.steeringWheel = "方向盤--其他";          this.pedestal = "底座--其他";      }        if ("動力強勁".equals(builder.getPower())) {          this.displacement = "排量--動力強勁";          this.maxSpeed = "最大速度--動力強勁";          this.steeringWheel = "輪胎--動力強勁";      } else if ("動力一般".equals(builder.getPower())) {          this.displacement = "排量--動力一般";          this.maxSpeed = "最大速度--動力一般";          this.steeringWheel = "輪胎--動力一般";      } else {          this.displacement = "排量--其他";          this.maxSpeed = "最大速度--其他";          this.steeringWheel = "輪胎--其他";      }  }

這是真實構建對象的地方,無論多複雜的邏輯都在這裡實現而不需要暴露給開發者,還是那句核心的話:實現了對象的屬性與構建的分離

這樣用起來就很簡單了:

@Test  public void test() {      Car car = new CarBuilder().comfort("很舒適").power("動力一般").type("緊湊型車").build();        System.out.println(JSON.toJSONString(car));  }

只需要指定我需要什麼什麼類型的車,然後具體的每個參數自然根據我的需求列出來了,不需要知道每個細節,我也能得到我需要的東西。

 

建造者模式在開源框架中的應用

文章的開頭有說很多開源框架使用了建造者模式,典型的有Guava的Cache、ImmutableMap,不過感覺MyBatis更為大家熟知,且MyBatis內部大量使用了建造者模式,我們可以一起來看一下。

以原生的MyBatis(即不使用Spring框架進行整合)為例,通常使用MyBatis我們會用以下幾句程式碼:

// MyBatis配置文件路徑  String resources = "mybatis_config.xml";  // 獲取一個輸入流  Reader reader = Resources.getResourceAsReader(resources);  // 獲取SqlSessionFactory  SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);  // 打開一個會話  SqlSession sqlSession = sqlSessionFactory.openSession();  // 具體操作  ...

關鍵我們看就是這個SqlSessionFactoryBuilder,它的源碼核心方法實現為:

public class SqlSessionFactoryBuilder {      ...      public SqlSessionFactory build(Reader reader, String environment, Properties properties) {      try {        XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);        return build(parser.parse());      } catch (Exception e) {        throw ExceptionFactory.wrapException("Error building SqlSession.", e);      } finally {        ErrorContext.instance().reset();        try {          reader.close();        } catch (IOException e) {          // Intentionally ignore. Prefer previous error.        }      }    }      ...      public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {      try {        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);        return build(parser.parse());      } catch (Exception e) {        throw ExceptionFactory.wrapException("Error building SqlSession.", e);      } finally {        ErrorContext.instance().reset();        try {          inputStream.close();        } catch (IOException e) {          // Intentionally ignore. Prefer previous error.        }      }    }      ...    }

因為MyBatis內部是很複雜的,核心類Configuration屬性多到爆炸,比如拿資料庫連接池來說好了,有POOLED、UNPOOLED、JNDI三種,然後POOLED裡面呢又有各種超時時間、連接池數量的設置,這一個一個都要讓開發者去設置那簡直要命了。因此MyBatis在SqlSessionFactory這一層使用了Builder模式,對開發者隱藏了XML文件解析細節,Configuration內部每個屬性賦值細節,開發者只需要指定一些必要的參數(比如資料庫地址、用戶名密碼之類的),就可以直接使用MyBatis了,至於可選參數,配置了就拿開發者配置的,沒有配置就默認來一套。

通過這樣一種方式,開發者接入MyBatis的成本被降到了最低,這麼一種編程方式非常值得大家學習,尤其是自己需要寫一些框架的時候。

同樣的大家可以看一下Environment,Environment也使用了建造者模式,但是Environment使用建造者模式最大的作用是讓用戶無法在運行時修改任何環境屬性保證了安全與穩定性,同樣這也是建造者模式的一種經典實現。

 

建造者模式的類關係圖

其實,建造者模式不像一些設計模式有比較固定或者比較類似的實現方式,它的核心只是分離對象屬性與創建,整個實現比較自由,我們可以看到我自己寫的造車的例子和SqlSessionFactoryBuilder就明顯不是一種實現方式。

看了一些框架源碼總結起來,建造者模式的實現大致有兩種寫法:

這是一種在Builder裡面直接new對象的方式,MyBatis的SqlSessionFactoryBuilder就是這種寫法,適用於屬性之間關聯不多且大量屬性都有默認值的場景

另外一種就是間接new的方式了:

我的程式碼示例,還有例如Guava的Cache都是這種寫法,適用於屬性之間有一定關聯性的場景,例如車的長寬高與軸距都屬於車型一類、排量與馬力都與性能相關,可以把某幾個屬性歸類,然後讓開發者指定大類即可。

總體而言,兩種沒有太大的優劣之分,在合適的場景下選擇合適的寫法就好了。

 

建造者模式的優點及適用場景

建造者模式這種設計模式,優缺點比較明顯。從優點來說:

  • 客戶端不比知道產品內部細節,將產品本身與產品創建過程解耦,使得相同的創建過程可以創建不同的產品對象
  • 可以更加精細地控制產品的創建過程,將複雜對象分門別類抽出不同的類別來,使得開發者可以更加方便地得到想要的產品

想了想,說缺點,建造者模式說不上缺點,只能說這種設計模式的使用比較受限:

  • 產品屬性之間差異很大且屬性沒有默認值可以指定,這種情況是沒法使用建造者模式的,我們可以試想,一個對象20個屬性,彼此之間毫無關聯且每個都需要手動指定,那麼很顯然,即使使用了建造者模式也是毫無作用

總的來說,在IT這個行業,複雜的需求、複雜的業務邏輯層出不窮,這必然導致複雜的業務對象的增加,建造者模式是非常有用武之地的。合理分析場景,在合適的場景下使用建造者模式,一定會使得你的程式碼漂亮得多。