閱讀本篇文章前,仔細想想看
- 已經熟悉類別的運作流程並懂得 OOP 的基礎概念。
- 熟悉了策略模式(Strategy Pattern)嗎?
如果還不清楚的話,可以看筆者講述關於策略模式的篇章喔!
筆者首先恭喜讀者:
在這 30 天,你已經學會了 ... 應該算四成左右的 TypeScript XDDDD(
笑 P 笑)
(讀者拔出手中球棒準備往作者頭上揮去)
慢著!筆者還沒說完 —— 預估可能還要 20 40 天以上才能完整補完筆者當初在本系列開端承諾的事項,哎呀...
之所以會需要更多時間的用意在於:
能夠靈活運用各種 TypeScript 的 Feature,不需要依靠框架也可以寫出不錯的程式碼
但請不要誤會,不是說框架這個東西不好,有時候用對工具或者是追求時效就是要基於熱心的社群幫你開發出的工具進行專案的開發。而筆者想要強調的點是 —— 如果你可以不需經由框架也能夠寫出有架構的程式碼,相信就算學習或跳槽到任何類似的程式語言,應用起來都會非常的輕鬆。
本來可以在第 20 幾天的時候就開始進入《戰線擴張》系列,但是類別可以講的應用很多 —— 對於只有 JavaScript 開發經驗而沒有實際的 OOP 經驗的讀者(其實也包含筆者本人),類別的概念會顯得有些不知道如何應用。(簡而言之就是要開天眼)
接下來,筆者將為《機動藍圖》篇章系列劃下美好的句點:就從重新認識 Favour Object Composition Over Class Inheritance 這個概念~
正文開始!
貼心小提示
本篇對於有 C#、Java 等語言有經驗的讀者會覺得理所當然,可是對於 JS 圈的開發者 —— 物件複合 Object Composition 的概念
至少在國外看來被曲解的超級嚴重,甚至有 YouTuber 型開發者或 Medium 文章撰寫者大肆散播亂七八糟的 JavaScript 版本的 Object Composition 概念。是的,你沒有聽錯 ——(JS 版本的)Object Composition 概念被很多 JS 圈的人亂解讀到連自己寫成 Anti-Pattern 還可以沾沾自喜到連自己都沒發覺。
但 C# 跟 Java 這一類本身遵循 OOP 的原則的語言圈,筆者看起來是沒有問題的,畢竟 OOP 是這一類語言的核心基礎,連 Object Composition 這個東西在這些語言被曲解的話,恩... 聽起來好像會蠻慘的 XDDDDD
首先,自本系列一直探討類別與介面的機制,我們都知道類別繼承比實踐介面還要來得死,因為耦合度(Coupling)太高,所以很難將類別繼承過後的東西重複把功能拔出來再利用到其他地方。
一但繼承過後的子類別不符合需求,可能還要再重新宣告一個子類別繼承父類別,然後又是痛苦的實作過程;另外,父類別一但被眾多子類別繼承,但是想要更動父類別的功能也很容易造成子類別被牽連,出現錯誤的機率也會大幅提升。
另一方面,實作介面時,因為類別是可以實踐多種不同的介面,而介面又可以被眾多類別重複利用,耦合度又比類別繼承還要來得低,因此會比較建議用介面進行功能實作上的組合。
當讀者鑽研 OOP 到一個程度,通常會聽過一句很有名的話:
Favour Object Composition Over Class Inheritance.
Object Composition(也就是物件複合)的目的,與使用介面的理由很像:都是要降低類別物件之間的耦合度,不過實作方式跟介面有差別。
今天筆者想要講述的重點在於:
讀者可以上網打關鍵字搜尋:JavaScript Object Composition
諸如此類的關鍵字(結果如圖一):
筆者跟你保證,就算是放在 Medium 的文章 —— 這些藉由 JavaScript 闡述 Object Composition 的觀念完全錯誤。
再三強調,大部分找到闡述 JavaScript Object Composition
相關的主題文章對於這句話的描述:
Favour Object Composition over Class Inheritance
以上這句話是對的!
但是那些文章:
本篇不特別針對是哪一篇文章 —— 讀者甚至隨便挑剛剛筆者搜尋的結果任何一篇都會差不多,請自行判斷。筆者就直接放其中一篇文章的案例:
以上的程式碼是一種 Anti-Pattern 的完美實踐 —— 請讀者不要亂學。(除非你想煮出美味的義大利麵)
首先,該範例有兩個主角:
Fighter
格鬥士Mage
法術士格鬥士分別擁有 name
、health
與 stamina
(代表耐力)三種屬性狀態;法術士則是擁有 name
、health
與 mana
(魔力值)。
另外程式碼有額外定義兩個模組 —— canFight
與 canCast
,可以為 Fighter
或 Mage
裡面的物件進行擴展的動作,所以才會有 Object.assign
那一行:
這些網路上文章寫的以上範例程式碼,它們主張的是:Object Composition 的概念就是物件組合的概念。因此才會將 Fighter
物件的狀態 state
與 canFight
組合起來;也就是說,它們將 Object Composition 想成是 JSON 物件組來組去的概念。
讀者可以去每個文章看一下,但它們得出的共通結論就是:我們應該選擇使用物件組合的概念勝過於類別繼承。(Favour Object Composition over Class Inheritance)
會不會有點跳 Tone? XD
以下筆者一一分析為何這句話是 BS:
網路上的謠言
Object Composition 的概念就是物件組合的概念 —— 也就是說,Object Composition 可以想成是 JSON 物件組來組去的概念
接下來是筆者對於這些文章闡述的概念提出裡面的盲點。
首先,如果將剛剛的 Fighter
與 Mage
類比成類別的話:
由於在該範例的程式碼,為了將那些文章所謂的物件組合起來而進行 Object.assign
的動作,用以下這種方式也同樣可以達成:
直接使用物件本身的狀態 state
再被 Object.assign
代入更多屬性 —— 這個行為不就等同於直接繼承該物件的狀態嗎?然而那些文章並非用類別的語法,而是單純物件組來組去的語法,但行為跟類別繼承的模式完全沒兩樣,因為是物件直接跟其他物件進行相依賴,直接從其他物件組出新狀態而已。
所以這句話是講假的嗎?
Favour Object Composition over Class Inheritance?
為了重新認識 Object Composition 的意義,這裡就要回歸這句話的源頭,於是筆者要介紹一個概念:Delegation 物件委任。
重點 1. 物件的委任 Delegation
(出自《Design Patterns - Elements of Reusable Object Oriented Software》 這本書)
"Delegation is a way of making composition as powerful for reuse as inheritance. In delegation, two objects are involved in handling a request: a receiving object delegates operations to its delegate."
筆者翻譯:物件的委任可以達到與類別繼承相當的功能,使得不同類別元件也可以被重複利用。兩個物件之間的關係中,若主要的物件需要執行的某項需求是需要另一個物件提供的功能,則主物件可以將這個需求遞給委任的物件進行處理。
聽起來很模糊,但筆者就直接破題:委任的概念跟策略模式篇章採取的作風很像。
但更精確的說法是:策略模式在進行策略替換時的那個參考點,該參考點連結到的不同之策略 —— 那些策略就是被委任的物件。
首先,我們回歸策略模式篇章的範例,以下筆者先把架構圖貼上來:
角色 Character
在這裡是主要的物件,其中想要實踐 Attack
的功能,與其直接用類別的繼承,不如設置一個參考點 —— 連結不同的 Attack
介面底下所綁定的不同的攻擊策略 —— 而那些攻擊策略就是被 Character
所委任(delegate)的對象!
以下的程式碼就是 Attack
策略是如何被實踐出來的:
而這個藉由物件委任其他物件的行為,建立起物件之間的連結 —— 這才是物件複合(Object Composition)的真諦!
筆者再舉一個簡單的例子,比如說我們有一個視窗類別 MyWindow
:
它可以被這樣使用(如圖二):
圖二:印出 myWindow
的面積與周長
假設,視窗可能分別是圓形或長方形,最粗糙的手法是 —— 分別宣告 Rectangle
與 Circle
並且綁定同一種介面,也就是 Geometry
:
然後再進行繼承的動作,變成 RectangularWindow
與 CircularWindow
:
可是這裡就出現問題 —— 要如何能夠只宣告一個 MyWindow
類別,但又能同時指定為 Rectangle
或者是 Circle
形狀的視窗呢?
另外,就算指定了幾何形狀,因為長方形跟圓形計算面積或周長的過程也都不ㄧ樣 —— 一個是需要長度 width
與寬度 height
;另一個則是半徑 radius
還有 Circle.PI
這個靜態成員。
所以不能直接運用類別繼承的概念,但是可以使用物件複合的技巧 —— 也就是物件的委任(Delegation)。
首先,我們將 MyWindow
設立一個委任物件的參考點 dimension
—— 再藉由該參考點,將 area
或 circumference
的計算請求遞給委任的參考點:
這樣我們就可以直接測試這段程式碼。(結果如圖三)
圖三:是不是通順很多?
而且物件委任(也就是物件複合的根本)的概念本身就可以作為策略模式的基礎,因此筆者才會在開篇章時直接篤定跟讀者講 —— 如果看過本系列的策略模式篇章過後,你早已會物件複合的概念。
筆者就要指出剛剛在推翻理論的步驟 1 提出的範例程式碼 —— 錯的或者是不太建議使用的點。
回歸 Fighter
與 Mage
,設計遊戲角色時,試著想想看,我們會將角色的每個屬性刻意訂得不一樣嗎? —— Figher
擁有 stamina
,而 Mage
則是對應 mana
。
解法一:統一規範 Character
介面,每個角色皆擁有 stamina
與 mana
屬性,但數值可以客製化。
interface Character {
name: string;
heath: number;
mana: number;
stamina: number;
}
解法二:
Character
,但是設置一個名為 characterStatRef
做為參考點,另外定義 Stat
介面並延伸出 FighterStat
與 MageStat
,然後進行物件的委任(Delegation)attack
介面,再分別定義不同的策略,例如 DirectAttack
或 CastingAttack
解法二是根據原著的範例程式碼看似想要朝向的方向進行實作,想當然要能夠達到那種層級的彈性,可不是只有所謂JSON 物件組來組去就能夠寫出好的設計。
首先,如果假設我們還多了一個 canStab
(可以刺擊)這個模組,其中 canStab
模組跟 canFight
模組都是想用 attack
方法,那麼:
let canFight = (state) => ({
attack() { /* ... */ }
});
let canStab = (state) => ({
attack() { /* ... */ }
});
function Fighter(name) {
let state = { /* 略... */ };
return Object.assign(state, canFight(state), canStab(state));
}
讀者看到應該也會知道 —— 命名會衝突,canStab
裡面的 attack
方法會覆寫掉 canFight
方法。
解法:直接採用正確版本的 Object Composition —— 就算你沒有用類別,也可以利用策略模式實踐出不同的攻擊策略!
藉由剛剛一連串的推論與分析過程,我們得知:
大部分在網路上跟 JS 相關闡述的 Object Composition 的概念事實上跟類別繼承的本質沒兩樣。
筆者可以宣告那些文章闡述的 Object Composition 的概念完全是錯誤的。
正確的概念如下:
重點 2. Favour Object Composition Over Class Inheritance
Object Composition 的觀念是藉由物件的委任(Delegation)進行物件與物件間的連結。
主要的物件若需要執行特定的功能,可以將執行的步驟遞給委任的物件代理執行。
Object Composition 的優勢大過於類別繼承的原因主要有:
- 彈性較高,關係是建立在抽象的基礎之上,而非實體繼承
- 隨時可以替換同質性的委任物件,只要委任物件都遵守同一個介面的規格
- 委任的物件可以被重複使用 —— 任何其他主要物件需要委任物件的功能,只要開出一個參考點連結到委任物件,並且將功能傳遞給委任物件就好了
- 大部分的設計模式都是基於 Object Composition 進行延伸
- 委任物件的模式就是讀者可能聽過的 Dependency Injection (依賴注入) 的實踐,可以避免物件與物件間的相依性過高造成難以功能分離與重複使用的狀況
儘管鐵人賽的基本標準已經過了,但本系列文的目標還未達成~
《機動藍圖》篇章系列 —— 至少從 TypeScript 介面到類別這樣的介紹過程,筆者認為基礎的闡述 —— 一個里程碑的抵達!
然而 TypeScript 的征途還未結束呢~筆者根據自己打過的草稿,我們才走完 ... 還不到一半的歷程。(汗)
不過讀者當然可以自由選擇要學習到哪個程度,畢竟寫軟體這種行業求的不是 100% 完美的程式碼,而是會寫、會用,至少還會改進程式碼的品質就已經是合格了。
下一篇總算要推播本系列的第三篇章 ——《戰線擴張》。
新的篇章的篇幅不會到《機動藍圖》那麼誇張 XD,但是對要和實際專案進行銜接是很重要的篇章呢!