iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 29
2
Modern Web

讓 TypeScript 成為你全端開發的 ACE!系列 第 29

Day 29. 機動藍圖・工廠模式 X 抽象工廠 - Factory Method & Abstract Factory Pattern Using TypeScript

https://ithelp.ithome.com.tw/upload/images/20191009/20120614WvCe3QZi8y.png

閱讀本篇文章前,仔細想想看

大致上已經了解抽象類別的運用性質與情境了嗎?

另外本篇會延續前一篇的範例,除了可以參考前一篇外,筆者本篇會再進行簡單的敘述!

本篇原本也不在筆者的計畫範圍內,然而由於前一篇已經介紹了抽象類別(Abstract Class)的意義,因此想要趁此機會延伸更多抽象類別的應用,於是誕生了今天這一篇。

貼心小提示

本篇的程式碼可以參照 Maxwell-Alexius/Iron-Man-Competition 這個 Repo.。

以下正文開始

工廠模式與抽象工廠模式 Factory Method & Abstract Factory Pattern

前情提要

首先,筆者必須快速帶過今天的起始程式碼範例,本篇是延用前一篇的範例進行解說喔!

這幾天的範例都圍繞在簡單的 RPG 角色的戰鬥功能設計,基本的角色 Character 程式碼如下:

https://ithelp.ithome.com.tw/upload/images/20190927/20120614x59qqH4nBO.png

其中最需要注意的點是 —— Character 運用 weaponRef 連結 Weapon 相關的物件,目的是實現角色裝備武器的特性,同時也可以替換武器,達成選擇不同武器策略的目標。(策略模式的應用)

equip 方法就是根據被連結到的武器,判定該角色是否能夠裝備,再來更新 weaponRefattack 方法則是藉由 weaponRef 連結到的武器,將角色攻擊的機制藉此用該參考點傳遞下去。

而武器 Weapon 的介面在上一篇變成了抽象類別,因為繼承了 Weapon 的任意武器的 attackswitchAttackStrategy 的成員方法都是固定的狀態。

https://ithelp.ithome.com.tw/upload/images/20190927/20120614pGzvJ7re8s.png

筆者在角色職業的設計中,有宣告三種不同的武器:BasicSwordBasicWandDagger。因為三個武器策略的實踐都差不多,因此以下為其中一個武器的實踐:

https://ithelp.ithome.com.tw/upload/images/20190927/20120614wetfSBgl5a.png

由於本篇不會扯到跟攻擊策略 Attack 有關的類別,因此跳過。

工廠模式 Factory Method Pattern

讀者如果從策略模式的篇章,到現在會發現:

每一次要選擇某項策略時,必須要將該策略載入(import)檔案後,我們才可以使用

這樣會造就一種很恐怖的狀況:假設今天角色的武器種類有十種以上 ... 以下是莫名中二的模擬程式碼,每一次要更換新的武器必須要載入該武器,並且將該武器建構起來再代入角色的 equip 方法。

https://ithelp.ithome.com.tw/upload/images/20190927/20120614muRaZEz2e7.png

這實在是太麻煩。

但此問題的解法非常簡單,以下就示範使用 Factory Method 模式 —— 建構一個名為 WeaponFactory 的這個類別 —— 負責代表建構不同武器的工廠

但在這之前,我們先將所有武器進行列舉的動作,存在 weapons/Weapons.ts 裡面:

https://ithelp.ithome.com.tw/upload/images/20190927/201206142i32uFRdPS.png

再來可以建立 WeaponFactory,並且根據輸入的武器種類,進行回傳並建立相對的武器物件。

https://ithelp.ithome.com.tw/upload/images/20190927/20120614vykvbrGiXf.png

我們可以用簡單的程式碼進行測試。(結果如圖一)

https://ithelp.ithome.com.tw/upload/images/20190927/20120614kqVTqLhShB.png

https://ithelp.ithome.com.tw/upload/images/20190927/20120614T2TfAFDg0p.png
圖一:工廠模式其實很簡單,就是建立一個工廠負責傳入要建立物件的選項,自動建立起來

Factory Method 模式下,一個工廠類別 Factory 會對應一個 Product 類別。而這裡的 Factory 指的就是 WeaponFactory,Product 指的就是 Weapon —— 而這個名為 createWeapon 的成員方法是工廠模式裡俗稱的工廠方法 —— Factory Method

理想情形下,一個工廠只會打造出對應的一種產品,不過在 WeaponFactory 裡面,筆者在 Factory Method,也就是 createWeapon 這個成員方法裡有宣告一個參數,代表必須指名建構的武器種類,實際上這種帶有參數化的 Factory Method 是工廠模式的變體。

有些讀者可能會問:

“這種參數化的 Factory Method 模式下,WeaponFactory 在建立武器時,用了 switch...case... 的判斷敘述式了啊!這樣不是跟作者提倡的不要用太冗長的判斷過程來寫程式碼嗎?”

這裡之所以允許使用 switch...case... 或者用 if...else if...else... 的主要原因非常簡單:每一次新增一個武器時,只會在 WeaponFactory 多加一個選項,沒有任何其他地方會需要重複這個過程,因此依然符合 DRY(Don't Repeat Yourself)原則!

重點 1. 工廠模式 Factory Method Pattern

工廠模式主要是將性質相近的物件 —— 與其分別各自進行建立物件的動作,不如讓一個工廠類別匯集那群物件,讓使用者可以在統一的窗口進行建立物件的動作。

另外,在 Design Pattern 原著的書籍裡,Factory Method 模式的定義下有兩個主角:Factory 類別以及對應的 Product 類別。

而 Factory 類別裡的 createProduct 方法就是俗稱的工廠方法 —— Factory Method。

理想情況下,一個 Factory 只會建造出對應的一種 Product

但 Factory Method 的其中一種變體是:可以藉由將工廠方法宣告參數的動作,指名要建造的不同 Product 物件。

工廠模式比較直觀,因此筆者這裡就不畫出關係對應圖了。

讀者試試看

通常工廠模式下的工廠個體也不太需要建構太多次,因此可以把工廠建立成單子(Singleton),讀者有辦法將剛剛的 WeaponFactory 使用單例模式包裝單一工廠個體嗎?

抽象工廠模式 Abstract Factory Pattern

貼心小提示

抽象工廠模式應該是目前筆者認為比較難理解一點的設計模式,因為牽扯到的抽象化過程很繁複,一下子又是介面、一下子又是類別連來連去的。因此第一次看不懂沒關係,筆者很鼓勵讀者去參照各種不同的資源來回學習比對。

工廠模式抽象工廠模式的概念差別就在於 —— 抽象工廠(Abstract Factory)是對於實體工廠(Concrete Factory)進行抽象化的動作

於是讀者們開始丟雞蛋:“作者講這句廢話是欠打嗎?”

筆者真的很難簡短解釋,但仍然可以依循一些線索來理解抽象工廠模式到底在做什麼。

在開始解說之前,我們先退一步想想看:武器的種類有很多種,新增一個武器必須將武器的規格與內容實踐出來 —— 因此會需要一個 Weapon 模板,也就是一個介面亦或者是抽象類別,因此上一篇才有 Weapon 這個抽象類別讓新的武器子類別進行抽象化的動作。

然而實踐出來的武器種類實在是太多種,因此才需要工廠模式下實踐的 WeaponFactory 實體工廠 —— 作為建構武器的統一窗口 —— 幫助我們把初始化武器的細節拔出來,塞到實體工廠類別。

今日的重點問題來了。

角色如果不只是會需要裝備武器,也要裝備防具(Armour)、頭盔(Helmet)或更多部分呢?

哇,那我們可能還會需要:

  • Armour 介面 —— 底下會有很多不同的防具,以及統一建構防具的 ArmourFactory 實體工廠類別
  • Helmet 介面 —— 底下會有很多不同的頭盔,以及統一建構頭盔的 HelmetFactory 實體工廠類別
  • 更多亂七八糟的東西,像是角色可能還可以裝備手套(Gloves)、靴子(Boots)、飾品(Decorations)等等

而且我們可能想設計出特別的行為:

  • Armour 分成上半身跟下半身 —— UpperArmourLowerArmour
  • 角色一次可以裝備多個飾品 Decorations
  • 有些職業,譬如 Swordsman 除了裝備武器外,還可以裝備盾 Shield
  • 道具本身也是可以被裝備在身上的,例如生命藥水等等,這些可能還被歸類為 Props

數數看,光是實體工廠至少有五、六種以上,所以用簡單的程式碼表示可能會變成這樣:

https://ithelp.ithome.com.tw/upload/images/20190927/20120614nSPpLvGm0Y.png

WeaponArmourGlovesBoots 等等物件的聯繫 —— 都隸屬於 Character 可以裝備上去的東西

第二重點問題則是:

角色職業也分超多種,每種職業都會有對應的武器、裝備等等,這麼多種實體工廠到底該如何是好?工廠是不是需要一個統一的規格呢?

我們也有可能設計出針對不同職業對應的裝備,要是這樣設計下去,假設職業有 4 種,裝備形式有 8 種,我們就有 32 種不同的實體工廠。

因此,這裡會需要更泛用的形式 —— 統一的 Equipment 代表各種裝備以及建構各種不同 Equipment 的工廠類型,可以建立不同職業的 WeaponArmourGloveBoots 物件,這就是需要抽象工廠的主要理由!

本篇將會示範 —— 屬於 Swordsman 的裝備工廠 SwordsmanEquipmentFactory 以及 Warlock 的裝備 WarlockEquipmentFactory 的實踐。(名稱超長

第一個步驟當然是先定義好裝備到底有哪些種類:

https://ithelp.ithome.com.tw/upload/images/20190927/20120614MDndzJk1VU.png

再來是宣告 Equipment 類型的介面:

https://ithelp.ithome.com.tw/upload/images/20190927/20120614Ktki4Sa2e8.png

最後是基本的 EquipmentFactory 的工廠介面:

https://ithelp.ithome.com.tw/upload/images/20190927/20120614BEjNZwRFuK.png

從以上的程式碼 —— 原本單純只是從 Weapon 介面延伸到專門鍛造武器的 WeaponFactory,這一次為了要建構更全面性的物件:包含 WeaponArmour 這兩種皆屬於 Equipment 的物件,因此才會先宣告 Equipment 的格式以及它相對應的工廠的介面。

理所當然,本範例早就有 Weapon 的宣告,但我們還沒有宣告 Armour,這裡一定會被 TypeScript 警告。

不過這裡有個很重要的點,因為 Weapon 同時也要符合 Equipment 介面的要求,因此在 weapons/Weapon.ts 裡,我們必須將該抽象類別與 Equipment 介面進行綁定的動作(使用 implements):

https://ithelp.ithome.com.tw/upload/images/20190927/20120614h6PAI5fGI6.png

Weapon 這個抽象類別因為跟 Equipment 介面進行綁定,因此也必須要實踐 type 這個由 Equipment 介面規定的成員;略過的程式碼跟之前的實踐狀況一模ㄧ樣,不需要改變,所以就只是多了 implements Equipment 的過程罷了。

另外,我們也需要 Armour 這個類別協助創造出更多不同的防具,筆者額外再建構一個新的資料夾名為 armours,然後將該類別的程式碼宣告在 armours/Armour.ts 如下。

https://ithelp.ithome.com.tw/upload/images/20190927/20120614i3N2wBtcCx.png

筆者也簡單創造出兩種不同的防具,分別為 BasicArmourBasicRobe

https://ithelp.ithome.com.tw/upload/images/20190927/20120614eNEaqAvDa4.png

https://ithelp.ithome.com.tw/upload/images/20190927/20120614vNSwc9W58v.png

之所以只要宣告 nameavailableRoles 成員的原因是:Armour 抽象類別只要求子類別實踐這兩個抽象成員而已喔!所以每個防具的宣告才會看起來很簡短。

另外,這邊額外舉個例子:比如說如果你有宣告道具 Prop 之類的類別隸屬於 Equipment 的一種,可能除了 nameavailableRoles 以外,你還可能會定義出 useProp(使用道具)類似的成員方法。

回過頭來,原本是藉由 WeaponFactory 去創造出武器相關的物件,這一次筆者來創造專門為 Swordsman 量身打造的裝備工廠,其中該工廠必須符合 EquipmentFactory 這個介面:

https://ithelp.ithome.com.tw/upload/images/20190927/20120614BvNJqgtAYN.png

另外,Warlock 當然也會需要專屬的裝備工廠:

https://ithelp.ithome.com.tw/upload/images/20190927/20120614zsKY2sZNf7.png

既然我們都已經有各自專屬的工廠了,首先,要先更新 Character 的內容,使得 Character 可以同時裝備武器跟防具:

https://ithelp.ithome.com.tw/upload/images/20190927/20120614KNN3uQFy1l.png

這裡比較複雜的地方應該是 —— equip 方法儘管可以填入 Equipment 類型的參數,經過第一個步驟確認該角色是否能夠被裝備後,由於 Equipment 還分成武器跟防具,必須得使用型別檢測的方式進行分流,各自再指派 equipmentweaponRefarmourRef

亦或者,讀者可以選擇分別定義 equipWeaponequipArmour 方法,不過這也會失去將所有類型的裝備進行抽象化成 Equipment 的意義。

另外,我們可以修改 SwordsmanWarlock 兩個角色職業的檔案 —— 各自在創建時就可以藉由裝備工廠客製化他們的裝備:

https://ithelp.ithome.com.tw/upload/images/20190927/20120614tIZVnf3sVq.png

https://ithelp.ithome.com.tw/upload/images/20190927/20120614cVaxjiXOIJ.png

有些剛接觸抽象工廠模式的讀者可能覺得模糊,因此筆者把架構圖畫出來!(如圖二)

https://ithelp.ithome.com.tw/upload/images/20190927/20120614AgYb9jNkLv.png
圖二:筆者云 —— 好大一張關聯圖...(筆者也被折騰一番才理解大致上的運作方式)

其中,筆者剛開始學習時,以為 Abstract Factory 的意思是:“要用抽象類別(Abstract Class)的方式對工廠進行抽象化的動作”。

然而,這句話其實有對的地方,也有錯的地方。

首先,對工廠抽象化的意思是:不要直接實踐出實體工廠,而是藉由介面先進行抽象化工廠的動作再分別延伸出想要創建的實體工廠是長什麼樣子。錯的地方是,不一定要用抽象類別去對工廠進行抽象化,而是定義一個介面再進行實體工廠類別綁定抽象工廠介面的動作

不過你仍然可以選擇將工廠介面改成用抽象類別方式去定義抽象工廠也是可以,然而抽象類別跟介面差別早在前一篇就已經提過:抽象類別只是多了一些實踐上的功能,比實體類別稍微彈性一點,但比介面稍微死板一點的做法

另外,以上的抽象工廠模式下的實體工廠都會產出固定的產品 Product,比較符合一般的 Factory Method 模式概念,但你仍然可以將實體工廠的工廠方法宣告參數並指定建立的武器類型也是可以 —— 意思就是,你選擇將那些 Factory Method 進行參數化也是可以的喔!

重點 2. 抽象工廠模式 Abstract Factory Pattern

若想建立的不同物件 —— 其中,每種不同物件差異性比較大,但又是隸屬於同一種類型 C —— 可以宣告 C 的抽象格式(根據情境使用介面抽象類別),並且讓不同物件類型繼承自 C(若 C 為抽象類別)或與 C 進行綁定(若 C 為介面)。

以上資料部分定好之後,就可以宣告 CFactory 的介面(或抽象類別)作為主要的抽象工廠類別,並且從 CFactory 延伸出針對 C 以下不同種類的物件相對應的實體工廠。

以本篇舉的例子來說,儘管武器 Weapon 跟防具 Armour 是兩個完全不ㄧ樣的物件,但同時又為角色裝備的類型 Equipment。因此 C 代表 Equipment,其中 WeaponArmour 就是從 Equipment 實踐出來的物件抽象類別。

Equipment 確定建好之後,就可以建立相對應的 EquipmentFactory 抽象工廠,本例子將 EquipmentFactory 宣告為介面。

藉由 EquipmentFactory 要求的格式,延伸出針對不同種類的裝備的工廠 —— SwordsmanEquipmentFactory 以及 WarlockEquipmentFactory,以針對不同的角色職業創建出各自的 WeaponArmour 類型的物件。

小結

今天應該是本系列中比較難一些的篇章,不過這是為了要讓讀者知道類別與介面有更多種使用方式~

最後的最後~為了要讓《機動藍圖》篇章做個完美的 Ending,好讓本系列迎向下一個篇章 ——《戰線擴張》,筆者要踢爆網路上 JS 圈流傳的物件複合(Object Composition)的概念流言 —— 又是很多天兵在網路上不經查證就散播亂七八糟的觀念導致越來越多很奇怪的程式碼出現。


上一篇
Day 28. 機動藍圖・抽象類別 X 藍圖基底 - TypeScript Abstract Class
下一篇
Day 30. 機動藍圖・流言終結者 X 重新認識物件的複合 - Favour Object Composition Over Class Inheritance
系列文
讓 TypeScript 成為你全端開發的 ACE!51
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
cuxy6705
iT邦新手 5 級 ‧ 2019-11-13 17:07:53

看到 BloodSucker 我直接笑出來 XD

/images/emoticon/emoticon37.gif

0
flier268
iT邦新手 5 級 ‧ 2020-12-29 11:38:49

感覺可以讓策略模式跟抽象工廠結合?

我要留言

立即登入留言