iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 13
3
Modern Web

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

Day 13. 機動藍圖・介面的延展 X 功能與意義 - Interface Extension & Significance

https://ithelp.ithome.com.tw/upload/images/20190922/20120614xpA289Yt1n.png

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

  1. 如何宣告介面(Interface)?
  2. 介面跟型別(Type)在語法上的差別與規則會是什麼?(筆者目前還沒講概念上的差別,讀者先回想語法層面就夠了)

如果還沒理解完畢的話,可以先翻看前一篇文章喔!

今天要來談談 TypeScript Interface 可以很彈性地編制與擴展的特性與更多可以在介面上面做的事情。

正文開始

TypeScript 介面的彈性機制

介面的擴展(Interface Extension / Inheritance)

介面的概念很熱門的主要原因是 —— 它可以被組來組去,也可以被延伸(Extend)。

在 TypeScript 通常會聽到 Interface Extension 或 Interface Inheritance,這都是可以的說法,不過筆者偏向於前者,畢竟後續要介紹的另一個關鍵字叫做 extends

我們舉一個讀者之前感覺好像在哪裡看過的例子:

https://ithelp.ithome.com.tw/upload/images/20190916/20120614pfSOyMZq03.png

UserAccount 這個介面作為 AccountSystem 以及 AccountPersonalInfo 的擴展(Extension)。因此我們來測測看以下基礎狀況。(檢驗結果如圖一)

https://ithelp.ithome.com.tw/upload/images/20190916/20120614UvfsOZPYD0.png

https://ithelp.ithome.com.tw/upload/images/20190916/20120614k7OxqJsOlN.png
圖一:檢驗結果,少一鍵或多一鍵都會出現警告

接下來,我們可以下一個重點:TypeScript 的型別系統與介面的主要差別(之一 XD)。

重點 1. 介面的擴展 Interface Extension / Inheritance

若我們有一系列 TypeScript 介面 I1, I2 ... In,其中:所有的介面裡,不同的介面卻互相有重複的屬性名稱 —— 這種情形是可以接受的;然而,名稱相同之屬性,各自對應之型別不能互相衝突

若滿足以上條件,並且宣告介面 IMainI1, I2 ... In 的擴展:

interface IMain extends I1, I2, ... In {}

IMain 為所有 I1, I2, ... In 交集的結果。

以下範例展示重點裡述說的條件,如果介面各自有重複的屬性名稱,但我們分成 —— 屬性之型別會不會有衝突兩種情況來看。(檢測結果如圖二;錯誤訊息如圖三、四)

https://ithelp.ithome.com.tw/upload/images/20190922/20120614LH23Vmw7bd.png

https://ithelp.ithome.com.tw/upload/images/20190922/20120614BFyaodFtAE.png
圖二:我們的介面範例中,只要出現 I2I3 同時交集的狀況就會產生衝突

https://ithelp.ithome.com.tw/upload/images/20190916/20120614Lttnw1Vm0o.png
圖三:單純對 I2I3 進行交集,結果產生衝突 —— I2I3 介面中,c 屬性的型別不同

https://ithelp.ithome.com.tw/upload/images/20190916/20120614v8F9QWNYG6.png
圖四:跟圖三的訊息一樣

因此讀者必須注意的是:只要多個介面要進行延伸,其中的兩個介面互不相容,就不能進行擴展的動作

介面 Interface V.S. 型別 Type

從剛剛我們得出的結論:介面可以進行延伸或擴展(Interface Extension),這裡就可以開始點出介面跟型別系統的主要差別。

介面(Interface)的意義 —— 跟規格的概念很像,可以擴充設計、組裝出更複雜的功能規格

型別(Type)的意義 —— 代表靜態的資料型態,因此型別一但被定義出來則恆為固定的狀態。儘管可以利用型態的複合(intersectionunion)看似達到型別擴展的感覺,然而這個行為並不叫作型別擴展,而是創造出新的靜態型別

介面本身的意義與好處

另外,還有著名的一句話:

Code against interface, not implementation: Decouple every part of your code and compose from them, instead of short-lived implementation.

直翻就是:

程式碼不應該直接實踐出功能(implementation),而是定義一系列的介面:必須將程式碼拆卸成一系列的小區塊,將主要功能藉由各種區塊組合起來,而非專注於直截了當的實作層面。”

為何我們必須將程式碼進行拆解動作再重新組裝出我們的功能呢?原因有以下:

  • 小單元的程式碼容易管理、寫測試也很簡單(測一個 Function 跟測一整個功能或專案的難度,前者當然較為簡單)
  • 每一小段程式碼進行抽象化(Abstraction),使用起來比較輕鬆容易,組出大功能後也會比較好管理
  • 直接實踐出完整功能,碰到某個未知的環節壞掉,可能還要猜測或者必須整段程式重新查過,很浪費時間
  • 直接實踐出完整功能,程式內部的細節根本很難拔出來個別測試

筆者沒有列出所有原因,要列下去應該還會有更多。

通常一段程式也是一次只做一件事情比較好(跟單一職責原則 SRP:Single Responsibility Principle 的感覺很像)—— 換成介面的想法,通常會把大功能一次拆成好幾個介面,然後再重新組合成想要的功能,其中每個介面都代表功能的一小部分。另外,這些被拆成的小介面有些就可以被重複利用,再組成另一種功能。

貼心小提示

軟體設計裡通常提到的設計模式或概念幾乎主要都是針對 OOP 裡的類別(Class),SRP 也不例外!

原本 SRP 的定義是這樣:

SRP: Single-Responsibility Principle
"A class should have only one reason to change."

很明顯,它指的是 Class,並不是函式、介面、型別、物件等等。因此這裡要澄清一件事情:“筆者只是藉由類似的軟體設計概念進行延伸!”

至於為何筆者要強調這一點呢?

我們不能夠直接把別人講的話誤解或是當成可以運用在各種層面上。如果我們直接把單一職責原則當成嚴格的定義並展開到各種領域的話,我們幾乎所有應用程式或發明就違反此原則:“電腦本身就違反單一職責原則,因為電腦不僅能夠做運算、還能夠做好多事情。” -- 這邏輯還蠻荒唐的啊~電腦生來就是要幫人類處理耗時的事情,哪會說電腦必須符合單一職責原則。(你也可以選擇按台計算機爽爽自己)

一個概念不能被 100% 強行應用在所有領域上,但只要是合理的情況下,就可以被延展到某些領域。筆者這裡即是把 SRP 的概念延展到我們在運用介面上,絕非說:“介面本身就遵照 SRP 原則”。

以下是剛剛有講到的範例:

https://ithelp.ithome.com.tw/upload/images/20190916/201206146OCeEasZcU.png

原本把 UserAccount 設計成所有屬性都摻雜在一起,但是如果將它拆成 AccontSystemAccountPersonalInfo —— 光是這麼做,開發者就可以推測:

  • AccountSystem 跟帳戶的基本運作機制有關
  • AccountPersonalInfo 跟使用者的個資有關

簡單的運用介面進行抽象化的動作就可以更明確的溝通功能主要敘述的東西。畢竟想要讓管理程式變得輕鬆些,就要讓程式寫得很像跟專案的文件ㄧ樣,一目瞭然。

記住這種對介面進行抽象化的感覺 —— 看看另一個容易造成誤會的 type 實踐出同樣跟 UserAccount 的效果,進行 InterfaceType 的意義上概念的比較。

https://ithelp.ithome.com.tw/upload/images/20190916/20120614fsVx6bc48F.png

這兩種分別運用型別系統與介面同時實做出 UserAccount 的效果,大致上感覺沒差(但實際功能還是有點小差別),不過意義上差別可大了

筆者直接不果斷翻譯:

type TUserAccount =
  TAccountSystem &
  TAccountPersonalInfo;

被翻譯出來的意思是:“TUserAccount 的靜態資料格式TAccountSystemTAccountPersonalInfo 的組成”。(感覺有翻沒翻都沒差)

interface IUserAccount extends
  IAccountSystem,
  IAccountPersonalInfo {}

被翻譯出來的意思是:“想要實作 IUserAccount 的介面時,除了必須符合 IUserAccount 本身的屬性與功能外,還必須實作出 IAccountSystemIAccountPersonalInfo 的介面定義出來的屬性與功能”。

哪一個版本聽起來很像在設計功能?後者 interface 的讀法通順很多,因此才會被建議:

"Code against interface, not an implementation"

interface 單字可以類比為 TypeScript 的介面,而 implementation 單字可以類比為型別系統裡的 type

再者,interfacetype 最大差別就是今天一開始講到的:能不能夠被延展(Extensibility)。

TypeScript 介面使用 extends 進行介面延展的這個特性是根本的,就很像是數字從 0 開始數是根本的(可能會從 1 開始也說不定)—— 不會有人從 3.1415926 或 2.71828 開始數數字。

而 TypeScript 型別:不管如何,代表的意義都是指 -- 靜態的格式。『 靜態 』這兩個字對於型別系統代表的意義是根本。不管你再怎麼重組型別,如果是用 type 宣告,你都只是在定義新的型別!如果這種重組行為被歸類為延展的話 -- 相反地,那叫做『 動態 』囉。

因此,大部分針對 TypeScript 介面跟型別的比較結論是:我們可以對介面進行延展,而型別不行

重點 2. 介面與型別的意義與特性
介面與型別各自代表的意義如下:

  • 介面:規格(Spec)的概念,可以組裝、延展(使用 extends
  • 型別:靜態的資料格式,不能被延展,每一次宣告新的型別化名 —— 對型別進行複合形式的操作 —— 都是在定義新的型別,不是延展作用

小結

今天至少開拓了型別 V.S. 介面的比較 —— 這個令筆者頭痛的議題(寫這篇文章跟打 BOSS 沒兩樣),後面我們還要介紹更多介面的功能。

最終的介面與型別的完整比較會在 Day 17. 揭曉~到時候會 Review 到今天學到的東西~


上一篇
Day 12. 機動藍圖・介面宣告 X 使用介面 - TypeScript Interface Intro.
下一篇
Day 14. 機動藍圖・函式超載 X 究極融合 - Function Overload & Interface Merging
系列文
讓 TypeScript 成為你全端開發的 ACE!51
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言