Builder Pattern(生成器)

前言

這是在讀書會當主講時負責的 builder pattern,因為本人對 OOP 以及 design pattern 並沒有到非常熟悉,內容非常可能有諸多錯誤,因此僅供參考,如果有錯歡迎指正,感謝!

情境

假設今天你是建商,要替客戶蓋房子,但房子會有各式各樣的要求,比如會有需要泳池的、需要雕像的、需要車庫的房子等等,也就是說生產產品時,你會有各式各樣客製化的需求。

而為了達成這個需求,你可能會有幾種解法,下面是可能的解法:

想法一:建立一堆 subclass

辦法就是基於 house 這個 class 去 extends 很多 subclass,也就是說會有很多客製化的房子會繼承 house 這個 class,程式碼可能會像是這樣:

class House {
  // 房子
}

class HouseWithSwimPool extends House {
  // 有游泳池的房子
}

class HouseWithStatues extends House {
  // 有雕像的房子
}

class HouseWithGardens extends House {
  // 有花園的房子
}

但這方法不是很理想,因為當你需要客製化的房子很多種類時,就要跟著寫一堆 subclass,擴充起來非常的麻煩。

想法二:用參數來決定

既然 subclass 很麻煩,那我們改用 constructor 裡面的參數來客製化房子吧!辦法就是在 new 一個新的 House 時,透過參數來決定客製化的內容是甚麼,程式碼可能會像是這樣:

class House {
    constructor(
      windows: number, 
      doors: number, 
      rooms: number, 
      hasGarage: boolean, 
      hasSwimPool: boolean, 
      hasStatues: boolean) {
    }
}

const aHouse = new House(4, 4, 1, true, true, true)

看起來是一定程度達成了需求,但會有甚麼問題呢?

可以發現到每 new 一個 House 就要傳非常多參數進去,假設今天有 10 個參數,new 的時候就要傳 10 個;又或者今天你只想要第 1 和第 10 個參數,中間 2 - 8 的參數你就只能傳 null,不得不給它一個值,但其實 2 - 8 的參數你是不需要的,也就是說每 new 一個 House 就會變得頗為麻煩。

想法三:Builder Pattern

接著就輪到我們 Builder Pattern 出場的時機了,我們不用 subclass,也不用參數來決定客製化的細節,而是把每個客製化的細節拆分成一個一個的 method,在調整完客製化的內容之後,再呼叫 getResult 來取得客製化的結果。

Builder Pattern 程式碼示範呢?

來惹來惹,熱騰騰的範例來惹。

Builder Pattern 的 UML

單純用文字講解有點不夠力,要理解 Builder Pattern 來看程式碼會更快一點!首先來看一下 Builder Pattern 的 UML 圖:

從上圖可以看到關於 Builder Pattern 我們需要幾個東西,

  1. Builder 的 Interface:定義 Builder class 需要實作的 method。
  2. Builder 的 class:建造客製化產品的 Class,裡面會實作 Builder Interface 的客製化 method。
  3. Director:屬於 optional 的 class,在 Builder Pattern 裡面可用可不用,用處是把常用的建造產品步驟寫成一個 method,而後再使用 builder 快速建立出常用的產品。
  4. Product 的 class:要生產的客製化產品。

可以看到總共需要有三個 class,一個 interface,接著我們就一個一個來實作,更仔細探討每個 class 的用途及細節吧!

既然我們剛剛的情境是擔任建商,那我們就先看看需要什麼:

  1. IHouseBuilder:HouseBuilder 的 Interface
  2. HouseBuilder:房子的建造者
  3. Director:指揮 HouseBuilder 進行一連串步驟蓋出房子的 class。
  4. House:要客製化的房子。

既然知道需要什麼了,就來動手實作吧!

實作:House Class

首先來到我們想要製造的產品 — House

// 要製造的 product
class House {
  public windows: number = 2
  public walls: number = 4
  public doors: number = 1
  public rooms: number = 2
  public hasGarage: boolean = false
  public hasSwimPool: boolean = false
  public hasStatue: boolean = false

  constructor(windows: number, walls: number) {
    this.windows = windows
    this.walls = walls
  }
  
  // 為了待會列出調整完客製化細節而使用的 console.log,實際上不需要
  public listParts() {
    console.log({
      window: this.windows,
      walls: this.walls,
      doors: this.doors,
      rooms: this.rooms,
      hasGarage: this.hasGarage,
      hasSwimPool: this.hasSwimPool,
      hasStatue: this.hasStatue
    })
  }
}

從 House 的 class 能看到內容就是各種可以調整的 member,以及為了 console 出 member 所使用的 method。

實作:IHouseBuilder

再來輪到了 IHouseBuilder,這個 interface 的用途是用來定義 HouseBuilder 需要實作什麼 method 的。

// builder 的 interface
interface IHouseBuilder {
  reset(windows: number, walls: number): void
  buildDoors(doors: number): IHouseBuilder
  buildRooms(rooms: number): IHouseBuilder
  buildGarage(): IHouseBuilder
  buildSwimPool(): IHouseBuilder
  buildStatue(): IHouseBuilder
  getResult(): House
}

可以看到這邊定義了 reset,目的是建造完產品後可以歸零,以便進行下一次的生產;buildDoorsbuildRooms 等等的 method,則是我們要客製化需要使用的 method,讓我們能夠調整 House 的細節;getResult 則是調整完各種東西後,呼叫它能夠讓我們得到客製化後的 House

實作:HouseBuilder

接著來到我們的主角,House Builder


// house builder
class HouseBuilder implements IHouseBuilder {
  private house: House;
  
  constructor(windows: number, walls: number) {
    this.house = new House(windows, walls)
    this.reset()
  }

  /* 重置 this.house,以便下一次的 build */
  reset() {
    this.house = new House(2, 4)
  }

  /**
   * 客製化 product 用的 method 們
   * 另外,return this 的原因是方便可以用 chain 的方式來呼叫 method,
   * 這個方式叫做 Fluent interface(流暢介面)
   */ 
  buildDoors(doors: number) {
    this.house.doors = doors
    return this
  }

  buildRooms(rooms: number) {
    this.house.rooms = rooms
    return this
  }

  buildGarage() {
    this.house.hasGarage = true
    return this
  }

  buildSwimPool() {
    this.house.hasSwimPool = true
    return this
  }

  buildStatue() {
    this.house.hasStatue = true
    return this
  }
  /**
   * getResult 會 return 客製化完成的 product,並且 reset,
   * 以便下一次客製化 product。
  */
  getResult() {
    const result = this.house
    this.reset()
    return result
  }
}

HosueBuilder 有幾個重點可以注意:

  1. reset:每次客製化完,並執行 getResult 後,通常都會執行 reset,得到初始的 House,以便進行下一次的客製化生產。
  2. buildDoorsbuildStatue 等 method:客製化用的 method,這邊為了示範有簡化,實際應用會更複雜,值得注意的是一般來說在每個客製化 method 都會 return this,目的是為了可以用 chain 的方式來調用 method。(待會看範例就會懂在說什麼了)
  3. getResult:客製化完取得產品用的 method,通常會在這個地方做 reset,因為得到結果後通常就又會再進行下一次客製化,不過這只是通常,實際 reset 的時機可以看應用的情景。

實作:Director

再來輪到 Director

class Director {
  private builder: IHouseBuilder

  constructor() {
    this.builder = new HouseBuilder(2, 4)
  }

  public setBuilder(builder: HouseBuilder): void {
    this.builder = builder
  }

  public buildCastle() {
    this.builder.buildRooms(200)
    this.builder.buildDoors(1000)
    this.builder.buildStatue()
    this.builder.buildGarage()
  }

  buildApartment() {
    this.builder.buildDoors(1)
    this.builder.buildRooms(1)
  }
}

可以看到這邊 Director 的用處就是命令 Builder 來執行一連串的客製化 method,把常用的產品步驟集合成一個 method,快速建立出常用產品。

實作:實際使用

// new 出 Director 
let director = new Director()

// new 出 HouseBuilder
let houseBuilder = new HouseBuilder(2, 4)

// 建造 houseA
const houseA = houseBuilder.buildDoors(2)
      .buildGarage()
      .buildRooms(2)
      .getResult()

// 建造 hosueB
const houseB = houseBuilder.buildDoors(100)
      .buildGarage()
      .buildSwimPool()
      .buildRooms(2)
      .getResult()

// 客製化的 houseA,結果:
// {
//   window: 2,
//   walls: 4,
//   doors: 2,
//   rooms: 2,
//   hasGarage: true,
//   hasSwimPool: false,
//   hasStatue: false
// }
houseA.listParts()

// 建造客製化的 houseB,結果:
// {
//   window: 2,
//   walls: 4,
//   doors: 100,
//   rooms: 2,
//   hasGarage: true,
//   hasSwimPool: true,
//   hasStatue: false
// }
houseB.listParts()

// 使用 director,先設定 builder 為 HouseBuilder
director.setBuilder(houseBuilder)

// 接著 director 命令 builder 執行一連串客製化 method
director.buildCastle()

// 使用 houseBuilder 得到結果
const houseC = houseBuilder.getResult()

// 建造客製化的 houseC,結果:
// {
//   window: 2,
//   walls: 4,
//   doors: 1000,
//   rooms: 200,
//   hasGarage: true,
//   hasSwimPool: false,
//   hasStatue: true
// }
houseC.listParts()

實作上大概是這樣,但這邊想提一次剛剛實作 HouseBuilder 時提到每個客製化 method return this 的事情。

jQuery 用 chain 的方式來調用 function 的用法大家應該都有印象,比如可以像是這樣:

$( "button.continue" ).html( "Next Step..." ).a().b()

可以這樣使用的原因,是因為每次 jQuery 內建的方法執行完之後,都會再回傳一次 jQuery 的 element,你就可以再次使用它內建的方法了。

HouseBuilderreturn this 的原因也是一樣,當我們在客製化 method 裡面 return this 的時候:

buildGarage() {
  this.house.hasGarage = true
  return this
}

我們 return 的是 HouseBuilder 本身,而 return HouseBuilder 能做什麼呢?我們就能夠繼續 chain 下去,呼叫 buildeRoomsbuildeDoors 等 method 了!

所以實際使用就會變成像剛剛實作那樣:

const houseA = houseBuilder
      .buildDoors(2) // 執行完 return houseBuilder
      .buildGarage() // 執行完 return houseBuilder
      .buildRooms(2)  // 執行完 return houseBuilder
      .getResult() // 得到結果

應用時機

可以發現到和工廠模式不同,工廠模式著重在生產的每個產品,而 Builder 則是注重在每個產品的客製化內容,也因此當你有需要很多的客製化細節需要調整時,就可以考慮 Builder 這個 Pattern,而根據我看到的文章所說,當你發現一個 class 的 contructor 有超過四個參數時,就是使用 Builder 的時機了!

Builder 的優點顯而易見,就是我們可以更細部的調整每一個 new 出來的 instance,並且 getResult 的時機也可以適時的調整;而 Builder 缺點是什麼呢?是每個 product 的同質性要夠高,像是 IHouseBuilder 定義的方法就很侷限,幾乎和房子有關的 class 才能夠 implement,差異比較大的 class 就需要另闢蹊徑,重新寫不一樣的 builder。

現實的應用

可以參考看看 Android 的 Notification.Builder

結語

這次當 builder pattern 的主講感想是自己對 OOP 真的還很不熟,實作、概念上都是,以目前的文章而言,實作也僅止於書上紙上談兵的程度,沒有實際應用在專案的經驗,當想要講解這個 pattern 的概念給其他人的時候,會有點不太知道該用什麼樣實際的應用來讓自己以及別人知道實戰上如何使用 QQ 但能夠先知道這個 pattern 也是好的,未來看到相關應用的時候就能夠較快的理解程式碼。

Refercences

TypeScript 设计模式与重构技巧 · 建造者模式

设计模式——建造者模式(TypeScript版)

設計模式—建造者模式 (Builder Design Pattern)

秒懂设计模式之建造者模式(Builder pattern)

設計模式 - 工廠方法及抽象工廠