閱讀本篇文章前,仔細想想看
筆者列出到目前為止我們學到跟類別有關的名詞,可以回憶一下它們各自的定義以及實用的地方在哪裡~
- 類別與物件的差別 Class v.s. Object
- 成員變數與方法 Member Variables & Member Methods
- 存取修飾子與模式 Access Modifiers(
public
/private
/protected
)- 建構子函式 Constructor Function
- 類別繼承 Inheritance 與
super
關鍵字- 靜態屬性與方法 Static Properties & Methods
- 存取方法 Accessors (Getter Methods & Setter Methods)
如果還沒理解完畢的話,可以先翻看 Day 18. ~ Day 22. 的文章喔!
另外,本篇所舉的例子會承接 Day 26. 策略模式篇章進行下去,不過也會前情提要一下,所以讀者放心!
其實筆者壓根沒想到關於類別的主題會寫這麼多,不過既然是一系列完整的教學文,筆者認為有必要好好的把任何細節交代清楚。
另外,本篇章的範例程式碼已經放在 Maxwell-Alexius/Iron-Man-Competition 這個 Repo.。
因此本日正文開始囉~
本篇文章的範例承接 Day 26. —— 運用策略模式設計陽春版 RPG 遊戲角色機制,那時候談到的東西是如何搭配介面(Interface)與類別(Class),結合起來並應用策略模式(Strategy Pattern)寫出容易管理、可以重複使用的程式碼。
不過本篇運用的起始程式碼跟 Day 26. 的結果ㄧ樣 —— 就是實踐到可以切換攻擊策略的功能,但還沒實踐裝備武器的功能。以下就是今天的起始範例程式碼,筆者就先快速說明帶過,讀者也是能夠理解就可以趕快進入本篇章的下個重點。
首先是主要的父類別 Character
的程式碼。
name
代表角色名稱role
代表角色職業,為列舉型別,分別有:Role.Swordsman
、Role.Warlock
、Role.Highwayman
以及 Role.BountyHunter
四種職業attackRef
代表攻擊策略 Attack
的參考點,主要是策略模式中最重要的父類別與策略的連結introduce
為簡單的角色自我介紹方法attack
方法則是藉策略模式 —— 由 attackRef
連結到的攻擊策略 —— 代為執行角色攻擊的程序switchAttackStrategy
則是負責將 Attack
策略進行切換的動作攻擊的策略介面很簡單,就只有 attack
方法需要實現而已,程式碼如下。
另外,根據 Attack
介面延伸出三種不同的攻擊方式 —— MeleeAttack
、MagicAttack
以及 StabAttack
,其中筆者就貼 MeleeAttack
的程式碼,因為其他兩種攻擊策略大致上的實踐方式都差不多。
最後,就是根據 Character
繼承過後,子類別的實踐 —— 也就是角色被建立的邏輯,以下以 Swordsman
的程式碼為例,而另一個職業 Warlock
的實踐方式也是大同小異呢。
好,我們今天就快速進入正題。
筆者按照本系列的調性:先遇到問題,才開始進行正題的討論。
首先,今天要實作的東西跟昨天舉的案例一模ㄧ樣 —— 就是實踐遊戲角色裝備武器 Weapon
的功能。
讀者云:“搞什麼啊!作者是在故意耍人嗎!?難道以為可以寫ㄧ樣的內容草草帶過嗎?”
不!不!不!(請不要丟爛番茄)
筆者的意思是:儘管今天跟昨天要達成的需求ㄧ樣,但示範的實作方式不同,這也是要彰顯設計系統的彈性 —— 你不需要更多進階的技巧,例如裝飾子 Decorators、泛用型別 Generics等 —— 光是學會正確地使用介面與類別就可以寫出很不錯的應用,這應該才是厲害的地方。(進階的東西會在第四篇章以後介紹,現在還在第二篇章)
如果筆者只有帶過語法但沒有講些應用的話,就算介紹進階功能,讀者不會用 —— 學了 TypeScript 根本就沒 P 用,回去用原生 JS 還比較自由些,本身又可以變出很棒又很蠢的戲法。
今天筆者希望達到這個目標:
與其讓角色能夠直接切換攻擊策略,不如藉由裝備武器
Weapon
—— 進行攻擊策略的切換與使用。
前一篇著重的點是:Character
同時有 attackRef
連結攻擊策略、weaponRef
連結武器的選擇 —— 藉由切換武器的同時,更新攻擊策略。
今天的目標則是:Character
只會有 weaponRef
連結裝備的武器;而裝備的武器 Weapon
有 attackRef
的設定,藉由 weaponRef
進行呼叫攻擊的動作。所以本篇文和前一篇文實作是有差別的喔!
讀者剛開始可能會覺得模糊,不過筆者一步步示範給讀者看 —— 按照前兩篇策略模式四步驟嚴格執行。(所以這是第三次示範策略模式了 XD)
首先,第一件事情就是先規範好武器 Weapon
的介面,畢竟武器的選擇也可以被策略模式給應用。
武器的介面有以下這些性質:
name
代表武器名稱,為唯讀模式availableRoles
代表可以被裝備該武器的職業attackStrategy
代表武器跟 Attack
攻擊策略之間的參考點(reference point)switchAttackStrategy
函式負責進行攻擊策略的切換attack
方法就是負責實現角色攻擊的功能 —— 由於是策略模式,所以會藉由 attackStrategy
進行呼叫筆者照樣實踐三種不同的武器策略:BasicSword
、BasicWand
與 Dagger
。
以上就是對在 Weapon
介面下,延伸出來三種不同的武器策略。
貼心小提示
敏銳的讀者一定發現:三種武器策略的
switchAttackStrategy
與attack
成員方法重複了 —— 因此違反了 DRY(Don't Repeat Yourself)原則!筆者這邊要恭喜讀者:能夠注意到這個點,就代表讀者快抓到 —— 判斷使用抽象類別的時機點的感覺!
不過這裡要請讀者繼續看下去~
這個步驟應該對讀者來說算單純 —— 但是要注意,本日目標明確指定一點:Character
類別必須藉由裝備的武器 Weapon
進行角色攻擊的動作。
以下就是對 Character
類別連結 Weapon
的實踐:
其中,筆者建立了 weaponRef
負責連結 Character
與武器之間的關係 —— 儘管就只有一行宣告而已,但卻是使用策略模式的重要關鍵呢!
接下來就是要讓角色的武器能夠攻擊別人。以下是對 Character
類別的實作過程:
Character
的 attack
成員方法是藉由 weaponRef
呼叫它的 attack
方法,將角色與被攻擊的角色傳遞下去,直到發動攻擊的策略。(這感覺跟英文單字 —— propagation 的行為很像)
另外,equip
方法則是負責切換角色的武器選擇(武器策略) —— 也會根據武器的 availableRoles
進行檢測,判斷該武器是否能夠被該角色裝備。
最後,我們在 Swordsman
與 Warlock
這兩個類別進行武器策略的初始化:
以下是簡單的程式碼檢驗。(編譯並且執行結果如圖一)
圖一:我們成功地讓武器可以被切換,照常可以動作!
另外,除了武器可以被切換外,我們也可以建立 BasicSword
物件並且將其 Attack
連結的策略從原本預設的 MeleeAttack
切換成 StabAttack
。
以下的程式碼檢測結果如圖二。
圖二:將 BasicSword
的攻擊策略切換為 StabAttack
也可以生效!
相信讀者看到這裡,會覺得策略模式還蠻好用的。從這裡開始,筆者要解決這個問題 —— 每次實踐新的武器,都會出現的重複的程式碼如下:
回憶過往本系列學到的東西:好像可以將那些重複的方法實踐整理起來,放在父類別,再一併繼承下去。
於是筆者將**Weapon
從介面晉升為類別等級**,並且把 switchAttackStrategy
跟 attack
成員方法的實踐寫下去。
不過這裡又會出現問題:name
、availableRole
與 attackStrategy
這些東西在父類別是不確定的,必須強制讓子類別去進行覆蓋的動作 —— 一種解法是,父類別針對這些屬性進行預設值的動作,於是出來的 Weapon
類別的實踐結果如下:
由於 Weapon
從介面晉升為類別,所有 Weapon
延伸出的武器策略必須從 implements
改成 extends
—— 也就是類別的繼承。以下就是 BasicSword
、BasicWand
與 Dagger
實踐過後的結果(基本上長得都差不多):
有些讀者認為這樣就夠了,但筆者可不這麼認同,因為父類別 Weapon
的實踐失去了介面的彈性,我們只能用預設值的方式防止程式碼壞掉,但不能利用介面的技巧,強迫子類別實踐出 name
、availableRoles
與attackStrategy
等成員。
如果同時想要擁有:
- 類別的性質 —— 成員有實際的實踐過程,以及
- 介面的性質 —— 一但跟介面簽訂條約,就必須強制實踐介面指名的功能
則可以選擇使用抽象類別(Abstract Class)!
要運用抽象類別很簡單,宣告抽象類別時記得使用 abstract class
關鍵字,並且在該抽象類別的成員裡,可以選擇:
abstract
因此,將 Weapon
從類別再轉換成抽象類別,程式碼會變得更簡潔呢!
你可以發現:name
、availableRoles
與 attackStrategy
被註記為 abstract
,代表子類別若沒有實踐這些功能,就會被 TypeScript 警告。(錯誤訊息如圖三)
圖三:筆者刻意在 Weapon
的子類別 —— BasicSword
裡面,將 name
欄位砍掉,結果被 TypeScript 警告,因為 name
是父類別的抽象成員,必須被實踐!
這裡筆者就略過程式碼檢驗的過程,讓讀者自己去嘗試看看吧!
重點 1. 抽象類別的宣告與意義 Abstract Class
介面與類別各自的特點,分別如下:
- 介面的特點:一但跟介面進行綁定的動作,TypeScript 會針對沒有被實踐到的規格進行監控的動作
- 類別的特點:定義物件的完整藍圖與實踐過程
如果想要兼顧介面與類別的優勢 —— 繼承父類別的同時,也能夠彈性地宣告規格,而非直接實踐出過程,則可以選擇使用抽象類別(Abstract Class)。
若抽象類別
AbstractC
的宣告方式如下:則一但繼承
AbstractC
的子類別擁有以下特性與條件:
- 繼承了
AbstractC
的成員變數Prop
與成員方法Method
- 必須實踐成員變數
Pabstract
以及成員方法Mabstract
另外,抽象類別也會有些限制 —— 可以藉由推理就推出特性:
重點 2. 抽象類別的限制 Limitation of Abstract Class
- 抽象類別不能進行建立物件的動作:因為裡面的抽象成員是還未實踐的狀態,就算硬要從抽象類別建立物件,該物件也會是不完整狀態
- 根據前一點推斷:抽象類別生來就是要被繼承的
- 抽象類別裡的抽象成員(Abstract Member),由於要滿足介面的特性 —— 代表規格並且強迫繼承的子類別必須實踐功能,因此抽象成員必需被實踐為
public
模式
重點 2 提到的最後一點,抽象成員必為 public
模式跟類別實踐介面本身的規格,那些成員必須為 public
模式的邏輯是一模一樣的!
筆者總算把 TypeScript 類別的最後一部分的語法交代完畢~
下一篇筆者要介紹抽象工廠模式這個設計模式~算是介面和類別結合的延伸應用喔~!
看完文章後有個問題:
由於抽象類別的成員必須被實踐為 public
模式,以本程式碼來說,創建 Weapon
類別的物件後,可以直接存取 attackStrategy
並直接改變 attackStrategy
,略過 switchAttackStrategy
的邏輯,造成武器與攻擊 mapping 上的錯誤。
此類問題有建議的解決方法嗎?
我好像有解法了XD
實作抽象類別的類別本身實作抽象成員時設 readonly
,就無法透過該類別建立的物件直接修改 attackStrategy
而 switchAttackStrategy
還是能修改 attackStrategy
的原因是,switchAttackStrategy
是在抽象類別內定義,
抽象類別本身定義的抽象成員沒有設 readonly
,所以抽象類別本身的 switchAttackStrategy
可以修改 attackStrategy
嗨~看完這部分後,有幾個疑惑想詢問一下:
為何不用 super()
,而要改用 abstract
方法,效果有什麼差別嗎?
Character.ts 的部分,是不是也適合改成 abstract class?
謝謝。
另外 abstract
應該不只能用 public
也是可以用 protected
的。