今天要來聊聊 Creational Patten 當中的工廠模式。
當我們透過類別建立出實例的時候,其實感覺就像是一個工廠生產出了產品。而同一個工廠 (類別),可以生產出無限多個同樣者產品 (實例)。
回到我們先前的例子,這裡的 BaseballPlayer
和 TennisPlayer
類別繼承了 Athlete
,並能夠各自產出各自的實例,並且針對 hit
方法有不同的實作方式。
abstract class Athlete {
constructor() {}
abstract hit(): void;
}
class BaseballPlayer extends Athlete {
hit() {
console.log('Baseball player can hit baseball')
}
}
class TennisPlayer extends Athlete {
hit() {
console.log('Tennis player can hit tennis')
}
}
const jeter = new BaseballPlayer()
const federer = new TennisPlayer()
jeter.hit() // Baseball player can hit baseball
federer.hit() // Tennis player can hit tennis
但如果我們希望能夠有個 AthleteFactory
,像是中央生產工廠,不管我想要 baseball player 還是 tennis player,找它就可以生產了
於是,這裡就出現了一個簡單的解法:建立一個靜態類別AthleteFactory
,並且有一個靜態方法 trainAthlete
,可以根據使用者的輸入,來判斷要提供什麼樣的產品
class AthleteFactory {
static trainAthlete(category: string): Athlete {
switch (category) {
case 'baseball':
return new BaseballPlayer()
case 'tennis':
return new TennisPlayer()
default:
return null
}
}
}
所以如果我想要一位 baseball player,就在呼叫方法的時候,輸入 'baseball'。想要一位 tennis player 的時候,就輸入 'tennis',就能得到期待中的結果。
const ohtani = AthleteFactory.trainAthlete('baseball')
const nadal = AthleteFactory.trainAthlete('tennis')
ohtani.hit() // Baseball player can hit baseball
nadal.hit() // Tennis player can hit tennis
另一方面,如果想要持續增加不同的產品,只要在 AthleteFactory
當中持續擴充 switch 區塊就行!
使用者能夠快速從工廠取出需要的實例來使用,不需要自己去找目標類別來建立實例。另一方面,使用者也不需要管這些實例是怎麼被建立的,只要負責使用就行。
簡單工廠的實作方式簡單好懂,能夠快速上手(平常可能不知不覺就採用了簡單工廠模式)。
不過缺點就是,如果要新增一個新的商品,就必須要回去修改 AthleteFactory
當中的程式碼,有可能會影響到舊有的程式碼。另一方面,簡單工廠集中了所有建立實例的邏輯與責任,一旦不能正常運作,那麼所有產品都會受到影響。
所以,與其讓某個實際的工廠擁有所有生產產品的邏輯,不如就讓每個產品有各自的工廠,但卻有遵守相同的生產方式。
所以在工廠模式中,沒有一個中央生產的工廠,只有一個指導其他工廠如何運作的抽象介面,像是下面的 AthleteFactory
interface AthleteFactory {
trainAthlete(): Athlete;
}
接著,我們分別位 baseball player 和 tennis player 建立 BaseballPlayerFactory
和 TennisPlayerFactory
工廠,並且執行 AthleteFactory
介面
class BaseballPlayerFactory implements AthleteFactory {
constructor() {}
trainAthlete() {
return new BaseballPlayer()
}
}
class TennisPlayerFactory implements AthleteFactory {
constructor() {}
trainAthlete() {
return new TennisPlayer()
}
}
最後,如果我們希望生產 baseball player,就建立一個 baseballPlayerFactory
;如果我們希望生產 tennis player,就建立一個 tennisPlayerFactory
const baseballPlayerFactory = new BaseballPlayerFactory()
const tennisPlayerFactory = new TennisPlayerFactory()
接著,兩者可以用同樣的方式,建立需要的實例,得到預期中的結果
const ohtani = baseballPlayerFactory.trainAthlete()
const nadal = tennisPlayerFactory.trainAthlete()
ohtani.hit() // Baseball player can hit baseball
nadal.hit() // Tennis player can hit tennis
有些時候我們因應外在環境的變動,需要產出類似的、但是又截然不同的實例(譬如棒球選手和網球選手),但又要避免像簡單工廠那樣,因集中生產所造成的維護和擴充問題,最後這裡我們透過「抽象」的方式來解決問題。
首先是建立一個介面(或是類別也可以)來定義各個工廠的實作要求 (譬如 trainAthlete
方法),但是不定義詳細的實作方式。
接著,讓不同的產品建立相對應不同的工廠,並各自執行 trainAthlete
方法。譬如 BaseballPlayerFactory
的做法就是呼叫 new BaseballPlayer()
。
最後,使用者根據需求,呼叫需要的工廠來生產實例。
相對於簡單工廠模式,工廠模式的好處是滿足了「單一功能原則」,讓 AthleteFactory
只管要有什麼方法,而不用管實作細節;也滿足了「開放封閉原則」,未來有任何新的需求出現,只要按照介面(或類別)設計新的工程即可,大大提升了維護性和擴充性。
不過缺點就是,每當有新需求出現,我們就需要建立一個新的工廠 (e.g., xxxFactory
),以及負責生產實例的類別,兩者會成雙成對出現。