iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 28
2
Modern Web

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

Day 28. 機動藍圖・抽象類別 X 藍圖基底 - TypeScript Abstract Class

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20190926/20120614Tv3mOYsfgi.png

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

筆者列出到目前為止我們學到跟類別有關的名詞,可以回憶一下它們各自的定義以及實用的地方在哪裡~

  • 類別與物件的差別 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.。

因此本日正文開始囉~

抽象類別的應用 Abstract Class

前情提要,本日的起始程式碼範例

本篇文章的範例承接 Day 26. —— 運用策略模式設計陽春版 RPG 遊戲角色機制,那時候談到的東西是如何搭配介面(Interface)與類別(Class),結合起來並應用策略模式(Strategy Pattern)寫出容易管理、可以重複使用的程式碼。

不過本篇運用的起始程式碼跟 Day 26. 的結果ㄧ樣 —— 就是實踐到可以切換攻擊策略的功能,但還沒實踐裝備武器的功能。以下就是今天的起始範例程式碼,筆者就先快速說明帶過,讀者也是能夠理解就可以趕快進入本篇章的下個重點。

首先是主要的父類別 Character 的程式碼。

https://ithelp.ithome.com.tw/upload/images/20190926/20120614X4aAmI03Rq.png

  • name 代表角色名稱
  • role 代表角色職業,為列舉型別,分別有:Role.SwordsmanRole.WarlockRole.Highwayman 以及 Role.BountyHunter 四種職業
  • attackRef 代表攻擊策略 Attack 的參考點,主要是策略模式中最重要的父類別與策略的連結
  • introduce 為簡單的角色自我介紹方法
  • attack 方法則是藉策略模式 —— 由 attackRef 連結到的攻擊策略 —— 代為執行角色攻擊的程序
  • switchAttackStrategy 則是負責將 Attack 策略進行切換的動作

攻擊的策略介面很簡單,就只有 attack 方法需要實現而已,程式碼如下。

https://ithelp.ithome.com.tw/upload/images/20190926/201206146CFRq4e7ny.png

另外,根據 Attack 介面延伸出三種不同的攻擊方式 —— MeleeAttackMagicAttack 以及 StabAttack,其中筆者就貼 MeleeAttack 的程式碼,因為其他兩種攻擊策略大致上的實踐方式都差不多。

https://ithelp.ithome.com.tw/upload/images/20190926/20120614NtsNVYAfSb.png

最後,就是根據 Character 繼承過後,子類別的實踐 —— 也就是角色被建立的邏輯,以下以 Swordsman 的程式碼為例,而另一個職業 Warlock 的實踐方式也是大同小異呢。

https://ithelp.ithome.com.tw/upload/images/20190926/201206146eSnZRhMou.png

好,我們今天就快速進入正題。

學習抽象類別的真諦 —— 做中學

筆者按照本系列的調性:先遇到問題,才開始進行正題的討論

首先,今天要實作的東西跟昨天舉的案例一模ㄧ樣 —— 就是實踐遊戲角色裝備武器 Weapon 的功能

讀者云:“搞什麼啊!作者是在故意耍人嗎!?難道以為可以寫ㄧ樣的內容草草帶過嗎?”

不!不!不!(請不要丟爛番茄

筆者的意思是:儘管今天跟昨天要達成的需求ㄧ樣,但示範的實作方式不同,這也是要彰顯設計系統的彈性 —— 你不需要更多進階的技巧,例如裝飾子 Decorators泛用型別 Generics等 —— 光是學會正確地使用介面與類別就可以寫出很不錯的應用,這應該才是厲害的地方。(進階的東西會在第四篇章以後介紹,現在還在第二篇章)

如果筆者只有帶過語法但沒有講些應用的話,就算介紹進階功能,讀者不會用 —— 學了 TypeScript 根本就沒 P 用,回去用原生 JS 還比較自由些,本身又可以變出很棒又很蠢的戲法。

開始今天的範例 —— 實踐角色的武器裝備功能

今天筆者希望達到這個目標:

與其讓角色能夠直接切換攻擊策略,不如藉由裝備武器 Weapon —— 進行攻擊策略的切換與使用。

前一篇著重的點是:Character 同時attackRef 連結攻擊策略、weaponRef 連結武器的選擇 —— 藉由切換武器的同時,更新攻擊策略。

今天的目標則是:Character 只會有 weaponRef 連結裝備的武器;而裝備的武器 WeaponattackRef 的設定,藉由 weaponRef 進行呼叫攻擊的動作。所以本篇文和前一篇文實作是有差別的喔!

讀者剛開始可能會覺得模糊,不過筆者一步步示範給讀者看 —— 按照前兩篇策略模式四步驟嚴格執行。(所以這是第三次示範策略模式了 XD)

步驟 1. 策略的介面綁定與宣告

首先,第一件事情就是先規範好武器 Weapon 的介面,畢竟武器的選擇也可以被策略模式給應用。

https://ithelp.ithome.com.tw/upload/images/20190926/20120614qEFxiwcDS9.png

武器的介面有以下這些性質:

  • name 代表武器名稱,為唯讀模式
  • availableRoles 代表可以被裝備該武器的職業
  • attackStrategy 代表武器跟 Attack 攻擊策略之間的參考點(reference point)
  • switchAttackStrategy 函式負責進行攻擊策略的切換
  • attack 方法就是負責實現角色攻擊的功能 —— 由於是策略模式,所以會藉由 attackStrategy 進行呼叫

筆者照樣實踐三種不同的武器策略:BasicSwordBasicWandDagger

https://ithelp.ithome.com.tw/upload/images/20190926/20120614YnnOBnmwpZ.png

https://ithelp.ithome.com.tw/upload/images/20190926/201206148dKpSESQzr.png

https://ithelp.ithome.com.tw/upload/images/20190926/20120614huC4GKfYd7.png

以上就是對在 Weapon 介面下,延伸出來三種不同的武器策略。

貼心小提示

敏銳的讀者一定發現:三種武器策略的 switchAttackStrategyattack 成員方法重複了 —— 因此違反了 DRY(Don't Repeat Yourself)原則!

筆者這邊要恭喜讀者:能夠注意到這個點,就代表讀者快抓到 —— 判斷使用抽象類別的時機點的感覺

不過這裡要請讀者繼續看下去~

步驟 2. 父類別建立策略參考點

這個步驟應該對讀者來說算單純 —— 但是要注意,本日目標明確指定一點:Character 類別必須藉由裝備的武器 Weapon 進行角色攻擊的動作。

以下就是對 Character 類別連結 Weapon 的實踐:

https://ithelp.ithome.com.tw/upload/images/20190926/201206143PqWXMxUSP.png

其中,筆者建立了 weaponRef 負責連結 Character 與武器之間的關係 —— 儘管就只有一行宣告而已,但卻是使用策略模式的重要關鍵呢!

步驟 3. 藉由參考點進行功能傳遞的動作

接下來就是要讓角色的武器能夠攻擊別人。以下是對 Character 類別的實作過程:

https://ithelp.ithome.com.tw/upload/images/20190926/20120614fiDig8nyqc.png

Characterattack 成員方法是藉由 weaponRef 呼叫它的 attack 方法,將角色與被攻擊的角色傳遞下去,直到發動攻擊的策略。(這感覺跟英文單字 —— propagation 的行為很像)

另外,equip 方法則是負責切換角色的武器選擇(武器策略) —— 也會根據武器的 availableRoles 進行檢測,判斷該武器是否能夠被該角色裝備。

步驟 4. 子類別可以選擇策略

最後,我們在 SwordsmanWarlock 這兩個類別進行武器策略的初始化:

https://ithelp.ithome.com.tw/upload/images/20190926/20120614niWZfoYCHf.png

https://ithelp.ithome.com.tw/upload/images/20190926/20120614fzUHKbLiDk.png

完成功能 —— 進行檢驗!

以下是簡單的程式碼檢驗。(編譯並且執行結果如圖一)

https://ithelp.ithome.com.tw/upload/images/20190926/20120614GHx2Z2QrGw.png

https://ithelp.ithome.com.tw/upload/images/20190926/20120614Q9l07dITeS.png
圖一:我們成功地讓武器可以被切換,照常可以動作!

另外,除了武器可以被切換外,我們也可以建立 BasicSword 物件並且將其 Attack 連結的策略從原本預設的 MeleeAttack 切換成 StabAttack

以下的程式碼檢測結果如圖二。

https://ithelp.ithome.com.tw/upload/images/20190926/20120614T46tymHxY4.png

https://ithelp.ithome.com.tw/upload/images/20190926/201206149md0uJ0IPK.png
圖二:將 BasicSword 的攻擊策略切換為 StabAttack 也可以生效!

運用抽象類別 Abstract Class

相信讀者看到這裡,會覺得策略模式還蠻好用的。從這裡開始,筆者要解決這個問題 —— 每次實踐新的武器,都會出現的重複的程式碼如下:

https://ithelp.ithome.com.tw/upload/images/20190926/2012061405LPXOW2M7.png

回憶過往本系列學到的東西:好像可以將那些重複的方法實踐整理起來,放在父類別,再一併繼承下去。

於是筆者將**Weapon 從介面晉升為類別等級**,並且把 switchAttackStrategyattack 成員方法的實踐寫下去。

不過這裡又會出現問題:nameavailableRoleattackStrategy 這些東西在父類別是不確定的,必須強制讓子類別去進行覆蓋的動作 —— 一種解法是,父類別針對這些屬性進行預設值的動作,於是出來的 Weapon 類別的實踐結果如下:

https://ithelp.ithome.com.tw/upload/images/20190926/20120614ygoU7IPuDh.png

由於 Weapon 從介面晉升為類別,所有 Weapon 延伸出的武器策略必須從 implements 改成 extends —— 也就是類別的繼承。以下就是 BasicSwordBasicWandDagger 實踐過後的結果(基本上長得都差不多):

https://ithelp.ithome.com.tw/upload/images/20190926/201206144mVQaeE2ir.png

https://ithelp.ithome.com.tw/upload/images/20190926/20120614elNHnzzSZe.png

https://ithelp.ithome.com.tw/upload/images/20190926/20120614p0gwWmZ5lt.png

有些讀者認為這樣就夠了,但筆者可不這麼認同,因為父類別 Weapon 的實踐失去了介面的彈性,我們只能用預設值的方式防止程式碼壞掉,但不能利用介面的技巧,強迫子類別實踐出 nameavailableRolesattackStrategy 等成員。

如果同時想要擁有:

  1. 類別的性質 —— 成員有實際的實踐過程,以及
  2. 介面的性質 —— 一但跟介面簽訂條約,就必須強制實踐介面指名的功能

則可以選擇使用抽象類別(Abstract Class)!

要運用抽象類別很簡單,宣告抽象類別時記得使用 abstract class 關鍵字,並且在該抽象類別的成員裡,可以選擇:

  1. 如果要實踐該成員方法或變數,跟宣告普通類別時,照常實踐出功能
  2. 如果想要讓該成員方法或變數擁有類似介面的性質 —— 也就是說,一但任何子類別繼承父類別,則必須要實踐出該成員方法與變數,就直接在該成員前面標註 abstract

因此,將 Weapon 從類別再轉換成抽象類別,程式碼會變得更簡潔呢!

https://ithelp.ithome.com.tw/upload/images/20190926/20120614MLAsTgZBE5.png

你可以發現:nameavailableRolesattackStrategy 被註記為 abstract,代表子類別若沒有實踐這些功能,就會被 TypeScript 警告。(錯誤訊息如圖三)

https://ithelp.ithome.com.tw/upload/images/20190926/20120614NrGKyBcKIF.png
圖三:筆者刻意在 Weapon 的子類別 —— BasicSword 裡面,將 name 欄位砍掉,結果被 TypeScript 警告,因為 name 是父類別的抽象成員,必須被實踐!

這裡筆者就略過程式碼檢驗的過程,讓讀者自己去嘗試看看吧!

重點 1. 抽象類別的宣告與意義 Abstract Class

介面與類別各自的特點,分別如下:

  • 介面的特點:一但跟介面進行綁定的動作,TypeScript 會針對沒有被實踐到的規格進行監控的動作
  • 類別的特點:定義物件的完整藍圖與實踐過程

如果想要兼顧介面與類別的優勢 —— 繼承父類別的同時,也能夠彈性地宣告規格,而非直接實踐出過程,則可以選擇使用抽象類別(Abstract Class)。

若抽象類別 AbstractC 的宣告方式如下:

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

則一但繼承 AbstractC 的子類別擁有以下特性與條件:

  1. 繼承了 AbstractC 的成員變數 Prop 與成員方法 Method
  2. 必須實踐成員變數 Pabstract 以及成員方法 Mabstract

另外,抽象類別也會有些限制 —— 可以藉由推理就推出特性:

重點 2. 抽象類別的限制 Limitation of Abstract Class

  1. 抽象類別不能進行建立物件的動作:因為裡面的抽象成員是還未實踐的狀態,就算硬要從抽象類別建立物件,該物件也會是不完整狀態
  2. 根據前一點推斷:抽象類別生來就是要被繼承的
  3. 抽象類別裡的抽象成員(Abstract Member),由於要滿足介面的特性 —— 代表規格並且強迫繼承的子類別必須實踐功能,因此抽象成員必需被實踐為 public 模式

重點 2 提到的最後一點,抽象成員必為 public 模式跟類別實踐介面本身的規格,那些成員必須為 public 模式的邏輯是一模一樣的!

小結

筆者總算把 TypeScript 類別的最後一部分的語法交代完畢~

下一篇筆者要介紹抽象工廠模式這個設計模式~算是介面和類別結合的延伸應用喔~!


上一篇
Day 27. 機動藍圖・策略模式 X 臨機應變 - Strategy Pattern Using TypeScript. II
下一篇
Day 29. 機動藍圖・工廠模式 X 抽象工廠 - Factory Method & Abstract Factory Pattern Using TypeScript
系列文
讓 TypeScript 成為你全端開發的 ACE!51
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
jason71708
iT邦新手 3 級 ‧ 2021-10-27 16:51:37

看完文章後有個問題:
由於抽象類別的成員必須被實踐為 public 模式,以本程式碼來說,創建 Weapon 類別的物件後,可以直接存取 attackStrategy
並直接改變 attackStrategy,略過 switchAttackStrategy 的邏輯,造成武器與攻擊 mapping 上的錯誤。

此類問題有建議的解決方法嗎?

我好像有解法了XD
實作抽象類別的類別本身實作抽象成員時設 readonly,就無法透過該類別建立的物件直接修改 attackStrategy
switchAttackStrategy 還是能修改 attackStrategy 的原因是,switchAttackStrategy 是在抽象類別內定義,
抽象類別本身定義的抽象成員沒有設 readonly,所以抽象類別本身的 switchAttackStrategy 可以修改 attackStrategy

0
iT邦新手 2 級 ‧ 2022-10-12 15:08:29

嗨~看完這部分後,有幾個疑惑想詢問一下:

  1. 為何不用 super(),而要改用 abstract 方法,效果有什麼差別嗎?

  2. Character.ts 的部分,是不是也適合改成 abstract class?

謝謝。

另外 abstract 應該不只能用 public 也是可以用 protected 的。

我要留言

立即登入留言