讓 TypeScript 成為你全端開發的 ACE!

  • 1561
  • 0

讓 TypeScript 成為你全端開發的 ACE!

讓 TypeScript 成為你全端開發的 ACE! :: 第 11 屆 iThome 鐵人賽 https://ithelp.ithome.com.tw/users/20120614/ironman https://ithelp.ithome.com.tw/images/ironman/11th/fb.jpg 讓 TypeScript 成為你全端開發的 ACE! :: 第 11 屆 iThome 鐵人賽 https://ithelp.ithome.com.tw/users/20120614/ironman zh-TW Mon, 04 Jul 2022 06:41:17 +0800 Day 50+ 用了會上癮的 TypeScript 新功能 - Easily Addicted New Features in TypeScript https://ithelp.ithome.com.tw/articles/10229832?sc=rss.iron https://ithelp.ithome.com.tw/articles/10229832?sc=rss.iron

貼心小提示

筆者最近開始在整理內容並且使用 TypeScript 實驗並且寫各種小專案,因此 Day 50+ 以後的內容通常都是筆者偶爾會整理的一些很...]]>

貼心小提示

筆者最近開始在整理內容並且使用 TypeScript 實驗並且寫各種小專案,因此 Day 50+ 以後的內容通常都是筆者偶爾會整理的一些很實用的東西給讀者看喔~

畢竟很多讀者 Sub 本串代表 TypeScript 在台灣的需求其實是有的,再加上 TypeScript 憑筆者才幾個月經驗來說覺得挺方便,後面還會分享自己寫的開源專案~

TypeScript 3.7 最新內容

雖然說 TypeScript 版本 3.7 是已經發布幾個月了,但是不免還覺得想跟讀者分享一些東西,因為這些 Feature 實在是太好用了。

ES10 Optional Chaining

讀者有沒有遇過一種情況是 —— 比如說函式裡通常會有所謂的設定相關的東西,通常用英文都是 optionconfig 來代表:

interface SomeConfig {
  a: TypeA;
  b: TypeB;
  c: {
    d: TypeCD;
    e: {
      f: TypeCEF;
      g: { /* ... */ }
    };
  };
};

想當然,以上的程式碼,如果我們要取出設定內部的屬性,就必須要很麻煩地使用一大堆 if 判斷敘述判斷設定之屬性是否存在,因此可能會長得像這樣:

function someFunc(param1, ..., config: SomeConfig) {
  if (config.a) { /* ... */ }
  if (config.b) { /* ... */ }
  if (config.c) {
    if (config.d) { /* ... */ }
    if (config.e) {
      if (config.f) { /* ... */ }
      if (config.g) {
        /* ... */
      }
    }
  }
}

亦或者是使用 && 串下去:

function someFunc(param1, ..., config: SomeConfig) {
  if (config.a) { /* ... */ }
  if (config.b) { /* ... */ }
  if (config.c && config.c.d) { /* ... */}
  if (config.c && config.c.e && config.c.e.f) { /* ... */}
  if (config.c && config.c.e && config.c.e.g) { /* ... */}
}

這樣實在是太麻煩,因此 ECMAScript 確實有 Proposal (名為 proposal-optional-chaining) 使用 ?. 這個串鏈運算子來簡化以上遇到的情形。事實上該 ES Proposal 是 2019 年初(筆者如果沒記錯的話)就已經到 Stage 4,嚴格來說代表它是 ES10 的特色,雖然已經正式釋出,但是因為也釋出快一年左右,所以目前大部分瀏覽器沒支援也是正常,通常都會用 Babel Compiler 來幫忙編譯。

但 TypeScript 於 3.7 版正式支援這個語法特色,其簡化結果如下:

function someFunc(param1, ..., config: SomeConfig) {
  if (config.a) { /* ... */ }
  if (config.b) { /* ... */ }
  if (config.c?.d) { /* ... */}
  if (config.c?.e?.f) { /* ... */}
  if (config.c?.e?.g) { /* ... */}
}

好,有些讀者可能剛學 JavaScript 就直接跳到 TypeScript 或還沒有很關注到 ECMAScript 的近期發展可能覺得這個 config.c?.d 寫法還是有點難轉過來,用口頭來說,意思是:

如果 config.c 的值為一個含有 d 屬性的物件,則呼叫 config.c.d

雖然說大部分文章都會講機制,不過縱貫本系列文的風格,這裡就要出給讀者幾個小問題:

讀者試試看

  1. 如果 config.c 的值為 undefinednull,則:config.c?.d 會出現什麼呢?
  2. 如果 config.c 的值為非物件的原始型別值,如數字 123 或字串 hello world,則:
  • config.c?.d 會出現什麼?
  • config.c 若為字串,則是不是代表 config.c?.[any]後面是不是可以接任何字串相關的方法,例如 reverse()toUpperCase() 等等
  1. 如果 config.c 的值為空物件 {},則 config.c?.d 會出現什麼?

ES10 Nullish Coalescing

讀者看的英文一定覺得很怪,Coalescing 這的動詞到底是啥?其實這是電腦科學專用名詞,代表為『 與...東西結合』的意思,也就是說 —— Nullish Coalescing 代表跟 Null 相關的東西結合的機制。

請讀者注意,是與 Null相關,所以不是只有 null 這個值而已,還包含 undefined

跟 Optional Chaining 相似,Nullish Coalescing 也出自於某個 ECMAScript 官方的 Proposal (proposal-nullish-coalescing),並於去年就已經升到 Stage 4,所以被筆者自個兒歸類為 ES10 的標準。(實際上筆者是根據正式升為 Stage-4 的官方 Commit 的時間為標準;意思是說,如果該 Proposal 被正式升級為 Stage-4 的時間是 2019 年,則筆者認為他就是 ES2019 —— 亦即 ES10 的標準,但精確一點應該是要翻官方文件,不過筆者目前懶得看

相信使用過 JavaScript 一段時間的讀者,會發現使用 &&|| 運算子不全然會回傳布林值 truefalse,但讀者可能會問:

恩!?那 if...else... 內部是怎麼處理那些判斷式的?(提示:toBoolean,或者是自己找書看,推薦《0 陷阱!0 誤解!8 天重新認識JavaScript!》,相信大部分看過鐵人賽文章都知道這本知名書籍~筆者不再多說)

由於上面的那個問題並不在本篇討論範圍,因此就請讀者自行查查囉~

主要是:有一種名為短路語法(Short Circuiting)的技巧,意思是這樣 —— 假設我們想要設定某個變數的值,但是該變數若為空,則需要預設(Default)另一個值:

let message = option.message;

if (!message) {
  /* 若 message 為空值 undefined,則預設為 'Hello World' */
  message = 'Hello World';
}

(以上的語法前提是,我們很確定 option.message 型別為 string | undefined

但是這樣的寫法實在麻煩,所以有一種短路的語法可以將上面的程式碼簡化成:

let message = option.message || 'Hello World';

意思是,如果 option.message 代表的值是 JS 裡偏向於 Truthy 的樣貌,則 message 就會被指派為 option.message 這個值;相反地,若 option.message 代表的值是 JS 裡偏向於 Falsey 的樣貌,則 message 就會指派為 'Hello World' 這個預設值

好,熟悉 JavaScript 的讀者一定知道,什麼是指偏向於 Truthy 或 Falsey 樣貌的值?

以下為代表偏向於 Falsey 的值:

  • 布林值 false
  • null
  • undefined
  • 數字 0NaN
  • 空字串 ''

也就是說,剛剛的程式碼:

let message = option.message || 'Hello World';

如果 option.message 被指派為 —— 假設是空字串 ''message 也會被自動短路為 'Hello World'這個值。但是,如果我們想要的行為是:message 也可以是空字串的話,那麼以上的寫法就錯了!必須改寫為:

let message = option.message || (option.message === '' ? '' : 'Hello World');

意思是:如果 option.message 是空字串,我們也會將空字串的值指派給 message 變數,又可以防止其他 Falsey 的值被指派到 message 變數裡。

哇,不過這樣的寫法也是挺麻煩的,所以也就是說,如果我們想要預設數字,可能也會這麼做:

let someNum = option.num || (option.num === 0 ? 0 : option.num);

(以上的語法前提是,我們很確定 option.num 型別為 number | undefined

這時候,ES10 Nullish Coalescing 的技巧出場了 —— 它的運算子看起來有點好笑,就是兩個問號 ?? —— 以上的 option.message 指派方式的範例可以被簡化為:

let message = option.message ?? 'Hello World';

?? 只會擋兩個東西:nullundefined,這也是筆者在剛剛有稍微強調過:

並不是只有 null,是跟 Null 相關的東西,也就是 nullundefined

Nullish Coalescing 英文是 Nullish —— 是形容詞,不是 Null Coalescing,再次強調不是 Null Coalescing,是 Nullish Coalescing!

所以上面的程式碼等效於:

let message = (option.message !== null && option.message !== undefined) ? option.message : 'Hello World';

這個語法用在預設值方面的技巧非常好用的說,但就一開始可能還是很多問號臨時看不懂,但用久習慣成自然。

其他的 Feature

其實 TypeScript 3.7 發佈的其他 Feature 稍微看過之後,筆者會想講 Assertion Function (斷言式)的部分,但考慮會加入到未來幾個月出的書的內容中,因為會跟 Type Guard 型別檢測的東西還蠻相關的,而正確的使用斷言式也可以改善 Debug 的效率,這也是書中會想要講的東西。(但筆者還在寫稿

另外,會比較建議如果是只有來自前端的背景但沒有使用或很深入 NodeJS 的經驗的讀者,可以嘗試一下 Assertion Function,不過測試相關的內容如果筆者有空的話也可以分享一下,有太多事情可以寫

以下內容為筆者推廣自己的開源專案,但想要看看 TypeScript 寫出的開源專案,可以參考以下內容喔~!

筆者最近寫的專案 —— Wyrd Lang

其實筆者最近也在試著用 TypeScript 寫過一個比較大的開源專案,畢竟累積一些經驗就可以主動提出些寫的過程可能會遇到的困難並記錄起來,並且推廣一下自己寫的東西 XDDD

可能有些讀者看到 Wyrd 這個字覺得很奇怪 —— 沒錯,它是英文的 Weird (古怪的)的古字前身,Repo 裡面的 README 有更詳細的說明

Wyrd Lang 主要是一個語言,想要走偏 Functional Programming 和借鏡一些 Ruby Lang 的語法,編譯出相對應的 JavaScript 程式碼。另外,筆者希望所有的東西都是表達式(Expressions),代表所有的程式碼都會回傳值

貼心小提示

敘述式(Statement)簡單來說是一種結構,並不會回傳值,例如判斷敘述式或重複敘述式:

// 判斷敘述式
if (/* ... */) else { /* ... */ }

// 重複敘述式
for (/* ... */) { /* ... */ }

表達式(Expressions)口語化來說是一種運算過程,理應當然會回傳值,例如運算或邏輯表達式:

// 運算表達式
(1 + 2) * 3 - 4

// 邏輯表達式
a > b || (c < d && e)

不過短短的一個禮拜內,目前大概有可以簡單編譯過後的成果(但主體 Compiler 本身還沒出來 XDD);以下是幾段 Wyrd Lang 對應編譯過後的 JS 語法程式碼:

  1. 基本的運算式與指派式

Wyrd 程式碼:

foo = 1 + (2 - 3) * 4

編譯過後:

const foo = 1 + ((2 - 3) * 4);
  1. 邏輯表達式

Wyrd 程式碼:

not (False or True) and False
8 / (4 * 2) > 3 and not 1 + 2 * 3 == 7 or a + b / c * d != w - x * y

編譯過後:

!(false || true) && false;
8 / (4 * 2) > 3 && !(1 + (2 * 3) === 7) || a + (b / c * d) !== w - (x * y);
  1. 判斷表達式(請注意:是表達式,代表以下的 if...then...if... => 會回傳值)

Wyrd 程式碼:

example1 = if age < 18    => "youngster"
           elif age <= 60 => "adult"
           elif age < 100 => "elder"
           else           => "centenarian"


example2 = if age < 18 then
             "youngster"
           elif age <= 60 then
             "adult"
           elif age < 100 then
             "elder"
           else then
             "centenarian"
           end

編譯過後:

const example1 = age < 18 ? 'youngster' : (age <= 60 ? 'adult' : (age < 100 ? 'elder' : 'centenarian'));
const example2 = age < 18 ? 'youngster' : (age <= 60 ? 'adult' : (age < 100 ? 'elder' : 'centenarian'));
  1. 函式宣告

Wyrd 程式碼:

def addition(x: Num, y: Num): Num => x + y
def complexArithmetic(w: Num, x: Num, y: Num, z: Num): Num do
  a = x + y * z
  b = w - 2 / a + 1
  b
end

編譯過後:

function addition(x, y) {
  if (typeof x === 'number' && typeof y === 'number') {
    return x + y;
  }

  throw new Error('Wrong Parameter Type in function \`addition\`');
}

function complexArithmetic(w, x, y, z) {
  if (typeof w === 'number' && typeof x === 'number' && typeof y === 'number' && typeof z === 'number') {
    const a = x + (y * z);
    const b = w - (2 / a) + 1;
    return b;
  }

  throw new Error('Wrong Parameter Type in function \`complexArithmetic\`');
}

但最近由於筆者自己開始會在編譯過程中紀錄型別內容等等,未來應該會編譯成更單純的樣子:

function addition(x, y) {
  return x + y;
}

function complexArithmetic(w, x, y, z) {
  const a = x + (y * z);
  const b = w - (2 / a) + 1;
  return b;
}

貼心小提示

通常編譯器的角色是要在編譯過程中檢測語法錯誤,因此在函式的宣告部分,筆者之所以會改成更簡單的形式的原因在於:如果編譯出的結果還要讓 JavaScript 再做一次型別檢測,豈不是很諷刺的一件事情?

另外,如果單純只是翻譯 Wyrd 到 JavaScript 的話,那這個 Wyrd Lang 應該比較偏向於 Interpreter 或者是 Transpiler 的概念,就是單純轉譯,而型別等內容都是由 JavaScript 來處理。

以上大概是筆者目前 Wyrd-Lang 開源專案初步的進展 XDD

另外,有興趣或者想要一起精進 TypeScript 的讀者,亦或者是想要試著在 GitHub 上貢獻並且放在履歷經歷的讀者們,筆者誠心歡迎大家去 Maxwell-Alexius/Wyrd-Lang 可以開 Issue 討論甚至是想要送 PR 都可以~(P.S. 寫測試是一種很好的起手式唷!)

其實寫過一次編譯器過後,覺得當時發明 JavaScript 的 Brendan Eich 真的還蠻強的,可以短短時間內 Prototype 出一個語言,自己還有很多東西還可以在精進。

未來的文章應該也會分享自己寫專案中遇到的種種狀況與心得~

]]> Maxwell Alexius 2020-01-27 16:35:47
Day 50. 通用武裝・非同步函式X非同步程序的同步化-TypeScript Generics with Asynchronous Programming III. Async Functions https://ithelp.ithome.com.tw/articles/10228649?sc=rss.iron https://ithelp.ithome.com.tw/articles/10228649?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

  1. Generators 使用上有哪些特點?
  2. 積極求值(Eager Evaluation)與惰性求值(Lazy Evaluation)分別特點在哪裡?各自優勢在哪?

另外,本篇承載前一篇文章的內容,強烈建議如果跳到這一篇的讀者務必把前一篇看完啊!

這系列不知不覺剛好到第 50 篇了,感覺接下來是下一篇章的開頭。

不過呢,由於本系列即將要跟出版社接下出書方面的事宜,筆者有考慮把接下來的篇章整合到未來出版的書本裡喔~(畢竟把全部的東西擺在網路上出書就沒啥意義了~);內容也會跟網路文章系列部分雷同但是也有取捨,並且新增自三個月自己實驗 TypeScript 寫小型專案以來的常見經驗與心得。

另外,由於正式出書部分可以寫的內容變多,筆者自認為可以再把本系列的某些原本規劃好卻打掉的細節也寫進來,實在是很迫不及待地開工啊~

如果後續有什麼樣的更新會在本系列補充~~~,所以可以繼續關注一下目前的動態喔~

至於書名是啥,筆者其實回去有 Contemplate 一番~(高級的晶晶體無誤!)

  • 跟本系列名稱一樣:《讓 TypeScript 成為你全端開發的 ACE!》
  • 《射出全端的穿雲箭 —— 我現在要用 TypeScript 出征!》
  • 《學習 TypeScript —— 用 18% 的力量學到 81% 的功力》
  • 《用 TypeScript 反滲透全端的開發生態》

(喂!不要亂搞啊!)

好的~廢話不多說,以下正文開始~

TypeScript 非同步編程第三彈 Asynchronous Programming in TypeScript

回歸正題 —— 非同步的編程到底跟 ES6 Generators 有什麼關係?

有些想知道 Async-Await 的讀者,看到這裡可能以為:“作者是不是忘記要講 Async-Await 的由來了?我到底還要看這東西看多久?”

本篇就會討論到了啦!但是前面的東西不講,讀者可能也沒法體會 Async-Await 從 Promise Chain 到 Generators 再變成 Async-Await 的演變過程 —— 對,Generators 這個的東西真的很重要!

筆者再把演變的口訣提出來稍微做些前情提要:

啟始於 Callback Hell,攤平為 Promise Chain,乘載於 Generator Functions,演變成 Async Functions

Originated from Callback Hell, flattened into Promise Chain, combined with Generator Functions, evoluted as Aysnchronous Functions.

好的,我們接下來要 Focus 在這句話上:

乘載於 Generator Functions

貼心小提示

以下的難度會超級、巨幅地提升(筆者不開玩笑),讀者不需要看得懂程式碼過程 —— 只要記得演變過程與結論,心理有數就夠了

首先,既然是從 Promise Chain 開始,承載於 Generators 的特點,這到底意味是什麼呢?

走一下筆者的思路 —— 筆者希望把 Promise Chain:

https://ithelp.ithome.com.tw/upload/images/20191018/20120614r19NhEZRe7.png

將裡面的 request 看作成迭代器的元素 —— 讀者看到這裡可能會疑惑,但事實上筆者想要將 Promise Chain 改寫成:

https://ithelp.ithome.com.tw/upload/images/20191018/20120614iTIotx2DYs.png

首先,如果第一次建立此 sendRequestGenerator 產生的迭代器,每一次 next 就相當於 yield 出一個 Promise<T> 物件,並且下一次呼叫 next 時填入某個值 —— 該值就會取代 yield request 部分並且指派到 response 變數去。

更簡單的想法是:

每一次 yield request 出去時(Pull 出一個 Promise<T> 物件)—— 會將該 requestresponse 回傳回來(Push 進該 Promise<T> resolve 出來的結果)

以下是更完整一點的範例程式碼,以下用 Promise Chain 表示形式示範。(結果如圖一)

https://ithelp.ithome.com.tw/upload/images/20191018/20120614pIGVopzGb6.png

https://ithelp.ithome.com.tw/upload/images/20191018/20120614LqIp6Bn7JW.png
圖ㄧ:Promise Chain 連續執行 pingRequest 的結果

另外,將 Promise Chain 轉換成 Generator 表示形式如下:

https://ithelp.ithome.com.tw/upload/images/20191018/201206149H8LAKowlU.png

這樣的形式是不是優雅許多~ 不過呢,相信敏銳的讀者早已察覺到,直接去 Run 這個 Generator 肯定沒用,勢必要用手動的方式去建立迭代器後,再不停地用 next 方法呼叫、迭代 Promise<T> 物件。以下這段程式碼,非常麻煩,但是測試結果(如圖二)ㄧ樣可以達到跟 Promise Chain 的效果。

https://ithelp.ithome.com.tw/upload/images/20191018/20120614StvE1pLruZ.png

https://ithelp.ithome.com.tw/upload/images/20191018/201206146RbA2R1S9x.png
圖二:使用 Generator 也可以達到跟 Promise Chain 相同的效果

這裡筆者就不強迫讀者要看得懂以上的過程(除非你有興趣與熱忱),但筆者想要跟讀者講的重點是:

使用 Generator 可以達到跟 Promise Chain 相同的效果

可是讀者一定會問:“作者你跟我開玩笑?裡面用超多層回呼函式的,根本比 Promise Chain 更悲劇啊!”

事實上呢,以上的那一段過程可以用遞迴(Recursion)的方式表示(以下程式碼的運行結果如圖三)。

https://ithelp.ithome.com.tw/upload/images/20191018/201206142ED2sby9tE.png

https://ithelp.ithome.com.tw/upload/images/20191018/20120614VDKRjtB1Ii.png
圖三:使用遞迴可以將 Generator 中的不同 Request Run 起來~

所以呢,筆者證明了:

可以藉由 Generators 的 Push-Pull 模式的概念 —— 結合遞迴(Recursion)的技巧,完整地實現非同步程序的運行

另外,這樣的 Generators 寫法比起 Promise Chain 更有好處的原因在於,你可以用平常寫指令式程式碼的想法,簡簡單單地進行 Error Handling 的邏輯:

https://ithelp.ithome.com.tw/upload/images/20191018/20120614LwV3UIatHV.png

以上使用 try...catch... 的方式,在 Promise Chain 幾乎很難做到,但是使用 Generator 就可以盡情地使用 try...catch... 語法 —— 處理錯誤方面的邏輯會更加簡單呢!

基本上,以程式設計的角度 —— 讀者可以這麼想:

Promise Chain 把非同步運作的邏輯過程用很強硬的方式直接串聯在一起,錯誤處理又必須要被隔離到.catch 部分,造成功能必須要強行被拆解掉。

如果想要在 Promise Chain 中途進行任何處理動作,就必須得再中間多串一些 then 等等的操作。

而 Generators 則是把 Promise Chain 中的每個 Promise 拆成迭代器中的一個個元素,元素與元素中間可以自由添加任何邏輯,包含錯誤處理(Error Handling)等。

這使得我們可以不需要使用 Promise API,而是很直觀的編寫程式的方式去寫出 response = yield promiseObject 之類的邏輯結合 try...catch... —— 可以專心在程序的敘述過程

另外,Generators 比起 Promise Chain,多了一個 runGenerator 函式,該函式只是負責將 Generator 的運作過成抽離出來而已。

重點 1. Generators 在非同步編程的演變過程中的角色

運用 Push-Pull Model 的性質 —— 結合遞迴(Recursion)的手法,我們可以取代 Promise Chain 的語法行為。

而 Generators 的寫法好處在於,我們可以自由地在 Generators 內部:

  • response 進行 Data Reshaping 的邏輯
  • 進行 try...catch... 相關的錯誤處理
  • 甚至你也可以 yield Promise.all([...]) 對多個請求進行平行化處理

讀者也可以參考 tj/co 這個 GitHub Repo.,筆者就是採用類似這個 Repo. 的手法展示 —— 如何使用 Generator 達成非同步的程式碼序列執行的過程。

漫長的 Generators 演變歷程就到此結尾,最後就是 Async Function 的演變結果還沒被筆者講完喔~!

最後的一塊拼圖前的 Recap.

在非同步的程式運作過程中,通常最簡單的處理方式是使用回呼函式 —— 當非同步程序執行完畢時,再呼叫回呼函式處理。

但多層的回呼函式會造成程式碼:

  • 層層疊疊非常混亂
  • 錯誤處理會非常麻煩
  • 不同區塊的非同步程式耦合度超高,不易進行功能上的拆解重複再利用

這時,ES6 Promise 將這個非同步程序使用類似 State Machine 的方式 —— 使得處理非同步程序從巢狀回呼函式,攤平成一系列的 Promise Chain,解決了:

  • 巢狀語法較不易快速理解
  • 不同區塊的非同步程式碼可以解耦(Decouple)成一個個 Promise 物件,自由串在一起

但 Promise 物件仍然在語法的寫法上還是稍嫌麻煩:

  • Promise Chain 比較不 DRY 的地方在於 —— then 這個東西出現次數超多
  • 錯誤處理還是稍嫌麻煩些,儘管可以隔離掉,但是沒有像 try...catch... 語法方式,直觀地寫得特別清楚

ES6 Generators 為迭代器的產生函式,透過 Push-Pull Model 方法以及惰性求值(Lazy Evaluation)的概念 —— 我們可以將不同區塊的非同步程式碼,使用 Promise 包裝起來外,將每個 Promise 視為迭代器裡的元素,讓它可以在 Generator 進行迭代,並且在外部使用遞迴(Recursion)的方式,同樣可以模擬出非同步程式依序執行的情境。

而 Generator 的好處就是:

  • Promise Chain 被串起來的行為再被抽象化成外部的遞迴演算法,因此我們可以專心處理非同步程式碼內部的主程序
  • 裡面可以將非同步程式碼,藉由 yield 方式,寫得很像同步(Synchronous)的程式碼
  • 錯誤處理更直覺,可以直接使用 try...catch... 敘述式

ES7 Async-Await 的運作機制

啟始於 Callback Hell,攤平為 Promise Chain,乘載於 Generator Functions,演變成 Async Functions

Originated from Callback Hell, flattened into Promise Chain, combined with Generator Functions, evoluted as Aysnchronous Functions.

事實上,這個演變非常簡單啊!(而且也可能令人感到白癡!)

就是把 Promise Chain 換作的 Generator 形式:

  • 將 Generator Function 改成 async function 寫法
  • yield 關鍵字取代成 await

所以以下這個程式碼中的 async function 形式等效於 Generator 的手法。

https://ithelp.ithome.com.tw/upload/images/20191021/20120614mxwEhcjKQb.png

但請切記,Generator 必須要有筆者宣告過的遞迴迭代方式去處理 Request —— 然而,Async Await 語法就已經將該邏輯幫你做掉了

另外,由於 Async Await 是非同步的語法,自然而然輸出的東西會是 —— 你應該想得到的 —— Promise 物件! (推論結果如圖四)

https://ithelp.ithome.com.tw/upload/images/20191021/201206148nGv1qhEwc.png
圖四:輸出為 Promise<void> 則是因為,該 Async Function return 的東西為空,因此才是 void 呢!

所以讀者也不需要擔心說 —— 如果想要寫成 Generator,還要自己客製化遞迴函式來處理該 Generator 的流程。

不用那麼麻煩!

Async functions comes to the rescue!

使用 Async Await Function 的手法很簡單,就很像是在呼叫 Promise 物件ㄧ樣。(以下程式碼執行結果如圖五)

https://ithelp.ithome.com.tw/upload/images/20191021/20120614zANpPBHV3y.png

https://ithelp.ithome.com.tw/upload/images/20191021/20120614EloShy09gE.png
圖五:就是當成 Promise 物件ㄧ樣在執行,差別在於它是函式,必須要呼叫

不過你倒是也可以將其想成它是回傳 Promise 物件的函式也可以 —— 如果遇到以下的程式碼,猜猜看會如何執行?

https://ithelp.ithome.com.tw/upload/images/20191021/20120614AV0W35OENn.png

畢竟本系列是 TypeScript 系列,我們當然要強調 TypeScript 的好處 —— 型別的推論(Type Inference)是很棒的工具!

請看推論結果。(圖六)

https://ithelp.ithome.com.tw/upload/images/20191021/20120614us3H8VshyB.png
圖六:看到了沒~Async Function 回傳的值,儘管不是非同步程序相關的東西,如 Promise 物件,但回傳值都會包裝一層 Promise。

另外,你也可以在 Async Function 執行另一個 Async Function,畢竟 await 都是等待 Promise 相關的非同步程序的結果,而 Async Function 回傳的結果都會是 Promise 物件。

由於細節討論下去太多,筆者只 Cover 重點演變部分,剩下的語法過程驗證一方面是直接請讀者讀 Doc,一方面就是試試看筆者列出來的一些案例。

以下筆者就在讀者試試看的部分列出一些案例,讓讀者可以去試試看這些程式碼會怎麼運作。

讀者試試看

以下範例裡使用到的 delayAfter<T>(milliseconds: number, value: T): Promise<T> 函式定義如下:
https://ithelp.ithome.com.tw/upload/images/20191021/20120614gDEntz4fEc.png

  1. 請問以下程式碼大概會如何運作?
    https://ithelp.ithome.com.tw/upload/images/20191021/20120614t5Y6yzdJ9q.png

  2. 請問以下程式碼大概會如何運作? TypeScript 會在 await 關鍵字部分出現什麼樣的訊息提醒你?
    https://ithelp.ithome.com.tw/upload/images/20191021/20120614bfW5uRu2WB.png

  3. 請問以下的程式碼,會印出什麼樣的結果,並且執行時間為何?
    https://ithelp.ithome.com.tw/upload/images/20191021/20120614WTQQ5pwzVn.png

  4. 請問以下的程式碼,會印出什麼樣的結果,並且執行時間為何?
    https://ithelp.ithome.com.tw/upload/images/20191021/20120614pd78l20cyv.png

  5. 請問以下的程式碼,差異在哪?
    https://ithelp.ithome.com.tw/upload/images/20191021/20120614nuwkeyea7g.png

  6. 請問以下的程式碼會如何運作?
    https://ithelp.ithome.com.tw/upload/images/20191021/201206147IcqEDiHnE.png

  7. 如果想要設計一個 Request Timeout Feature,並且進行錯誤處理,你會如何運用 Async Function 與 Promise 設計?

  8. 如果 async function 被積極註記輸出為 Promise<number> 代表什麼意思?若 async function 被積極註記為非 Promise<T> 類型的物件會發生什麼事?

重點 2. 非同步函式 Asynchronous Function

自 Generator 結合 Promise Chain 的概念演化出來的結果,為 ES7 的標準。

非同步函式可以用同步的語法撰寫各種非同步行為,使得:

  • 程式碼可讀性大幅提升
  • 提供另一種方式操作 Promise 物件,可以使用 await 來等待 Resolve 出來的結果
  • 錯誤處理更直觀,可以直接使用 try...catch... 敘述式寫非同步程序的錯誤處理部分
  • 非同步程序可以進行高度抽象化
  • 非同步程序又可以互相用 await 組合(await 其他 Async Function 的執行結果)
  • await Promise.all 可以等待多個非同步程序的執行結果
  • await Promise.race 可以等待多個非同步程序的其中一項最先執行完畢的結果

非同步函式的推論結果必須要是 Promise<T> 類型的結果,如果積極註記為非 Promise<T> 相關型別的物件,TypeScript 會出現警告!

非同步函式跟普通的 Promise 物件差別在於 —— 非同步函式是函式,Promise 是物件 —— 非同步函式必須要有括弧進行呼叫的動作。(這應該是廢話

小結

筆者已經把非同步編程部分大致上已經 Cover 完畢囉~

沒意外這應該會是本系列的結尾,也是本系列篇章《通用武裝》的結尾~

接下來是不是該出《進化實驗》篇章呢?也就是 Decorator 相關的語法與用途~ 這裡就留待未來出書內容進行詳解吧~ 因為後面要寫的東西可不是開玩笑的簡單,書本可以做得到的事情讓書本來補充

也感謝支持本系列的讀者們以及舉辦第 11 屆鐵人賽的主辦單位 —— IT邦幫忙團隊,認可本系列為 Modern Web 組的其中一個冠軍文章系列~ (好像有點太晚 Celebrate?

但筆者還是補充一下,未來出的書本中,裡面不會出現的內容,本系列文章會再繼續更新下去,所以可以照樣關注下去喔~

]]> Maxwell Alexius 2020-01-16 15:56:16
Day 49. 通用武裝・非同步迭代 X 無窮地惰性求值 - TypeScript Generics with Asynchronous Programming II. ES6 Generators https://ithelp.ithome.com.tw/articles/10228504?sc=rss.iron https://ithelp.ithome.com.tw/articles/10228504?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

  1. 同步與非同步程序的差異性在哪?你能夠列舉哪些是 JS 裡有非同步的機制的東西嗎?
  2. 為何我們需要非同步的程序?同步執行不是很直觀嗎?
  3. 回呼函式地獄(Callback Hell)可以用什麼方式解決?

另外,本篇承載前一篇文章的內容,強烈建議如果跳到這一篇的讀者務必把前一篇看完啊!

今天要講比較麻煩一點的東西,也許是本系列最難的部分吧!

相信讀者經歷過這一段過程,應該就連 ES6 Generators 的語法精髓都順便學完了。

以下正文開始

TypeScript 非同步編程第二彈 Asynchronous Programming in TypeScript

二部曲 —— 乘載 Generator Functions 的特性

ES6 Generator Functions 的介紹

事實上,從這裡開始,筆者要順便介紹 —— ES6 Generators 在搞什麼,不過在理解這個東西前,筆者建議讀者好好看過迭代器模式篇章,裡面的內容有涵蓋到 Generator Function 的特性,因為:

Generators 可以看作是 JavaScript 版本的產生迭代器的 Factory Method

『恩!?好像看得懂又好像看不懂!?』

貼心小提示

若想要追求名詞的精確性 —— ES6 Generator Functions 儘管被筆者形容成可以看作為迭代器模式中的 Factory Method 部分,但它並不是用 OOP 裡面的類別的成員方法來達成,而是單純的一個函式作為所謂的 Factory Method;不過 Method 這個詞的定義依然跟 Function 有差,所以 Generator Functions 應該要看作為迭代器的 Factory Function 而非 Method。

但是! —— Design Pattern 原著裡,因為本著 OOP 的實踐基底,所以並沒有所謂的 Factory Function 這一套說法 —— 這個詞是筆者產出來的延伸物,不是那些大神們的說法,筆者只是小蝦米而已。

事實上,學過 Python 的人應該也有接觸過 Generators,如果你有這樣的背景,事實上你早就知道大部分 JavaScript 的 Generators 的運作機制,差別僅僅在於 —— 如果迭代器已經到尾端,繼續呼叫 next 方法時,Python 會丟出類似 Out of bound 之類的 Exception,而 JavaScript 不會。這一段如果看不懂也沒差,因為我們在講 JavaScript,而不是一條滑滑的爬蟲類動物。XDDDDD

回過頭來,筆者開始介紹 Generator Functions,而且會結合 TypeScript 的型別系統推論機制 —— 這可是本系列天大的重點啊!啊!啊!啊!啊!(其實筆者只要感嘆一次就好

以過往的慣性,就從最簡單的 Generator Function 的範例出發:

https://ithelp.ithome.com.tw/upload/images/20191015/20120614KnBOokeHIp.png

沒看過的讀者云:“嘖嘖,麻煩死了,那麼多東西要學。”

這裡劇透一下,為了追求更好的寫法 —— 尤其是 ES7 Async Await 的 Feature,筆者自從會了這東西,突然覺得寫 JS 的過程,前途一片光明(請不要誇大) —— 但在這之前,必須要先好好介紹 ES6 Generators。

回過頭來,以上的 numbersIteratorGenerator 為產生某種迭代器的函式,使用情形如下。(程式碼結果如圖ㄧ)

https://ithelp.ithome.com.tw/upload/images/20191016/20120614tPADVgtchQ.png

https://ithelp.ithome.com.tw/upload/images/20191016/20120614TnPD0KexGG.png
圖ㄧ:numbersIter 不停呼叫 next 方法時,輸出的格式皆為 { value: number | undefined, done: boolean }

筆者把結果呈現出來:

https://ithelp.ithome.com.tw/upload/images/20191016/20120614BTJBGfSBev.png

不覺得很像迭代器模式篇章提過的 —— 迭代器的長相嗎?(這句應該是廢話

而這裡將迭代器的元素邏輯 —— 用一個函式的流程,將迭代的元素改成用 yield 關鍵字進行輸出的動作;並且,每個元素輸出時的結構為普通的物件搭配 valuedone 這兩個屬性 —— 分別代表當前迭代元素之值以及迭代器的狀況(迭代完畢時為 true)。

不過這裡還是要雞婆的提醒讀者:這個 Generator Function 的 Feature 不是 TypeScript 提供的,這是列為 ECMAScript 標準的語法!(這句應該也是廢話

重點 1. ES6 Generator Functions

若想要建立一個迭代器產生函式(Generator Functions),可以宣告一個 ES6 Generator 函式,並且必須在 function 以及函式名稱之間註記一個 * 字號 —— 而迭代器的元素可以使用 yield 關鍵字進行輸出:

https://ithelp.ithome.com.tw/upload/images/20191016/20120614liqZXn2JtK.png

以下簡稱 Generator Functions 為 Generators。

若想要從 Generators 建立一個迭代器(Iterator),可以直接呼叫該函式,視情況填入函式之參數。

https://ithelp.ithome.com.tw/upload/images/20191016/20120614KlsbMAjkTP.png

每一次迭代一個新的元素必須呼叫 next 方法:

https://ithelp.ithome.com.tw/upload/images/20191016/20120614BgGTw0tyMs.png

每一次迭代出來的元素結構為:

https://ithelp.ithome.com.tw/upload/images/20191016/20120614eyjZKnfylz.png

其中,若迭代器還未迭代完畢,done 的狀態為 false,反之為 true。(天大廢話一籮筐

由於本 TypeScript 系列要探討的重點跟型別系統有關的東西,因此筆者就把語法基本層面 —— 讀者可能必須要知道的事項放在讀者試試看的部分。畢竟筆者可以選擇直接把 MDN 或其他篇文章已經產出的語法教學複製貼到這裡來,但筆者覺得,改成用測試的方式看看讀者能不能在學習過程中注意到細微的點,如果讀者主動測試過,筆者也不需要耗費太大力氣,讀者也至少會有印象,碰過這些東西。

讀者試試看

請讀者親自試試看或者查個資料也行,看能不能回答以下問題:

  1. 試問以下 generator1generator2 差別在哪?

https://ithelp.ithome.com.tw/upload/images/20191016/20120614XUmweEqUPV.png

  1. 假設我們有一個 generatorFunc 為一個 Generator Function,試問從該函式剛建立一個迭代器時,裡面的 console.log 會跑到嗎?還是要等到呼叫第一個 next 才會跑到呢?

https://ithelp.ithome.com.tw/upload/images/20191016/20120614CGhi7bFuBw.png

  1. 試問以下的 Generator 會產生什麼樣的效果?

https://ithelp.ithome.com.tw/upload/images/20191016/20120614ULYN2EgfLn.png

Generators 本身的型別推論 Type Inference of Generators

很簡單,直接用滑鼠 Point 到 Generator 函式位置就會出現提示,舉剛剛的 numbersIteratorGenerator 為例,它的推論結果如圖二。

https://ithelp.ithome.com.tw/upload/images/20191016/201206141MfMOe2BZu.png
圖二:輸出好長...

以下筆者整理一下輸出:

https://ithelp.ithome.com.tw/upload/images/20191016/20120614DamFf2FNGi.png

推論結果裡出現一個很陌生的東西 —— Generator<X, Y, Z> 型別,該泛用型別有三個型別參數,這可真是複雜,但是請讀者謹記,我們還是可以繼續鑽下去查看 Generator型別宣告(Type Declaration)部分。(如圖三)

https://ithelp.ithome.com.tw/upload/images/20191016/20120614HdNhiLIciP.png
圖三:Generator 的型別定義(Type Definition)

原來 Generator 是一個介面 —— 擴充自 Iterator<T, TReturn, TNext>。(參見介面擴充篇章

這時筆者就要得意的說,看到了沒 —— 這就是筆者所謂的 TypeScript 的好處:

筆者完全不需要上網查詢,直接在編輯器裡就可以查到陌生的型別的規格內容,並且理解內部的結構

筆者就把規格拔下來給讀者看:

https://ithelp.ithome.com.tw/upload/images/20191016/201206148OUOnG8ZQn.png

ES6 Generators 迭代器的結構就被筆者曝光了!

  • next 方法就是輸出 TNext 型別的東西
  • 我們剛剛沒有介紹 return 這個方法,但筆者個人覺得 —— 由於沒用到所以不再介紹,讀者可以自行研究
  • throw 方法看起來是錯誤情形時使用的方法
  • [Symbol.iterator]() —— 這東西筆者賣個關子,覺得後面講到 Symbol 以及 for...of... 迴圈可以補充

反正 Generator 的參數分別為 TTReturn 以及 TNext,而剛剛的 numbersIteratorGenerator 輸出之結果為對應為:

  • T1 | 2 | 3 | 4 | 5
  • TReturnvoid
  • TNextunknown

筆者這邊大致猜得出來,T 指得是所有從迭代器 yield 出來元素的 unionTReturn 則是因為我們的 Generator 函式內部沒有使用 return 表達式,所以才會為 void

TNext 這個東西,筆者下一篇會再講到為何是 unknown 型別。

所以如果我們正常使用 Generator 的迭代器,推論結果會很有趣。(如圖四)

https://ithelp.ithome.com.tw/upload/images/20191016/20120614oDVxunLqZe.png
圖四:IteratorResult<1 | 2 | 3 | 4 | 5, void>

如果再查 IteratorResult 這個介面會長這樣。

https://ithelp.ithome.com.tw/upload/images/20191016/20120614bMHXnsLIXf.png

這應該更明顯了吧:一個是指 yield 出來的元素型別、另一個則是 return 出來的型別,兩個 union 起來的結果!

所以呼叫 next 方法輸出的結果,如果迭代器還沒迭代結束,就會是 1 | 2 | ... 5 型別,而如果迭代結束就會成為 void 型別 —— 但迭代結束實際上是輸出 { value: undefined, done: true } 這個物件。

接下來要繼續講述 ES6 Generators 的特點,以及 Generators 在 ECMAScript 標準裡很重要的原因。

推入與輸出 Push-Pull Model

事實上,從上一篇解剖 Generator<T, TReture, TNext> 這個型別背後的介面長相,筆者必須要把這一行特別 Highlight 出來給大家看:

https://ithelp.ithome.com.tw/upload/images/20191017/201206141rwoAHTxry.png

這一行 next 方法的結構是 next(...args: [] | [TNext]): IteratorResult<T, TReturn> —— 先不要被這個結構嚇到,...args: [] | [TNext] 的意思代表 —— 匯集所有在 next 方法裡的參數只能為兩種情形:

  • [] 代表沒有任何參數
  • [TNext] 代表只能存在一個參數符合 TNext 的型別(別忘了,這是元祖型別!代表該陣列只會有一個元素,此元素型別為 TNext,剛開始會搞混也是很正常!)

讀者有想過,我們從 Generators 建構出的迭代器,使用 next 方法時傳入參數的意思是什麼呢?

筆者就來簡單的方式呈現,以下為 summationGenerator 迭代器產生函式:

https://ithelp.ithome.com.tw/upload/images/20191017/20120614DFxEmxWg8o.png

讀者看到這一行應該覺得很怪,以下這一行:

total += yield total;

這是什麼意思?

首先,筆者先把 summationGenerator 產生的迭代器,使用起來的效果給讀者看看。(以下程式碼結果如圖五)

https://ithelp.ithome.com.tw/upload/images/20191017/20120614PJIzJo7HoH.png

https://ithelp.ithome.com.tw/upload/images/20191017/20120614xxiGUxO8Dz.png
圖五:summationGenerator 產生的迭代器 summationIter 使用起來的效果

連續呼叫三次且分別在 next 方法傳入數字 5711 —— 結果出來的結果依序為 0718

其實筆者舉這個迭代器函式的用意是希望可以進行累加的功能,不過讀者應該覺得奇怪,第一個迭代出來的數字結果為何不是 5 而是 0

這裡就不賣關子,直接說明迭代器產出函式及其產出的迭代器的 next 方法特點:

重點 2. Generators 的性質

  1. 從 Generators 產出的迭代器不會馬上動作
  2. 執行過程上:迭代器呼叫第一次的 next 方法,該迭代器就會執行到 Generator 函式的第一個 yield 位置;亦或者,如果沒有 yield,就會執行到 return 為止
  3. 迭代過程上:迭代器呼叫第一次的 next 方法,並且執行到第一次 yield 的位置時,會將 yield 旁邊的值(可能也包含 undefined)進行輸出的動作;如果沒有 yield 則是會將 return 的結果值輸出
  4. 迭代器只要在第 n 次呼叫 next 方法並且代入任何值,該值就會取代前一個 yield 關鍵字的位置

這邊很複雜,讀者看不懂也沒問題,直接看筆者分解過程後,再回來看重點 1 應該會很清楚筆者想講什麼。

首先,第一次(n = 1)呼叫 summationIter.next(5) 時,由於是第一次呼叫,但此時迭代器的初始地點,也就是函式的一開始根本沒有 yield 關鍵字,也就是說 5 這邊有填沒填都沒差。(如圖六)

https://ithelp.ithome.com.tw/upload/images/20191017/20120614cDi6uGfSZB.png
圖六:第一次呼叫 next 是在函式的開端,根本不會有 yield 關鍵字供數字 5 取代

所以剛開始 total = defaultValue,而呼叫 summationGenerator 由於沒有填入參數,因此預設為 0,所以 total = 0 —— 遇到第一次的 yield total 時,輸出為:

{
  value: 0,
  done: false
}

第二次(n = 2)呼叫 summationIter.next(7) 時,由於是第二次呼叫,它會將第一次呼叫 next 跑到的 yield 位置取代為數值 7;也就是說,將 yield total 看成 7 這個數字,就會變成:

while (true) {
  total += 7; // 原本是 yield total;
}

然後繼續執行直到遇到下一個 yield,不過剛執行完 total += 7 時,就已經碰到迴圈的底,因此重新執行迴圈,又遇到一次 total += yield total,於是停住 —— 此時的 yield total 出來的值是原本的 defaultValue = 0 再加上第二次呼叫 next(7) 的數值 —— 0 + 7 = 7,因此第二次輸出結果為:

{
  "value": 7,
  "done": false
}

第二次輸出後就停掉,直到第三次(n = 3)我們呼叫 summationIter.next(11) 時,我們要把第二次跑到的 yield total 的值,更改成第三次呼叫 next 時的輸入值 11,所以可以看成:

while (true) {
  total += 11; // 原本是 yield total;
}

然後開始跑,此時的 total += 117 + 11 = 18,而剛好接下來執行時又碰 while 迴圈的底,於是重跑,又撞到第四次 yield total —— 此時的 total = 18,因此輸出結果為:

{
  "value": 18,
  "done": false
}

所以呢,讀者應該可以感覺到:

n 次呼叫 next 方法時,會執行到第 nyield 位置

這應該很直觀,但是:

n 次呼叫 next 方法時,填入的參數值會取代第 n - 1yield 位置

這觀念非常重要,而且我們執行第一次 next 時,如果有填入參數,但是想要取代 n - 1 = 0yield 位置是不可能的事情,因此筆者才會說:第一次呼叫 next(5) 時,裡面的 5 這個數字有填沒填都沒差 —— 通常第一次呼叫 next 大部分情形都有點像是在初始化(Initialize)迭代器內部的過程

好的,筆者要再給予這個 Generator 的這個特性兩個名詞:

  • 呼叫 next 方法時,從迭代器取出值的過程,稱之為 PULL
  • 而呼叫 next 方法並且填入參數的這個行為,就好像是在迭代器這個黑箱子外面提供資訊輸入進去,以改變迭代器後續的輸出結果樣貌,這樣的輸入行為稱之為 PUSH

這樣的行為,讀者可能覺得還好,但是筆者要強調:

重點 3. Generators 的優點

普通的迭代器模式中的迭代器是靜態(Static)的,代表內部的值是固定的 —— 每一次從迭代器模式的 Factory Method 建立出的迭代器都會是固定的。

然而,ES6 Generators 的特點在於 —— 藉由 Push-Pull 的機制,可以達到迭代出來的元素可以根據不同的情形更改元素輸出的狀態,因此可以將 Generators 產出的迭代器視為動態(Dynamic)的。

筆者 O.S.:之前講解 OOP 迭代器模式不是沒理由的講解,而是可以比對 Generators 的迭代器與 OOP 迭代器模式的差別在哪裡。

惰性求值 Lazy Evaluation

另外,讀者應該也會發現,在 summationGenerator 和前一篇有講到的 fibonacci Generator 裡,筆者有用到 while 的無窮迴圈 —— 這裡筆者必須要講到另一個動態迭代器的特點 —— 惰性求值(Lazy Evaluation)的概念。

首先,讀者有沒有想過,有沒有辦法表示一個數列代表所有的正整數

讀者可能覺得筆者在開玩笑:“電腦的 Memory 又沒辦法承載所有的數字,哪有辦法說使用陣列或 Space Complexity 更大的 Linked List 來表示?”

不過,設想一個情境,如果我們想要使用這種數列,但是並不是馬上使用,而是需要的時候再用呢

於是,想當然以下的 positiveNumberGenerator 定義出的迭代器行為就可以達到剛剛所說的目標:

https://ithelp.ithome.com.tw/upload/images/20191017/20120614PcHq754v2Y.png

以上的 positiveNumberGenerator,同時滿足:

  • 產出的確實是迭代器會有的行為
  • 表達一個全部都是正整數的數列

跟普通使用陣列的情形:

[1, 2, 3, 4 ... n, n + 1, ...]

完全是不ㄧ樣的 —— Generators 產出的迭代器 —— 需要用到值的時候,呼叫 next 方法就可以了,是不是聽起來懶惰了些?;而陣列由於必須積極地把值寫出來,因此一開始就得完整表示出值來。

重點 4. 惰性求值 Lazy Evaluation

惰性求值的概念在於 —— 需要用到值的時候,再把值求取(Evaluate)出來。

好處在於,除了可以表示無窮元素列表的概念外,如果遇到運算負荷重,但不需要馬上處理的邏輯時,可以採取這種惰性求值的策略 —— 需要處理時再去處理。

除了 ES6 Generators 產出的迭代器具有惰性求值的性質外,另一個是在本系列提到的單例模式篇章(Singleton Pattern)中,如果遇到建構單子耗費的資源稍微龐大但又不太需要即時建構的情形,就可以延遲建構單子,此為懶漢模式

相對於惰性求值的行為,以下筆者也列出積極求值(Eager Evaluation)的特性。

重點 5. 積極求值 Eager Evaluation

積極求值相對於惰性求值 —— 是即時運算的行為。

一般程式語言的任何表達式(Expressions),基本上都是積極求值的行為,如:

  • 指派表達式(Assignment Expression):let a = 1 會立刻將數值 1 指派到變數 a 身上(可以查看關於 Memory Allocation 相關的行為)
  • 運算表達式(Arithmetic Expression):3 + 5 會立刻求值為結果 8
  • 邏輯表達式(Logical Expression):10 >= 8 會立刻比對數值 10 是否大於等於 8,立馬求值的結果為 true
  • 函式/方法的呼叫(Function/Method Invocation):Math.pow(2, 3) 會立刻運算出結果 8

積極求值的優點在於事情一到就會立刻進行,但同時也是缺點的地方在於,如果資源過大,很容易會有程式卡住的情形。

小結

由於篇幅問題(已經飆到“剛好” 13,000 字,筆者吃驚!),因此打算把剩餘的東西下一篇再說明。

ES6 Generators 應該是本系列最麻煩的東西,吸收本篇很需要時間。

另外,筆者還沒講到 Promise Chain 演變到 Generator 的寫法,到底會是什麼呢~ 請讀者繼續看下去喔~~~

]]> Maxwell Alexius 2020-01-13 15:38:46
Day 48. 通用武裝・非同步概念 X 脫離巢狀地獄 - TypeScript Generics with Asynchronous Programming I. Promise Chain https://ithelp.ithome.com.tw/articles/10228010?sc=rss.iron https://ithelp.ithome.com.tw/articles/10228010?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

  1. ES6 MapSet 在 TypeScript 裡使用時需要注意的事項。
  2. ES6 Promise 的基本運作機制為何?
  3. ES6 Promise 在 TypeScript 裡使用時需要注意的事項。

如果還不清楚可以看一下前一篇文章喔~

事實上 ES2015+ 與泛型機制的主題還沒結束,最有看頭的應該是 TypeScript 結合非同步語法的編程,這應該對普遍入門 TypeScript 的讀者來說也是很重要的課題。

對任何 JavaScript 開發者,同步與非同步在 JS 的機制也是重點之一,萬一如果遇到職場面試之類的,或多或少會問到關於這方面的問題。

而 ES6 Generators 的應用層面,事實上對於導出 Async-Await 的語法是一環很重要的元素,這也會被筆者提到。

由於 ES6 Generators 與 ES7 Async-Await 的難度相對稍微高一些,因此筆者也會重新討論這些東西到底在做什麼。

因此,就算你是不熟悉這些 Feature 的讀者們也不需要擔心,因為筆者會從頭講到尾 XD。由於這是個大主題,請讀者耐心地領會。

以下正文開始

TypeScript 非同步編程 Asynchronous Programming in TypeScript

同步 V.S. 非同步的概念 Sync. V.S. Async. in JavaScript

同步與非同步的概念 —— 相信早已在 JavaScript 圈已經是熱門無法再繼續補充的觀念 —— 因為都已經被熱心的社群講完了。(筆者只能講 P 話

但筆者依舊認為,非同步的觀念在學習曲線上,依舊讓大部分學習 JS 的人感到困難,事實上筆者也是花了好一段時間才理解 Event Loop 到底在做什麼,因此這裡筆者會稍微講解一下差異到底在哪,但筆者依舊鼓勵讀者好好參照多方資源學習。

同步的概念

同步的概念很簡單,相信任何初學 JavaScript 的讀者早就遇過好一陣子了,只要是依照順序執行的方式即同步的行為(Sequentially Executed)。

let a = 3;
let b = 5;

let sum = a + b;
let power = Math.pow(a, b);

以上的範例程式碼在 JS 引擎裡一定會是同步的執行狀態,如下:

  • 第一行將數字 3 指派到變數 a,處理好 Memory Allocation 後換下一行
  • 第二行將數字 5 指派到變數 b,處理好 Memory Allocation 後換下一行
  • 第三行為空,換下一行
  • 第四行遇到 + 運算子,將變數 ab 進行 + 運算後,指派結果到變數 sum,處理好 Memory Allocation 後換下一行
  • 第五行遇到 Math.pow 方法的呼叫,將 ab 值代入到該方法後,指派輸出結果到變數 power,處理好 Memory Allocation 後換下一行
  • 沒有下一行,程序結束

非同步的概念

接下來就比較麻煩一點 —— 非同步程序在 JS 的運作機制過程,讀者可能以為:不就只是單純譬如 setTimeout 或者是 Event Listener 之類的東西嗎?這有什麼好講的?

不過講白一點,非同步的概念就是跟同步相反,不按照程式碼執行的順序執行(這是很大的廢話)。

反過來說,問題在於:那非同步的程式碼到底是怎麼樣的不按照程式碼順序執行 XD?

一種是回呼函式(Callback Function)的方式處理,比如說你呼叫了一個方法,該方法可能蘊藏一些複雜運算,然後你會想要等該方法結束掉時觸發某些程序,才會傳入 Callback Function。

事件的監聽也是非同步程式碼的一環,事實上這會牽扯到幾個重要主角:Call Stack、Event Table、Event Queue 以及 Event Loop,以下筆者慢慢對 JS 事件的處理概念進行展開。

1. Call Stack

在 JS 的世界裡,變數作用域只有分兩種 —— 全域(Global Scope)與函式作用域(Functional Scope)。

每一個作用域都有屬於自己的執行背景(Execution Context)。

Execution Context 泛指程式碼執行到的位置之背景資料狀況,比如說,在函式作用域內宣告變數 a,該 a 的資料會紀錄在該函式作用域內的執行背景;一但該函式執行結束回到全域時,該函式執行背景會移除,所以變數 a 的資料也會被清掉,全域的執行背景就沒有了變數 a 的資料 —— 換句話說,全域的執行背景就沒辦法使用變數 a

貼心小提示

讀者若想要再更了解 Execution Context,除了上網搜尋外,筆者剛剛舉的 Execution Context 紀錄變數的機制,指的是 Execution Context 裡的 Variable Object 這個東西。

當呼叫一個函式時,一個執行背景會被建立起來;相對地,當函式執行結束時,該執行背景會被處理掉 —— 這樣的操作情形跟堆疊(Stack)很像,而且是每一次呼叫函式(Call)時都會 Push 一個新的執行背景,結束就會 Pop 掉該函式的執行背景,這就是 Call Stack。

貼心小提示

Stack Overflow 名稱的由來就是:如果很北爛地刻意亂寫一個,比如說一個遞迴函式,處理不好的話會一直呼叫到自己,每一次呼叫函式就等於將新的 Execution Context Push 到 Call Stack,直到電腦的記憶體爆掉,這就是令人感到開心歡樂的 Stack Overflow

// 呼叫此函式保證讓您感到歡樂!
function callsItself() {
  callsItself();
}

2. Event Table

另外,JavaScript 有提供一些 API,譬如 setTimeout 代表計時器到的時候會觸發的事件;或者是 DOM 元素的事件監聽機制(Event Listener)。

由於程式碼不可能遇到事件監聽的情況時就卡在那邊(俗稱 Blocking),因此必須要有東西負責記錄有哪些事件被註冊,這就是 Event Table。

這裡就有同步與非同步程式碼的關鍵差異出現的地方:如果程式碼是同步的狀況,就有可能出現 Blocking 的情形,而非同步程式碼則是避免程序卡住,先記錄下來,等事件觸發時才會執行其他動作。

因此,如果碰到運算資源龐大的情形,有時候採用非同步的程序,將運算龐大的過程隔離掉也是一種解決方法。

3. Event Queue

再來,一但事件被觸發了(但同時也有可能多種其他事件也會觸發),必須要有另一個東西負責幫事件進行列隊,具有列隊性質的資料結構是 Queue,而因為是專門幫事件進行列隊的動作,這就是 Event Queue。

4. Event Loop

最後,假設開始有事件被逐步觸發,並且列隊到 Event Queue 裡面,必須要有人負責將事件的觸發進行執行對應的程序的動作Event Loop 則扮演此角色。

但必須要注意的是:Event Loop 會等到 Call Stack 被清空時(也就是主程序都完成的時候)才會開始將 Event Queue 裡面的事件一一拔出來,執行事件對應的程序

重點 1. 同步 V.S. 非同步的概念

同步(Sync.)代表程式碼會按照順序一行一行執行。

非同步(Async.)則會避免程式碼卡住(Blocking),採取其他策略來執行程序。

在 JavaScript 裡,Web 相關的 API 會經由事件的註冊,紀錄在 Event Table 上。

如果事件被觸發時,就會將該事件排序在 Event Queue 內部。

等到主程序 Call Stack 內部都執行完畢清空時,Event Loop 會開始將 Event Queue 所列隊的事件按照順序進行輸出的動作。

由於本系列要講到泛用型別的應用,因此對於非同步的過程不在多作敘述。

以下筆者就要開始深論非同步程式碼語法在 JS 的演變(當然要結合 TypeScript XDDDD)。

非同步編程的演化三部曲 Evolution of Asynchronous Programming - The Trilogy

啟始於 Callback Hell,攤平為 Promise Chain,乘載於 Generator Functions,演變成 Async Functions

Originated from Callback Hell, flattened into Promise Chain, combined with Generator Functions, evoluted as Aysnchronous Functions.

以上這一句話可以貫穿整個非同步編程在 JavaScript 領域的演變過程,一併地解釋 Async-Await 語法的演化由來,以下就由筆者娓娓道來。

混沌的開端 —— 回呼函式地獄 Callback Hell

JavaScript 裡面,使得程式碼變成巢狀地獄的混亂根源莫屬 Callback Hell(又名 Pyramid of Doom),筆者代入簡單的範例。

https://ithelp.ithome.com.tw/upload/images/20191015/201206146X91fzhLxA.png

以上的程式碼宣告一個很白癡的 sendRequest 來代表送出請求的功能。

假設想要達成送出三個請求,並且得依序送出,後面的請求必須等待前面請求完成才能開始執行。(以下的程式碼執行結果如圖一)

https://ithelp.ithome.com.tw/upload/images/20191015/20120614I50tarwcpw.png
圖一:依序送出不同的 Request 結果,不過如果讀者 Run 以上的程式碼,有可能會中斷出現 500 Server Error 的原因是因為在 sendRequest 筆者刻意保留有出現這個狀況的可能

儘管功能是達到了 —— 想當然,這種寫法實在是麻煩甚至是爛透了 —— 巢狀部分光是幾個空白鍵要多少就很麻煩外,你會發現,假設今天要 Handle 第一個 Request 的錯誤結果,你得爬到最後面才能處理到。(如圖二)

https://ithelp.ithome.com.tw/upload/images/20191015/20120614HWcfwDFfyi.png
圖二:錯誤要進行處理真是麻煩

由於需求是:必須等待前一個請求送出完畢後,才能送出後續的請求 —— 因此以這種回呼函式的寫法,後面的請求必須要寫在前面的請求的回呼函式裡,這樣造成請求之間的使用耦合度高,很難把功能拔出來

於是 —— Promise 物件就此誕生了。

首部曲 —— 攤平巢狀地獄的 Promise Chain

還記得前一篇講到的 Promise 物件的特性嗎?

筆者就再把那張連筆者覺得很精美的圖給附上來。(如圖三)

https://ithelp.ithome.com.tw/upload/images/20191015/201206149pNKCKUZYw.png
圖三:Promise 的狀態機示意圖

其中,昨天筆者有提到很重要的點:Promise 物件是可以被串接的,這個特性是一個絕佳極好的特性!

筆者就示範將剛剛的 sendRequest 改寫成 Promise 版本的物件:

https://ithelp.ithome.com.tw/upload/images/20191015/2012061415PWYOJWyy.png

恩~其實就只是把剛剛那一連串 successerror 的回呼函式改成用 Promise 提供的 resolvereject 來處理。如果要實踐相同的功能,也就是等待前一個請求送出完畢後,才能送出後續的請求 —— 首先,我們當然可以使用巢狀寫法來處理:

https://ithelp.ithome.com.tw/upload/images/20191015/20120614wZ9F6Vjz3y.png

但是,更好的解法是使用 Promise 物件如果回傳新的 Promise 物件時,可以進行串接的特性:

https://ithelp.ithome.com.tw/upload/images/20191015/20120614e8tJF3JuSf.png

你可以發現,原本的巢狀地獄就消失得無影無蹤了,圖四是執行結果。

https://ithelp.ithome.com.tw/upload/images/20191015/20120614Ok9ZBwTJkK.png
圖四:執行結果正常

但假設筆者偶然執行出現錯誤時,就算 catch 被串接到後面也可以正常運行。(如圖五)

https://ithelp.ithome.com.tw/upload/images/20191015/20120614fkZNlqm4zK.png
圖五:第一次送出請求是 200 狀態,但第二次失敗時,也可以執行到 catch

讀者試試看

其實 Promise 串接可以研究的東西多得很,讀者有興趣可以試試看各種不同的串接方式,但筆者選擇的是個人習慣的寫法,所以也沒什麼太多可以講的。

  1. 每組皆使用 then 然後 catch
    https://ithelp.ithome.com.tw/upload/images/20191015/201206144ZTz62cBwd.png

  2. 每組不使用 catch,但是都在 then 的第二個參數位置進行 catch

https://ithelp.ithome.com.tw/upload/images/20191015/20120614Jiphx2ZCRr.png

經過剛剛的講解,我們得知:

重點 2. Promise Chain

Promise Chain —— 也就是 Promise 物件的串接可以解決掉一個很麻煩的問題 —— 回呼函式的地獄 Callback Hell —— 可以使用 Promise Chain 進行攤平(Flatten)的動作。

非同步編程的演變過程,第一個環節就這樣結束了~

小結

由此可知,脫離巢狀地獄的第一步就是 —— 善用 Promise Chain 技巧,讓巢狀結構不覆存在。

不過呢,非同步的編寫手法還沒結束,下一篇筆者要介紹 ES6 Generators。

這段過程坦白說會非常麻煩,但理解過後會覺得有一種 —— 哦~~~ 的感覺。

]]> Maxwell Alexius 2020-01-09 13:12:51
Day 47. 通用武裝・泛型應用 X 結合 ES2015+ - TypeScript Generics with ES2015+ Features https://ithelp.ithome.com.tw/articles/10227593?sc=rss.iron https://ithelp.ithome.com.tw/articles/10227593?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

  1. 迭代器(Iterator)與聚合物(Collection)的差別在哪?
  2. 迭代器模式要如何實踐?實踐的目的為何?
  3. 什麼是多型巡訪(Polymorphic Iteration)?

如果還不清楚可以看一下前一篇文章喔~

今天的主題對於任何本系列的讀者應該是很重要的篇章,相信讀者也是早就碰過 ES2015+ 的語法,但是想要再進修才會學習原生 JavaScript 結合 TypeScript 的型別系統下去。

ES2015+ 與泛用的結合

請記得一定要在 tsconfig.json 裡的 lib 設定裡新增 es2015

這已經在專案編譯篇章裡面已經講過了。

反正就是在 tsconfig.json 裡,將 es2015 選項新增到 lib 設定裡喔。

{
  "compileOptions": {
    "lib": ["dom", "es2015"]
  }
}

ES6 鍵-值對物件與集合物件 —— Map & Set

筆者先從比較單純的東西講,MapSet 在 TypeScript 的泛型用法。

貼心小提示

這裡筆者就預設讀者已經知道 ES6 MapSet 的用法囉~但是如果沒有用過的話可以看這裡:

  1. ES6 Map MDN Documentation
  2. ES6 Set MDN Documentation

另外,沒有用過的讀者可能會疑惑,為何要講這個東西 —— 理由以下筆者就會解釋。

ES6 Map 是用來儲存鍵值對的物件,跟普通的 JSON 物件感覺很像,但是差別在於:

重點 1. ES6 Map V.S. JSON Object

ES6 Map 可以使用任何型別(不局限於字串)的值作為鍵(key);但普通的 JSON 物件 —— 儘管看似可以使用字串和數字作為鍵,但任何型別的值作為 JSON 物件的鍵(key)都會被轉換成字串型態。

然後,因為 Map 可以使用任何型別的值作為鍵,因此這裡就是泛用之型別參數可以代表的地方;不過,理所當然地,Map 裡的鍵所對應到的值也可以作為另一個型別參數 —— 也就是說,泛用的 Map 本身有兩個型別參數分別代表鍵與值的型別。(以下程式碼是可以正常使用的,讀者可以自行試試看)

https://ithelp.ithome.com.tw/upload/images/20191014/20120614u4NCxFj81k.png

另外,筆者想要強調,善用型別系統的推論部分 —— 比如,如果臨時忘記 Map.prototype.set 方法要填的參數與對應型別,TypeScript 會自動跟你提示。(如圖一)

https://ithelp.ithome.com.tw/upload/images/20191014/20120614gjjsIR2XWV.png
圖一:使用 Map.prototype.set 時,TypeScript 會彈出視窗提示

另一個筆者認為很好用的資料結構為 ES6 提供的 Set

重點 2. ES6 Set 與普通的列表狀結構(如:Array)的差別

Set 為數學上集合的一種體現,因此代表內部所存取的元素符合:

  • 無序性:沒有任何順序可言
  • 互異性:沒有任何元素重複,每個元素互為不同
  • 確定性:元素不外乎只有存在於或者是不存在於集合裡面,這兩種情形

但普通的陣列:

  • 可以存在重複的元素
  • 內部的元素具有順序性

而 ES6 Set 使用起來比起陣列可以更輕易地進行不同 Set 物件的聯集(union)、差集(difference)或交集(intersection)。

理所當然地,我們可以提供型別參數的值代表 Set<T> 內部存的元素型別。

https://ithelp.ithome.com.tw/upload/images/20191014/20120614uP6Cz4S6rT.png

重點 3. 泛用 MapSet 物件

若建構一個 ES6 Map 型別的物件,建議提供兩個型別參數的值分別代表 Map<Tkey, Tvalue> 的鍵與值的對應型別。

若建構一個 ES6 Set 型別的物件,建議提供一個型別參數的值代表 Set<T> 內部元素所存取的型別。

讀者試試看

如果是沒有提供型別參數的情況下,以下的 unspecifiedTypeMap1unspecifiedTypeSet1 分別的推論結果為何?

https://ithelp.ithome.com.tw/upload/images/20191014/201206145fGsiewDkc.png

但是如果是以下的情形,unspecifiedTypeMap2unspecifiedTypeSet2 分別的推論型別為何?

https://ithelp.ithome.com.tw/upload/images/20191014/20120614oiBzt3ZbKr.png

裡面有無初始值會影響推論結果嗎?

ES6 Promise 物件

事實上,之前在模擬戰 —— UBike 篇章有稍微使用過 Promise 物件了,不過暫且還是簡介一下:

重點 4. ES6 Promise 物件的用意與目的

Promise 物件可以針對非同步的事件(Asynchronous Events)進行狀態機(State Machine)式的編程操作,狀態有:

  • pending當 Promise 物件被 JS Engine 讀到時,就會馬上啟用的狀態pending 意指等待內部的程式碼的結果
  • resolved:Promise 內部的非同步過程執行成功時的結果
  • rejected:Promise 內部的非同步過程執行失敗時的結果

對於 Promise 有任何問題,社群上有很多熱心的人們已經把 Promise 講到不能筆者覺得不能再講下去了 XD,而且 MDN Document 也寫得清清楚楚。

貼心小提示

筆者還是再三強調一次:當 Promise 物件被 JS Engine 讀到時,就會馬上啟用的狀態;這也代表不管你後續有沒有串接 thencatch 方法,Promise 在被建構的那一刻就已經開始在執行內部的程式碼

通常簡單的 Promise 物件程式碼可能會長這樣,

https://ithelp.ithome.com.tw/upload/images/20191014/20120614cm0MfBSATL.png

sendRequest 本身是非同步的 Action(當然,簡單一點的如:setTimeout 等也是非同步的行為),而如果執行過程有結果就會根據不同的 response.status 結果判斷該 Promiseresolved 還是 rejected 狀態。

而後 request 存的 Promise 物件分別對 resolved/rejected 狀態有不同的後續處理方式。

事實上,更好一點的理解形式,筆者就直接畫出圖來。(如圖二)

https://ithelp.ithome.com.tw/upload/images/20191014/201206146YgsclLifq.png
圖二:Promise 可看作是針對非同步事件進行狀態機的表示形式

不過 Promise 的功用還有很多,像是如果是 resolved 的狀態時,在 [Promise Object].then 裡的回呼函式如果回傳的是另一個 Promise 物件,我們可以不停地一直串聯下去。

https://ithelp.ithome.com.tw/upload/images/20191014/20120614rEv936UT3u.png

這樣的行為用更完整的圖會是這樣的呈現。(如圖三)

https://ithelp.ithome.com.tw/upload/images/20191014/201206142bTePWplM3.png
圖三:更完整的 Promise 運作圖,就連你在 catch 錯誤時,依然可以選擇回傳新的 Promise 物件持續這個狀態機的迴圈下去喔!

回過頭來,Promise 在 TypeScript 裡依然跟泛型的使用有關 —— 也就是 Promise<Tresolved> —— 你可以提供一個型別值代表當 Promise 進入 resolved 的狀態時的結果的值之型別。

以下筆者就寫個簡單範例:

https://ithelp.ithome.com.tw/upload/images/20191014/20120614IzH5NyQOFu.png

當你特別註記 Promise<string> 就代表:resolve() 內部的值必須填入 string 型別,如果不是的話就會顯示錯誤訊息如圖四。

https://ithelp.ithome.com.tw/upload/images/20191014/20120614nPyINSyRZ8.png
圖四:顯示數字 200 不為 string | PromiseLike<string> | undefined 型別

其實光是錯誤訊息就透露 —— 連 undefined 也就是空值可以視為 resolve 可填入的東西,至於 PromiseLike<string> 可以想成可以填入類似 Promise.resolve('Succeeded') 這種東西。

不過筆者依然想不透什麼情形會寫成 resolve(Promise.resolve('Succeeded'))

另外,如果你註記為 Promise<string> 時,使用該 Promise 物件的 Promise.prototype.then 方法則是會提示參數的型別。(如圖五)

https://ithelp.ithome.com.tw/upload/images/20191014/20120614eEfXdrpg4S.png
圖五:then 裡面的提示

乍看之下裡面內容挺恐怖的,但是整理過後仔細瞧瞧:

https://ithelp.ithome.com.tw/upload/images/20191014/20120614ZR79S4ri9a.png

好,還是很亂 XDD,但慢慢來解析。

onfulfilled?選用屬性,可以填入一個函式型別 —— 該函式的參數為 value: string,跟原本的 Promise<string> 裡提供的 string 型別參數的值連結。而輸出部分則除了 string 與 Nullable Types 以外,還可以填入 PromiseLike<string>,也就是指出剛剛筆者在圖三時有提到的:Promise 物件可以串下去的行為。

另外的 onrejected? 則是當 Promise 物件裡的非同步程式碼執行有出錯或者是被使用者主動呼叫 reject 的話觸發的行為。裡面也可以填入函式型別,其參數型別為 reason: any,這一點之所以沒有跟 Promise 物件的型別參數進行連結的原因,可想而知,錯誤的出現形式可不會只是字串而已,光是物件的組合也挺多種,因此才是少數會用到 any 型別的情形。

至於 PromiseLike<never> 就是指這個 Promise 本身無法完整地執行完畢,直接拋出錯誤的概念。(參見 never 型別篇章

同理,Promise.prototype.catch 方法裡的敘述跟 onrejected?Promise.prototype.then 差不多。(如圖六)

https://ithelp.ithome.com.tw/upload/images/20191014/20120614FtPVvpZVLe.png
圖六:Promise.prototype.catch 的提示內容

重點 5. 泛用 ES6 Promise 物件

在 TypeScript 的世界裡,Promise<Tresolved>Promise 物件的完整泛用表示式。而型別參數 Tresolved 代表 Promise 物件時,呼叫 resolve 方法可以填入的型別外,還代表未來在使用 Promise.prototype.then 方法時,回呼函式的參數代表之型別。

此外,resolve 時除了可為 Tresolved 型別外,也可以是空值(undefined)以及類似於 PromiseLike<string> 的型別之值。

貼心小提示

讀者應該還是會感到疑惑,為何有所謂的 xxLike 型別的東西 —— 比如 ArrayLike<T>PromiseLike<T> 等。

提示:跟 TypeScript lib 設定有關。

由於這個是進階性的話題,筆者還是給讀者一帖 StackOverflow 提問做補充。

讀者試試看

這幾題比起剛剛的 MapSet 還要來得重要,請讀者務必要親手驗證過以下的行為

  1. 請問如果我們不提供型別參數的值給 Promise 物件,以下的 unspecifiedTypePromise 的推論型別為何?

https://ithelp.ithome.com.tw/upload/images/20191014/20120614o01ZG5Vnnf.png

  1. 但如果假設,Promise 物件裡的 resolve 函式被呼叫時有填入特定型別之值,則 unspecifiedTypePromise 此時的推論結果為何?

https://ithelp.ithome.com.tw/upload/images/20191014/20120614N33vVpfz9R.png

  1. 請讀者根據題目 1 與 2 的結果推論:Promise<T> 中,T 必須主動註記的必要性 —— 我們是否應當積極註記 Promise<T> 而非 Promise 而已?

  2. 如果是直接用 Promise.resolvePromise.reject,請問個別的推論結果為何?

https://ithelp.ithome.com.tw/upload/images/20191014/201206144PxwZAm0Nb.png

若讀者對於 unknown 型別有問題的話,請參見 any v.s. unknown 型別篇章

筆者以下再測試幾個不同常見的 Promise 物件的功能。

Promise.all —— 當所有的 Promise 進入 resolved 狀態時執行

Promise.all 的概念有點像是很多不同的 Promise同一個時刻開始運行,直到所有在 Promise.all 內部的 Promise 都成功 resolve —— Promise.all([ ... ]).then 才會被執行。

以下的範例程式碼,Promise.all(...) 的推論結果為 Promise<[string, number, boolean]> —— 該型別參數代表的是元組型別。(如圖七)

https://ithelp.ithome.com.tw/upload/images/20191014/20120614RFu7MW7vCv.png

https://ithelp.ithome.com.tw/upload/images/20191014/20120614lxgyqSc021.png
圖七:以上的程式碼,Promise.all 此時的推論結果

如果刻意將其中一個故意 reject 掉,儘管看似應該要回傳類似 Promise<never> 這種會出現錯誤的狀態,但它仍然會按照元組格式去顯示結果。(如圖八,不過這種行為依筆者來看應該是錯的)

https://ithelp.ithome.com.tw/upload/images/20191014/20120614Cz3nX5Xztj.png
圖八:儘管很明顯筆者刻意要用 Promise.rejectPromise.all 壞掉,但事實上它還是會顯示元組型別格式的推論結果

Promise.race —— 所有的 Promise 進行比賽,誰先 resolve 誰就獲勝

這邊很明顯應該不會是用元組型別來顯示結果,而是會用 union 複合型別方式呈現推論結果,因為 Promise.race 裡的每個 Promise 都有被 resolve 的可能。(以下範例程式碼推論結果如圖九)

https://ithelp.ithome.com.tw/upload/images/20191014/20120614IiNWRwaaC1.png

https://ithelp.ithome.com.tw/upload/images/20191014/20120614BxVQkiwAVb.png
圖九:推論結果為 Promise<string | number | boolean>

以上的程式碼,筆者只是簡簡單單地建構一個 delay<T> 函式,目的是將 Promise 延緩幾個時間 resolve

Promise.race 通常好用的地方在於實現 Request Timeout 功能:

https://ithelp.ithome.com.tw/upload/images/20191014/20120614gMCLFfKLp4.png

比如說,你有一個 arbitraryRequest 是為一個 Promise(或 PromiseLike)物件,但是你希望這個請求能夠在三秒內處理完畢,如果沒有就 reject 掉,你可以使用 Promise.race 並且將該請求跟一個計時器比賽 —— 如果計時器獲勝則代表該 Promise 可以執行 reject 過後的狀態。

小結

筆者在本篇大概講最多的應該是 Promise<T> 這個物件的型別以及推論機制與結果,讀者應該也從這一篇發現泛用型別的重要性了吧~

下一篇筆者要緊接著正進階的部分 —— 泛用型別與 ES2015+ 非同步語法的結合應用喔~

]]> Maxwell Alexius 2020-01-09 13:10:48
Day 46. 通用武裝・迭代器模式 X 泛用迭代器 - Iterator Pattern Using TypeScript https://ithelp.ithome.com.tw/articles/10227255?sc=rss.iron https://ithelp.ithome.com.tw/articles/10227255?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

泛用類別與泛用介面結合時的注意事項為何?

如果還不清楚可以看一下前一篇文章喔~

其實筆者在泛用方面的型別推論與機制並沒有討論很多(不過還是有三篇都在講一些使用泛型的細節 XD),那是因為就只是在型別系統上多了型別參數化的機制,筆者要嘛可以搬出前 20 天的文章然後冠上泛用的機制闡述各種不同的推論結果,多寫個 20 天份跟泛用型別推論機制相關的文章,不過這樣的學習效率實在是很差,所以務必要好好釐清《前線維護》《機動藍圖》篇章,方能夠駕馭 TypeScript 的進階功能,走入王道啊!

讀者云:“作者你想太多了”

那我們進入正文開始吧~

迭代器模式 Iterator Pattern

迭代器是什麼? What is Iterator?

雖然這問題可能對普遍的讀者來說應該很理所當然,不過筆者還是正規化一下迭代器的定義:

重點 1. 迭代器的定義 Definition of Iterator

專門用來巡訪(Iterate)亦或者遍歷(Traverse)目標聚合物(Collection)的內容。

小心喔 —— 迭代器這個詞也是很容易被亂用

迭代器是專門巡訪聚合物的一個物件,而聚合物就是我們所認知的 —— 陣列啊、列狀物啊、甚至是集合(Set)也是一種聚合物。

所以:

陣列或列狀物件不等於迭代器,再次強調 —— 它們是聚合物;迭代器是專門遍歷聚合物的物件。

另外,樹狀資料結構(Tree)也是一種聚合物,只是組織成樹狀結構,但是巡訪模式就有分前序(Preorder)、中序(Inorder)以及後序(Postorder),不過還有一個是層級巡訪(Level-order)的模式,但個人覺得不太算常使用的巡訪模式,所以覺得還好。(可參見 Tree Data Structure

貼心小提示

事實上,樹狀資料結構的尋訪演算法,筆者還是大概講一下細節與各自的優勢,讀者有空可以自己想想看。

深度優先尋訪(Depth-First Search)專門以尋訪子樹(Child Tree)為優先,比較適合解『找不找得到路徑』(Finding the existence of the path)相關問題,譬如迷宮的尋訪。

  • 其中深度優先尋訪演算法就有再分前序、中序與後序
  • 樹狀結構如果越深,演算法的耗費時間可能會越久。

層級優先尋訪(Breadth-First Search)專門以層級(Level)為優先,比較適合解『最短路徑』(Finding the shortest path)相關問題,譬如 LinkedIn 上,人脈的維度(Degree)—— 你的朋友和你的關係是第一度人際關係(1st Degree)、你朋友的朋友與你是第二度人際關係(2nd Degree)等等,依此類推。

  • 其中層級優先尋訪就只有層級尋訪演算法一種而已。
  • 樹狀結構如果越廣,演算法耗費時間可能越久。

重點 2. 迭代器 Iterator V.S. 聚合物 Collection

聚合物(Collection)泛指常見的聚合型資料結構,不限列狀、環狀、樹狀或者是圖(Graph)。

迭代器則是負責巡訪聚合物的物件,而非聚合物本身。

有些讀者可能想說:“恩... 可是我們不是有 forwhile 迴圈協助我們迭代一些列表行資料嗎?我們真的有需要用到迭代器這種東西嗎?”

筆者答:“因為你不可能只會遇到普通串列的格式(如陣列型別),但你可能也會遇到樹狀資料結構 —— 如果把樹狀的巡訪情形邏輯,再分成上述的前序、中序與後序,你可能也會使用 if...else... 等等語法去寫出很龐大一片不同的 for 迴圈巡訪演算法,除了程式碼可再利用性差東西塞在一起要維護起來也挺麻煩的。”

以上所謂的程式碼可再利用性差 —— 讀者請仔細想想:

https://ithelp.ithome.com.tw/upload/images/20191012/20120614oDlWqTHnrP.png

如果別的地方也會需要用類似的遍歷演算法,一種就是 Copy-Paste 方式搬移,另一種可以採用過往筆者介紹的策略模式來解決,抽換掉遍歷演算法。

不過本篇要介紹的是針對這一類,需要遍歷不同的資料結構型態,更適合的一種設計模式 —— 迭代器模式

迭代器模式 Iterator Pattern

資深的讀者應該隱約猜出筆者想要講的重點,筆者就速速把迭代器模式要達到的效果交代一下:

重點 3. 迭代器模式 Iterator Pattern

迭代器模式的主要目的在於不需要知曉任意聚合物的細節,就可以依序遍歷內含的每一個元素

也就是說,我們可以宣告統一的產出迭代器的介面(Iterator Generator Interface),而任何互相毫無關聯甚至是實作上天差地遠的聚合物,都可以藉由實踐該介面,產出同樣功能的迭代器,達到泛用的效果。

讀者看不懂,沒關係,因為設計模式這種東西光看定義能夠領會的人(除非你有經歷過)應該也不到兩成,原著也只有說明第一句話:

不需要知曉任意聚合物的細節,就可以依序遍歷內含的每一個元素 - 出自《Design Patterns — Elements of Reusable Object-Oriented Software》

筆者必須澄清:重點 3 的第二段不是出自原著,而是筆者為了因應 TypeScript 的使用情形,才寫出來的額外補充片段。

迭代器模式下的角色

根據本篇每一個重點的描述,角色看起來很明確只有兩個:迭代器(Iterator)以及聚合物(Collection)。

不過,事實上這裡有一個很細微、不知道讀者有沒有注意到的點:

(...重點 3 第二段前面略)而任何互相毫無關聯甚至是實作上天差地遠的聚合物,都可以藉由實踐該介面,產出同樣功能的迭代器,達到泛用的效果。

“產出”這兩字的關鍵 —— 超級重要,暗示隱藏的背後角色還有一個產出迭代器的介面(Iterator Generator Interface)。

也就是說,我們除了迭代器的介面到底長什麼樣子外,聚合物也必須實作另一個功能 —— 專門產出對應的迭代器 —— 這不很像之前講過的 Factory Method 模式

所以筆者宣告兩個介面 —— Iterator 以及 Iterable 這兩種分別代表 —— 迭代器的介面,以及可被迭代的聚合物必須實踐的介面

https://ithelp.ithome.com.tw/upload/images/20191012/201206144gQFMnSOcU.png

所以筆者簡單實作陽春版迭代器~!

https://ithelp.ithome.com.tw/upload/images/20191012/20120614nZN5JKCVS6.png

讀者可能看到上面的程式碼心想:“搞什麼嘛!普通的陣列不是單純用 for 迴圈迭代就好了嗎? —— 難道作者耍我嗎?”

不!不!不!並不是這樣的,筆者當然要先舉簡單的例子給讀者看,讓讀者先熟悉實踐迭代器模式的基本過程而已,讀者忍一下~

史上最基本的迭代器的關聯圖呈現如圖一。

https://ithelp.ithome.com.tw/upload/images/20191012/20120614t9qkO1ycO9.png
圖一:迭代器模式分成兩個部分 —— 一是迭代器本身的介面,另一個是對任意的聚合物實踐 Iterable 介面,也就是工廠方法負責產出該聚合物的迭代器

要使用聚合物的迭代器非常簡單。(以下程式碼輸出結果如圖二)

https://ithelp.ithome.com.tw/upload/images/20191012/20120614WjdD4gXqgj.png

https://ithelp.ithome.com.tw/upload/images/20191012/20120614XIsUBLyzW7.png
圖二:順利地迭代普通陣列的值

迭代器模式的優勢

接下來筆者要解釋為何迭代器模式很好用。前一篇筆者有展示過一個鏈結串列的實踐過程 —— GenericLinkedList<T> 類別。

https://ithelp.ithome.com.tw/upload/images/20191012/20120614PJLVRoKDPh.png

普通情況下,要迭代這個 GenericLinkedList,我們可能必須手動這樣寫:

https://ithelp.ithome.com.tw/upload/images/20191012/20120614cPDSGUeeeH.png

不過我們希望這個 GenericLinkedList 也可以產出和 Iterator 同樣介面的迭代器,這樣一來,除了基本的陣列 MyArray 外,如果是 GenericLinkedList 也可以用相同的迭代器介面去遍歷裡面的內容。

一種寫法是直接讓 GenericLinkedList 再去實踐 Iterable 這個介面,另一種則是在宣告一個子類別繼承 GenericLinkedList 並實踐 Iterable 介面:

https://ithelp.ithome.com.tw/upload/images/20191012/20120614IMgGNmbKlo.png

貼心小提示

筆者在很遙遠的陣列型別篇章有講到:空陣列由於內部沒有任何型別可以參考,因此 TypeScript 無法判斷 [] 型別為 Array<T> 中的 T 為何,因此必須積極註記。

筆者來寫一個函式 foreach<T>,專門接受 Iterator<T> 型別還有一個回呼函式,進行迭代的動作:

https://ithelp.ithome.com.tw/upload/images/20191012/20120614nh9dil3mV0.png

記得,foreach 之所以要有型別參數變成泛用函式的理由是因為要能夠接收各種不同型別的 Iterator 物件。

以下的程式碼就來測測看 foreach<T> 這個函式。(測試結果如圖三)

https://ithelp.ithome.com.tw/upload/images/20191012/20120614lGWBI7LAZE.png

https://ithelp.ithome.com.tw/upload/images/20191012/20120614xGygHKUnnK.png
圖三:這個行為在設計模式原著又有一個名稱 —— 多型巡訪(Polymorphic Iteration)

以上的 foreach<T>,儘管來源是不同的聚合物,但是我們卻可以用同一個迭代方式去遍歷每一個元素,這個被稱為多型巡訪。(多 Fancy 的名稱)

另外,你也可以發現我們不用在擔心說還要特定幫 LinkedList 相關的類別還要想著怎麼寫 while 迴圈去做迭代 —— 這個邏輯本身被 NormalIterator<T> 給做掉了

重點 4. 迭代器模式的優勢 Advantage of Using Iterator Pattern

不需要管聚合物的結構如何,我們都可以藉由迭代器模式的操作下,統一所有聚合物的迭代方式 —— 又名多型巡訪(Polymorphic Iteration)。

你也不需要再去為不同的聚合物宣告迭代的方式,因為這些邏輯都被迭代器模式給做掉了

其實迭代器模式,講直白點主要就是把聚合物內部的結構隱藏在該聚合物類別裡 —— 綁定 Iterable<T> 介面負責去宣告建構統一的迭代器介面而已。

筆者繼續延伸:假設我們也有二元樹 BinaryTree 這種資料結構。

https://ithelp.ithome.com.tw/upload/images/20191012/20120614BQNQZapdig.png

筆者知道以上的 BinaryTree 是過度簡化的版本,不過就將就一下給大家看建造一顆二元樹的過程。另外,忘記存取方法(Access Methods)請參考這一篇

https://ithelp.ithome.com.tw/upload/images/20191013/20120614kO4R2zVoji.png

以上的程式碼建造的樹之結構如圖四。

https://ithelp.ithome.com.tw/upload/images/20191012/201206141x1ylyiygm.png
圖四:這是按照上面的程式碼建構出來的 BinaryTree 的長相

平常如果你想要遍歷樹狀資料結構,我們又還分前序、中序與後序 —— 筆者本篇以*前序(Preorder)走訪模式*為例,根據圖四,前序走訪的順序剛好是從 TreeNode 之數字 1 走到數字 10。

所以我們可以這麼做 —— 將 BinaryTree<T> 實踐 Iterable<T> 介面,然後創建出前序走訪過後的迭代器

https://ithelp.ithome.com.tw/upload/images/20191013/20120614XrcSzauNAi.png

以下是比較普通使用 preorderTraversal 的手法,比對建立迭代器然後套入剛剛筆者定義的 foreach<T> 函式:

https://ithelp.ithome.com.tw/upload/images/20191013/20120614uw9X9nVquz.png

測試結果都是印出 1 ~ 10 這些數字。(如圖五)

https://ithelp.ithome.com.tw/upload/images/20191013/20120614c1CnrpxFpW.png
圖五:筆者一再地印證,就算資料結構再複雜,我們都可以在外面使用相同的 Iterator<T> 提供的功能,達到多型巡訪這個很酷的功能

以下的關聯圖(圖六)展示的就是將剛剛 MyArray<T>IterableLinkedList<T> 以及 BinaryTree<T> 這三種看似不可能用同個介面巡訪的方式 —— 藉由迭代器模式統一下來。

https://ithelp.ithome.com.tw/upload/images/20191013/20120614DC7mDgWsJt.png
圖六:就算你用再複雜的聚合物,我們都可以達到統一巡訪的模式

小結

這一篇不用說,花最多時間就是在畫圖上...

不過筆者大致上把該 Cover 的東西都大概講述完畢了,接下來還要其他泛用型別的應用~

]]> Maxwell Alexius 2019-10-31 15:52:06
Day 45. 通用武裝・泛用類別與介面 X 終極組合第二彈 - Ultimate Combo of Generic Class & Interface https://ithelp.ithome.com.tw/articles/10226989?sc=rss.iron https://ithelp.ithome.com.tw/articles/10226989?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

  1. 泛用型別化名的如何進行宣告?
  2. 泛用化名註記在變數時的注意事項為何?
  3. 泛用函式的特點為何?

如果還不清楚可以看一下前一篇文章喔~

這段寫作過程真是很神奇,不過筆者還是就貼一下在《機動藍圖》篇章探討過的介面與類別的結合篇章 —— 該篇重點在於探討類別 implements 介面種種的型別推論與註記的機制。

本篇則是多增加了泛型(Generics)的機制,但是就跟前一篇的調性差不多,也就是說 —— 筆者不會再度探討泛型的類別與介面結合時的推論與註記機制,因為這跟《機動藍圖》篇章討論的沒差多少。

而本篇重點在於強調泛型的宣告下類別與介面綁定時的規則與特點

以下正文開始

泛用類別與介面的終極組合 Ultimate Combo of Generic Class & Interface

泛用類別與介面的綁定

這一節的重點跟前一篇 —— 子類別繼承父類別的概念很像。首先,筆者先把前一節的 LinkedList<T> 泛用介面給讀者過目一下,因為這是本篇的主角介面。

https://ithelp.ithome.com.tw/upload/images/20191011/201206140HyK4S4T1i.png

另外,前一篇討論過普通類別或者是泛用類別繼承泛用的父類別的狀況;這一次也是討論相似的狀況,普通類別或泛用類別綁定泛用介面的狀況,如下。

https://ithelp.ithome.com.tw/upload/images/20191011/20120614Tloragv592.png

首先,這兩段程式碼一定都會被 TypeScript 叫來叫去,因為一但跟介面綁定就等同於簽下契約 —— 必須實踐介面所描述的功能。(參見介面與類別的終極組合篇章

筆者就把兩種不同的錯誤訊息丟出來給大家看看~(如圖一、二)

https://ithelp.ithome.com.tw/upload/images/20191011/2012061471TKB3UmcU.png
圖一:除了 MyLinkedList 沒有符合介面的要求外,重點在於它所要 implements 的介面被推論為 LinkedList<any>,這是因為型別參數 T 在全域裡並沒有代表任何型別結構,因此被視為 any

https://ithelp.ithome.com.tw/upload/images/20191011/20120614eit1TWEM5Z.png
圖二:由於 MyGenericLinkedList<T> 有特別宣告 T 這個型別參數,因此試著 implements 這個 LinkedList 泛用介面時,可以填入該參數 T 進行型別連動的動作

思路跟昨天的文章差不多,因此筆者就不解釋直接丟昨天那一套重點改成今天這一套重點,相信讀者也想趕快目睹類別跟介面結合擦出火花的優勢而不是看筆者囉哩八唆。

本篇唯一重點.類別綁定泛用介面的情形 Class Implementing Generic Interface(s)

分成兩種形式,若類別為普通類別時,實踐到的泛用介面必須確切指名該介面之型別參數的確切型別值

然而若類別也是泛用類別時,則實踐到的泛用介面除了可以指定特定的型別外,也可以填入泛用類別所宣告的型別參數建立型別上的連結

貼心小提示

沒辦法,正式的東西不囉唆ㄧ點又顯得有些隨便,讀者在開發時不需要像作者ㄧ樣斤斤計較;除非像是遇到傳遞知識的場合,肯定要確保知識的精確性到某個程度,錯了就得改 —— 所以筆者還是得啟動廢話一點的模式。

但平常開發時,筆者有時會不經意寫出爛程式碼也會被別人 Fk 來 Fk 去,這是正常的。

每個人都 Fk 來 Fk 去是正常的 —— 古時候的蘇格拉底 Fk 別人,別人也 Fk 回去的過程據說也是挺猛烈的(參見本系列踢翻 Object Composition 迷思篇章,筆者也是寫得挺挫的,深怕被 Fk 來 Fk 去的,不過秉持本系列要成為 ACE 的理念還是得跨出這一步)

另外也可參見 WTF per Minute - An Actual Measurement for Code Quaility

類別與介面進行綁定的簡易實作 —— 鏈結串列

根據前一篇已經宣告過後的 LinkedList 這個泛用介面,我們來試試看用類別去實作該介面吧。

以下是完整的程式碼實踐:

https://ithelp.ithome.com.tw/upload/images/20191011/20120614CkrJEU5VqY.png

以上的程式碼筆者會開始深入分析。

GenericLinkedListNode 泛用類別的型別參數 TLinkedListNode 進行綁定的動作,建構子非常簡單,就是填入該節點必需要儲存的值 value。而 next 成員則是代表它所鏈結的下一個節點 LinkedListNode

GenericLinkedList 則複雜許多,但結構事實上很簡單,不過這邊要講很多細節:

1. head 成員變數

head 成員變數存的是 LinkedListNode,裡面的型別參數與 GenericLinkedList 的型別參數 T 進行綁定,也就是說 —— GenericLinkedList<number> 存的 LinkedListNode 必須也要為 LinkedListNode<number> 型別

2. length 成員方法

length 成員方法則是計算整個鏈結串列的長度(有些鏈結串列在計算長度的命名是用 size)。

然而,以下這一段:

https://ithelp.ithome.com.tw/upload/images/20191011/201206146PQlhdRonh.png

你會看到筆者 —— 儘管在 this.head 經過一次的型別確認(Type Guard)下 —— 確認不為 null,但仍然對 currentNode 作的註記是 LinkedListNode<T> | null,也就是與 null 的理由是因為後面使用 while 迴圈會不停更新 currentNode 的值,還是有潛在 null 發生的可能性。

另外,讀者可能認為 while 迴圈內部的 currentNode = currentNode.next 這一行可能會出現問題,因為在呼叫 next 之前,currentNode 被筆者強行註記為 LinkedListNode<T> | null,因此有成為 null 的可能性,而 null 不可能有 next 屬性。

然而,不需要積極註記為 currentNode = (currentNode as LinkedListNode<T>).next 的原因是因為 while 迴圈的判斷過程就已經確認 currentNode !== null,因此可以確定 while 迴圈內部的 currentNode 100% 絕對會是 LinkedListNode<T> 的型別 —— 這就是之前在講 Type Guard 部分:根據判斷敘述架構下進行型別限縮的案例之一。(如圖三)

https://ithelp.ithome.com.tw/upload/images/20191011/201206149YYwLQqWFD.png
圖三:currentNode 變成 LinkedListNode<T> 而非 LinkedListNode<T> | null,因為 null 狀況被 while 的判斷部分給濾掉了

另外,因為這一行 currentNode = currentNode.next,被指派的部分就因為出現指派 currentNode.next 而推論結果退化為 LinkedListNode<T> | null。(如圖四)

https://ithelp.ithome.com.tw/upload/images/20191011/20120614erKjYGqLZx.png
圖四:因為又再度被指派為型別 LinkedListNode<T> | null 的值,型別推論退化為 LinkedListNode<T> | null

3. at 成員方法

這裡就可以比對剛剛的 length 方法的實踐。

https://ithelp.ithome.com.tw/upload/images/20191011/20120614JhbtMHD3z9.png

首先,實踐鏈結串列的邏輯部分應該沒問題,at 方法主要是要找尋某節點在鏈結串列內的位置(index),而 index 如果被超出去的話就會丟出類似 Out of bound 之類的錯誤。

另外,筆者這一次在 while 迴圈積極註記了 ... as LinkedListNode<T> 型別:

https://ithelp.ithome.com.tw/upload/images/20191011/20120614nOQJNxJbxB.png

讀者可能會問:

“這樣不就跟筆者之前在第三篇章某部分有談到的型別壟斷(Type Monopolization)的情型很像嗎?你偏偏選擇將一個可能是 LinkedListNode<T> | null 的型別強行註記為 LinkedListNode<T>,這不就是打自己臉嗎?”

筆者之前提到類似這樣的概念:“假設某變數可能型別為 A | B,但有 100% 信心說,目前所代表的型別為某 A 型別,你才可以選擇註記”。

此時的狀況是:

筆者確認 index 這個值在鏈結串列的 length 範圍內,所以 currentNode 不管如何 100% 絕對會是 LinkedListNode<T> 這個型別

畢竟程式是人寫的,我們也會有自個兒一套的判斷標準 —— 不太可能會 100% 交給 TypeScript 去幫我們推論型別的樣貌,這也是為何我們有時候必須要積極註記來輔助 TypeScript 編譯器幫助維護程式碼的品質。(筆者知道這很廢話

4. insert 成員方法

https://ithelp.ithome.com.tw/upload/images/20191011/20120614MeV3ksr692.png

insert 方法運作的邏輯過程是這樣:

  1. 假設鏈結串鏈本身是空的,你也只能指定 index0 的狀況下插入新的值
  2. 假設鏈結串列不為空,而且 indexlength() 的範圍內,在該 index 上的節點拔出來後變成舊的節點,取代為新的節點,此時新的節點的 next 必須鏈結舊的節點,而 index - 1 位置的節點的 next 也必須取代為新的節點(示意圖如圖五)

https://ithelp.ithome.com.tw/upload/images/20191011/20120614E3Iwx6LgPv.png
圖五:鏈結串列插入一個新的節點的過程

不過這邊的判斷式過程複雜很多,因為你還要確認:

  1. 是不是插入第一個位置,是的話就得取代 this.head 的值,然後新節點就必須連結舊的 head 節點
  2. 是不是插入最後一個位置,是的話你就不需要鏈結後面的節點,因為本來插入到最後一個位置,而最後一個位置再更後面就是空的

另外,你會看到筆者依然有 ... as LinkedListNode<T> 這個顯性註記代表筆者 100% 確定該變數或值一定是 LinkedListNode<T> 這個型別,跟實踐 at 成員方法的概念也差不多。

讀者試試看

可以到 Maxwell-Alexius/Iron-Man-Competitionlinked-list 部分程式碼下載下來,並且試試看實踐拔除節點 remove 的功能。

本篇主旨在希望 —— 讀者要能夠自行判斷:

  1. 什麼時機不需要積極註記?什麼時機則會需要?能夠分辨哪些行為並不是型別壟斷的動作呢?
  2. 另外,如果是超出鏈結串列以外的值需要丟出 Out of bound 錯誤。
  3. 其實筆者故意漏掉了檢查 index 小於 '0' 的情形,亦或者是 index 值為 Floating Number,讀者可以想想看要怎麼去 Handle 這一類的狀況。

驗證型別泛用類別與介面的結合之推論與註記的機制

接下來就是簡單測試剛剛的程式碼實踐結果,首先筆者刻意多宣告 getInfo 這個成員方法,目的旨在檢視鏈結串列的內部內容。

貼心小提示

事實上筆者先寫出實踐出來的結果再去測試,跟提前先寫測試再去實踐功能比起來,筆者比較 Prefer 後者。由於本篇在談論的東西跟測試無關,所以不會深究太多這一類的東西。(當然筆者可以先講測試,不過考慮過後還是以語法教學為主不然範圍莫名其妙又被擴大,但又感覺這是一個好想寫寫看的坑啊

請參見 TDD(Test Driven Development)相關資源。

以下就是簡單的測試程式碼。(以下程式碼測試結果如圖六)

https://ithelp.ithome.com.tw/upload/images/20191011/20120614zL5CbefiAp.png

https://ithelp.ithome.com.tw/upload/images/20191011/20120614GM7IulTazO.png
圖六:鏈結串列的內容

當然,如果你也可以使用 at 方法來檢視裡面的內容。(以下程式碼測試結果如圖七)

https://ithelp.ithome.com.tw/upload/images/20191011/20120614NA4JpPiNvW.png

https://ithelp.ithome.com.tw/upload/images/20191011/20120614gKyEbhUohY.png
圖七:index 在 0 ~ 3 時會正常動作,但是超出 3 時就會跳出 Out of bound... 訊息

以上就是簡單的鏈結串列的實作結果過程,不過筆者強調的點在於 —— 判斷註記型別的時機,若型別部分你有 100% 信心認為是某特定型別,你應該選擇型別積極註記的動作,不需要擔心會不會造成型別壟斷的行為。

另外,仔細看會發現因為我們特地建構 GenericLinkedList<number> —— 指定型別參數為 number 型別 —— 也就是說你在使用 at 成員方法或 insert 成員方法,你都會看到原本型別參數 T 的提示都會被自動取代為 number 型別。(如圖八、九)

https://ithelp.ithome.com.tw/upload/images/20191011/201206147AaVZLHSOM.png
圖八:insert 方法內部的提示內容之 value 型別被自動提示為,需要填入 number 型別之值

https://ithelp.ithome.com.tw/upload/images/20191011/20120614pjaNgeTVlS.png
圖九:at 方法則是在函式的輸出部分自動推論為 LinkedListNode<number> | null

所以就算你可能在很遙遠的地方(如:其他檔案)宣告 lLinkedListNode<number> 型別,當要載入該 l 變數的值時,你可以藉由 TypeScript 的提示 —— 填入正確的型別值,而不需要追本溯源回去看 l 的型別到底是什麼

讀者試試看

儘管前面的讀者試試看單元,寬鬆一點來說可以跳過,但筆者強烈要求,這邊的試試看一定要親手驗證,而且就留給讀者去做。

由於鏈結串列 GenericLinkedList 的泛用類別部分,它的建構子函式沒有成員變數跟 GenericLinkedList 所宣告的型別參數 T 進行連結。因此,筆者想要考考讀者:

  1. 以下的程式碼會不會被 TypeScript 警告?

https://ithelp.ithome.com.tw/upload/images/20191011/20120614IwNIQ5mCJW.png

  1. 如果可以通過的話,那麼 unspecifiedTypeParamLinkedList 的推論結果為何?
  2. 如果可以使用 unspecifiedTypeParamLinkedList,設型別 Tany 為宣告過的若干型別化名,其中,specifiedTypeParamLinkedList 的宣告方式為:

https://ithelp.ithome.com.tw/upload/images/20191011/20120614Q6FEeOl1V1.png

請問 specifiedunspecified 版本的 TypeParamLinkedList 使用上各自差別會在哪?需要注意哪些事項?

這一題筆者認為有難度,但個人覺得不給讀者想想看也不好,姑且做題過程中給幾個提示,讀者可以選擇要不要看下面 XD:

  1. 前一篇有講過變數註記泛用型別的情形,與泛用類別的建構子建構物件的情形,兩者使用下有關鍵性的差別,第一題就算不經手驗證也可以秒答出結果。
  2. 可以參見本系列的 unknown 型別篇章

再三強調,《前線維護》的篇章的重要程度 —— 非・常・重・要。

小結

其實本篇都是在講實踐的部分,而非型別推論與註記的機制,但都是在練習泛用類別與介面的使用與開發過程中可能遇到的情境。

下一篇筆者想要提一下本系列中即將出場的第六個 OOP 的設計模式 —— Iterator 模式,中文名叫迭代器模式 —— 作為《通用武裝》篇章的第一個應用,順便讓讀者更熟悉 OOP 的設計外,也熟悉 TypeScript 類別與介面應用。

反正筆者的目標就是要逼讀者看過一遍又一遍的介面與類別的終極組合的應用,而這個就只能靠實作經驗來應證給讀者看

敬請期待~

]]> Maxwell Alexius 2019-10-28 15:42:08
Day 44. 通用武裝・介面與類別 X 泛型註記機制 - TypeScript Generic Class & Interface https://ithelp.ithome.com.tw/articles/10226510?sc=rss.iron https://ithelp.ithome.com.tw/articles/10226510?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

  1. 泛用型別化名的如何進行宣告?
  2. 泛用化名註記在變數時的注意事項為何?
  3. 泛用函式的特點為何?

如果還不清楚可以看一下前一篇文章喔~

這一次在第四篇章又新增了泛型的機制,想當然,筆者還是得點出泛用介面與類別的特點。到底會擦出什麼樣的火花呢?

以下正文開始

泛用介面與泛用類別的型別註記機制

泛用介面 Generic Interface

筆者為了好探討介面與類別結合,今天就以鏈結串列(Linked List)這個資料結構的實踐舉例,以下預設讀者已認識此資料結構,繼續講下去。

首先,普通情況下,我們可以訂立 LinkedListLinkedListNode 這兩種介面:

https://ithelp.ithome.com.tw/upload/images/20191010/20120614QZEtBvmJPv.png

以上的 LinkedList 介面有六個規格:

  • head 代表鏈結串列的首個元素 LinkedListNode,由於鏈結串列可為空的狀態,因此不排除 headnull 型別的可能性
  • length 是一個方法,輸出的是鏈結串列的長度(當然也可以使用普通的 number 型別而不採用 (): number 函式型別,但你可能必須要監控好 insertremove 鏈結串列的值時,更新 length 的大小)
  • at 是一個方法,輸入為 index 代表鏈結串列的位置,但是輸出既可以為 LinkedListNode 外也可以為 null
  • insert 代表將某任意值 value(型別為 any)插入進鏈結串列,位置由 index 指定
  • remove 則是根據 index 指名的位置移除連結串列裡的元素

而代表鏈結串列的元素是 LinkedListNode 介面,裡面的結構很單純:

  • value 代表該元素所存的值
  • next 則是取得下一個鏈結串列的元素,但結果也可以為 null 代表該元素可能是鏈結串列裡的最後一個元素

不過想也知道,這個連結串列可以存取的值是任意型別的值 —— value 屬性對應到的是 any 型別,因此我們可以將其改成泛用介面的模式,使得彈性增大:

https://ithelp.ithome.com.tw/upload/images/20191010/20120614tsw8rPogxP.png

另外,泛用參數的命名是什麼其實不重要,所以你取 TUV 甚至是口語化的名稱都無所謂,重點是看得懂就好。

從以上的程式碼得知,譬如使用 LinkedList<number>U 被取代為 number)代表該鏈結串列存的元素必須符合 LinkedListNode<number> 這個介面下的規格;也就是說 LinkedListNode<number> 存的 value 必須為 number 型別。

泛用介面的宣告其實很簡單,就是這樣而已。

另外,有些讀者可能會疑惑:

為何不討論直接將介面的型別化名註記在變數上的案例?

筆者一開始有想要多做說明,不過後來想想,筆者認為這樣的討論結果不如讓讀者參考介面的推論篇章還比較快,差別就是多了泛用的機制,但是型別參數被顯性註記過後,跟普通型別差不了多少。

此外,讀者如果會了本系列自立的物件完整性理論,延伸推論出泛用介面的註記機制也是可以的。

通常泛用的介面會和類別結合在一起使用,因此筆者認為討論泛用類別的型別推論與註記重要性比起泛用介面還要大。

泛用類別 Generic Class

泛用類別部分,筆者快速帶過跟類別相關的功能。

以下就舉一些很蠢又很簡單的例子,為了展示泛用類別的機制。譬如說我們有 C 類別:

https://ithelp.ithome.com.tw/upload/images/20191010/20120614r79vglxhPD.png

貼心小提示

讀者若是跳到本篇章然後不曉得成員變數、成員方法(Member Variables/Methods)等類別相關的東西,請記得參見《機動藍圖》篇章系列,筆者已經懶到不想貼哪一篇文章連結了QQ。

以上的 C 類別有:

  • 名為 memberProp 的成員變數,對應型別為 C 所宣告出的型別參數 T
  • memberFunc 則是輸出 memberProp 的值
  • 存取方法 value,分別覆寫或者輸出 memberProp

還記得前一篇講過的 —— 泛用的機制可以輔助 TypeScript 型別推論的功能,使得開發上能夠更靈活。

以下繼續展示泛用類別,或者乾脆說泛用機制超好用超變態的地方,以下筆者分幾個案例討論。

情形 1. 不註記變數,建構 C 物件時填入型別參數

https://ithelp.ithome.com.tw/upload/images/20191010/20120614DnAw0Qc69H.png

以上的程式碼,筆者不對變數 instanceOfC1 註記任何型別,並且建構物件時使用 new C<number>,代表 T 這個型別參數備取代為 number 型別。(圖)

https://ithelp.ithome.com.tw/upload/images/20191010/20120614kB8XYc8mog.png
圖一:單純建立 C<number>instanceOfC1 會被自動推論為 C<number>

https://ithelp.ithome.com.tw/upload/images/20191010/20120614C38duLV7eL.png
圖二:由於成員變數與型別參數 T 進行綁定,理應出現的推論結果為 number,結果確實也是 number

https://ithelp.ithome.com.tw/upload/images/20191010/20120614ekqiPM0bjk.png
圖三:雖然成員方法沒有被顯性註記輸出的型別,但根據函式型別篇章,TypeScript 聰明地根據輸出判斷推論結果為 number 型別,因為輸出為成員變數 memberProp,而該成員被綁定 number 型別

https://ithelp.ithome.com.tw/upload/images/20191010/20120614ZoUGD5btTj.png
圖四:呼叫 Getter 方法時,因為輸出為 memberProp,然後該成員綁定 number 型別,因此推論也是 number 型別

https://ithelp.ithome.com.tw/upload/images/20191010/20120614luLF4C7ouR.png
圖五:呼叫 Setter 方法時,因為該方法指定輸入的值 input 為型別參數 T,而 T 在本案例被綁定為 number 型別,因此可以被數字代入

另外,根據類別存取方法篇章,Setter 方法接收錯誤型別的值就會發出警訊,而本案例被綁定的型別為 number,代表如果代入非 number 型別的值就會輸出警訊唷~

https://ithelp.ithome.com.tw/upload/images/20191010/20120614rYYx9Zwl1V.png
圖六:被 TypeScript 打臉的感覺如何~?(請筆者不要唱衰讀者

情形 2. 註記型別在變數上,不註記在類別建構子旁的型別參數裡

首先,筆者在講這個案例前必須補充,根據前一篇討論到的某個重點:

除非型別參數有預設值(又可稱作預設型別參數 Default Type Parameter),否則本身為泛用形式的型別化名,少掉任何一個型別參數的值就會出錯

也就是說,你如果選擇這樣註記變數,一定會出錯。(如圖七)

https://ithelp.ithome.com.tw/upload/images/20191010/20120614NpYuHeJV65.png
圖七:就算筆者後面建構物件時亂填 Non-sense 的東西,TypeScript 讀到變數的註記時 —— 遇到泛用型別化名,少了任何一個型別參數就會跟你槓上(聽起來蠻有義氣的

所以不可能會有變數註記泛用型別卻不填上型別參數的狀況 —— 變數的型別註記要的是確切的型別,而非泛用的形式。

一但泛用的型別形式確立了內部型別參數的值,它就會退化為普通的型別

若是該型別參數有預設型別,你才可以省掉指定型別參數的部分

回過頭來,我們繼續探討情形 2,以下是測試的範例程式碼。

https://ithelp.ithome.com.tw/upload/images/20191010/20120614T3p30SzEjo.png

事實上,不是筆者偷懶,但是筆者真的認為情形 2 的推論結果跟情形 1 符合(事實上筆者親手測過 XDDD),所以打算讓讀者或者是推卸給讀者自行試試看,不然要張貼情形 2 推論每個單元的測試結果 — 一貼就六張圖了,佔太多版面,讀者應該也會覺得筆者廢話很多。(事實上筆者在本系列的廢話也不是一次兩次的事情了

所以呢~ 請:

讀者試試看

試著將情形 2 的範例程式碼中的每個單元的推論結果驗證看看是不是跟情形 1 相符合?

另外,筆者雞婆的一定要測給讀者看 —— 泛用類別的型別推論的每種案例的主要原因是:以下描述的情形 3 的行為是很重要的!

情形 3. 不註記在變數上,但指派建構之物件時,你甚至不需要填入型別參數在建構子函式上

其實對於使用 TypeScript 已成習慣的人應該很少察覺到這一回事,或者覺得應該理所當然;然而,這個機制可是在型別系統的推論(Type Inference)上,佔了很大的功勞:

有時你不需要在類別上顯性註記型別參數之型別,TypeScript 仍然可以藉由你填入的參數反向推論出泛用到的型別參數對應的型別值

https://ithelp.ithome.com.tw/upload/images/20191010/20120614sMStwhMU5D.png

筆者也是懶惰到想丟給讀者自己 Try Try 看,推論結果絕對都是跟 number 型別有關,筆者就貼變數被推論的結果。(如圖八)

https://ithelp.ithome.com.tw/upload/images/20191010/2012061451btuN8Mos.png
圖八:就算你只有用 new C(...) 建構物件,TypeScript 可以照樣推論出 C<number> 型別

讀者試試看

試著將情形 3 的範例程式碼中的每個單元的推論結果驗證看看是不是跟情形 1 相符合?

重點 1. 泛用類別的推論行為

C 這個型別化名為類別宣告而成的,並且 C 有一個型別參數 T(也就是型別化名 C<T>)。

C<T> 註記到變數時,必須要填入 T 代表的型別值,符合前一篇的重點 1 所講述的概念。

但是建構物件時,建構子函式 new C(...) 不一定需要註記型別參數 T 的值,就可以藉由型別系統間接推論出 T 所代表的型別。

然而,通常你會想要註記為 new C<T>(...) 的情形,除了是為了增加程式碼的可讀性外,註記行為可以輔助 TypeScript 的型別推論系統,直接闡明 T 所代表之型別 —— 因此也可以選擇積極註記型別參數 T

泛用類別的繼承 Generic Class Inheritance

筆者還沒討論完泛用類別,因為我們還有類別的繼承。(汗水直流)

不過應該不會花太多篇幅講這個子類別繼承父類別的推論機制,請看類別的推論與註記篇章,裡面就有討論到大致上子類別與父類別的推論註記行為,相信泛用類別就只是多了泛用的概念外,也符合重點 1 所描述的特點。

筆者這邊要講的是一些泛用類別繼承上的宣告方式差異,這裡只會討論子類別繼承父類別,且父類別為泛用類別的狀況;如果反過來是子類別為泛用類別而父類別不是的話,討論起來根本沒意義,會跟前一節講到的普通泛用類別的推論註記機制差不多,只是多繼承一個非泛用的普通類別罷了。

首先,你能夠分辨出來這兩種格式差別在哪裡嗎?

https://ithelp.ithome.com.tw/upload/images/20191010/20120614r8Q66rsHm4.png

首先,如果直接這樣宣告 D 類別,而且 T 型別不為某個型別化名的話,一定會出現錯誤。(如圖九)

https://ithelp.ithome.com.tw/upload/images/20191010/20120614L4FaRrV7yu.png
圖九:它說,T 這個名稱找不到對應的型別

其實仔細看應該也會猜到 DE 差別在哪。

首先,D 本身沒有型別參數,因此它不是泛用類別,但是它繼承了 C —— 不過 D 必須要確定你繼承的 C 對應的確切型別,這概念就很像是你要註記一個特定型別給一個特定變數 —— 因此筆者再次重申,前一節的重點 1 已經講過:如果某變數必須註記某個泛用型別,除非該泛型的型別參數有預設值,否則缺少型別參數對應的型別值就會出現錯誤警告。

頂多以 D 子類別為例,這一次 D 因為想要直接繼承某個類別,它也必須很確定它到底要繼承的確切型別是什麼 —— 也就是說,如果你選擇忽略類別 CT 型別參數對應的型別值也會被 TypeScript 警告。(如圖十)

https://ithelp.ithome.com.tw/upload/images/20191010/20120614HXq9cNOvvK.png
圖十:跟變數註記某個泛用型別的概念很像,差別在於這一次繼承的東西也必須是確切的型別而非為指定型別值的泛用類別狀態

因此你可能必須要改成:

class D extends C<number> {}

等等諸如此類的形式。

另外,E<T> 由於本身就是泛用類別的宣告,因此它有型別參數 T,而且它還可以將它的型別參數代入到它要繼承的 C 類別的型別參數部分。也就是說,想要建構 E<number> 這種型別的物件,就等同於模擬 E 繼承 C<number> 的情形。

重點 2. 子類別繼承泛用類別的情形 Child Class Inherit Generic Class

分成兩種形式,若子類別為普通類別時,繼承到的泛用父類別必須確切指名該型別參數的確切型別值

然而若子類別也是泛用類別時,則繼承到的泛用父類別除了可以指定特定的型別外,也可以填入子類別所宣告的型別參數建立型別上的連結

讀者試試看

這應該算是筆者的整人時間,不過筆者倒是有在網路上的某個角落看到有人在討論以下這些案例會發生什麼樣的奇葩情形,筆者就不多做說明,留給讀者去玩玩看吧~

https://ithelp.ithome.com.tw/upload/images/20191010/20120614tZTs9KyDgj.png

由於篇幅問題,筆者認為如果可以到出書的程度,再來認真補這些坑,儘管以上的程式碼看起來很愚蠢,但是可以考驗到底讀者對於預設型別(Default Type)跟型別限制(Type Constraint)的概念。

不過認真地想,筆者還真的不知道會不會用到以上的奇葩情形來為難自己,難道讀者會想要嗎?XD

但既然別人都問了這個問題,勢必可能在少數地方會用到,所以筆者也不能保證你不會遇到類似的狀況,因此還是泡出來給讀者無聊或想不開時試試看。

小結

筆者大致上講完了介面與類別結合泛型機制時大致上需要注意的方向,介面應該覺得還好,但主要是泛用類別的重點比較多。

並且藉由前一篇得出的結論 —— 也就是泛用型別可以間接推論程式碼未來動作時在型別推論方面的能力 —— 以此重點為基礎,闡述泛用類別也運用了類似的優勢,達到更完美的型別推論境界。

下一篇要講的是泛用類別與泛用介面結合的終極 Combo 第二彈~ 敬請期待啦~~~

]]> Maxwell Alexius 2019-10-25 19:22:39
Day 43. 通用武裝・泛型註記 X 推論未來 - TypeScript Generic Declaration & Annotation https://ithelp.ithome.com.tw/articles/10226311?sc=rss.iron https://ithelp.ithome.com.tw/articles/10226311?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

  1. 泛用型別的意義是什麼?
  2. 泛用型別大致上有哪些種類或形式?

如果還不清楚可以看一下前一篇文章喔~

以下就直接正文開始

泛用化名的宣告與註記機制 Generic Type Alias Declaration & Annotation

讀者可能想問:“這不是前一篇講過了嗎?”

筆者必須要說,泛型要討論的案例比較多,而且上一篇只是介紹而已,以下就才是真正地進入正式討論

泛用化名 Generic Type Alias

貼心小提示

讀者可能覺得這標題很疑惑:“為何不要直接取泛用型別,還要取泛用化名?”

筆者認為,如果要很精確討論泛用的行為,而又要概括型別(Type)、介面(Interface)與類別(Class)的狀況,因為共通點都是型別化名的一種,所以才會取這個名稱

首先,筆者就以 Array<T> 這個泛用型別作為基礎。

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

讀者有沒有想過,如果泛用型別沒有指定型別參數的值會發生什麼事情

以上面的程式碼為例,unspecifiedTypeParamArr 儘管被註記為 Array 型別,但卻沒有指名 Array<T> 裡面的 T 型別參數。(推論結果如圖一;錯誤訊息如圖二)

https://ithelp.ithome.com.tw/upload/images/20191009/2012061418C4yPo5Qi.png
圖一:被 TypeScript 警告,Array 這個型別註記上很有問題。

https://ithelp.ithome.com.tw/upload/images/20191009/20120614c4o0SgrBfb.png
圖二:TypeScript 告訴你,Array 不單單只是 Array 型別,它的形式是泛用的 —— 是為 Array<T>;也就是說,你如果少掉指名型別參數 T 的值就會出錯。

重點 1. 泛用化名的型別參數 Type Parameter(s) of Generic Types

除非型別參數有預設值(又可稱作預設型別參數 Default Type Parameter),否則本身為泛用形式的型別化名,少掉任何一個型別參數的值就會出錯

泛用化名宣告 Declaration of Generic Type Alias

既然提到了預設型別(Default Type Parameter)—— 但在講這個 Feature 之前,筆者還是很雞婆地先把宣告泛用型別化名的方式提點一下。

重點 2. 泛用化名的宣告 Declaration of Type Alias

型別化名分成三種 —— 型別(Type)、介面(Interface)與類別(Class)。

若想宣告泛用型別(Generic Type)GT,並且擁有型別參數 TP1TP2 ... TPn,格式如下:

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

若想宣告泛用介面(Generic Interface)GI,並且擁有型別參數 TP1TP2 ... TPn,格式如下:

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

若想宣告泛用類別(Generic Class)GC,並且擁有型別參數 TP1TP2 ... TPn,格式如下:

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

泛用抽象類別就是在泛用類別旁邊註記 abstract 關鍵字就可以了。

不過,宣告的泛用化名裡面,沒有使用到宣告的型別參數,預設的 TypeScript 編譯器設定不會出現任何錯誤訊息,但還是建議讀者不要這樣亂宣告型別參數卻沒有使用

除非你怕有闕漏,可以開啟 tsconfig.json 裡面的 noUnusedParameter 選項,它就會出現 Declared but never used 類似這樣的警告訊息。(如圖三)

https://ithelp.ithome.com.tw/upload/images/20191009/20120614nkggki2O3S.png
圖三:noUnusedParameter 預設為 false,開啟它就會出現 Declared but never used 警告喔~

預設型別參數 Default Type Parameter

預設型別的語法其實很簡單,就是用等號連結型別值就夠了,跟 ES6 介紹的函式預設參數(Default Parameters)的概念差不多。

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

以上的推論狀況就由讀者自行驗證吧,基本上都不會出錯的~

重點 3. 預設的泛用化名型別參數 Default Type Parameter

若某型別化名 Tspecified 不為泛用型別,而泛用型別 GT 的宣告為:

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

則我們稱:Tspecified 為型別參數 TP 的預設型別(Default Type)。

若在變數註記上註記 GT 型別卻沒有指名 TP 代表的型別值,則該 GT 型別的註記等效於 GT<Tspecified> 的註記行為。

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

同樣的情形也適用於介面(Interface)或類別(Class)的型別化名宣告

泛用化名之型別參數限制 Type Constraint

另外,讀者可能也會覺得,泛用化名的使用有時候並不需要自由到任何型別都可以泛用

以下就舉一個型別參數限制(Type Constraint)的案例。

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

以上的程式碼宣告 Primitives原始型別的 union 複合的結果,這應該對看到這裡的讀者很熟悉。

另外 PrimitiveArray 的宣告裡出現了 extends 關鍵字 —— 型別參數裡面若出現 extends 就代表該參數被限制到某個範圍 —— 這就是型別參數限制的用法。而 PrimitiveArray 的型別參數 T 被限制為 Primitives 型別可以囊括的範圍。

以下的範例程式碼簡單地進行測試。(檢驗結果如圖四;錯誤訊息如圖五)

https://ithelp.ithome.com.tw/upload/images/20191009/201206148o4a9AgNZr.png

https://ithelp.ithome.com.tw/upload/images/20191009/20120614ds2cdKRxqc.png
圖四:很明顯 —— PrimitiveArray<PersonalInfo> 被警告了

https://ithelp.ithome.com.tw/upload/images/20191009/20120614qNNm0b7hHj.png
圖五:PersonalInfo 沒有符合 Primitives 的限制需求,因此被 TypeScript 駁回

值得注意的點是,從以上的案例 —— string | number 這種複合型別也符合 Primitives 的範疇,也就代表只要複合過的結果也包含在 Primitives 就會予以通過呢

重點 4. 泛用型別化名的型別參數限制 Type Constraint in Generic Type Alias

某泛用型別化名 GT 之型別參數 Tparam 被限制在某型別 Tcontraint 範圍之下,可以使用 extends 關鍵字,其格式為:

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

則該型別 GT 一但被註記到某變數時,裡面的型別參數不能代入超出 Tconstraint 以外的型別值。

Tconstraint 為一系列型別 T1T2、...、Tnunion 結果,則 Tparam 代入的型別可以為 T1T2、...、Tn 任意組合 union 過後的結果。

泛用介面與泛用類別的型別參數限制的語法皆相同。

讀者試試看

  1. 若是出現 intersection 的狀況,型別的限制行為為何呢?讀者也可以想想看為何不太需要討論到 intersection 的型別限制狀況。(提示:某些 intersection 狀況會導出 never 型別,亦或者是 intersection 過後的結果不太需要使用參數型別的限制;所以才說第一篇章《前線維護》很重要吧~)

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

  1. 回歸 union 的情境,如果以上的範例將 User 這個泛用型別的參數限制改成 union 的形式,參數型別限制的狀況會如何?

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

不過,讀者可能又會問 —— 有沒有單純限制型別參數範疇為 Primitives 中的其中一種型別而非複合過後的型別呢

恩... 有的 XD,但這要用到條件型別的寫法,這邊筆者暫時展示一下但不會多作探討。(如果筆者認為真的有必要會深究下去,啊~~~好想要探究下去

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

以上的程式碼推論結果如圖六、錯誤訊息如圖七。

https://ithelp.ithome.com.tw/upload/images/20191009/20120614m671vYqoil.png
圖六:自然而然,number | string 是不能被 TypedPrimitiveArray 混用狀態

https://ithelp.ithome.com.tw/upload/images/20191009/20120614Q5ZjF76Dap.png
圖七:不過這個錯誤訊息很奇怪,而且 invalidPrimitiveUnionedArr 被推論的結果是 number[] | string[]

筆者認為目前沒有必要講以上的程式碼的機制,因為超出本日內容範疇;然而,筆者倒是可以劇透一下:

條件型別(Conditional Type)針對 union 過後的複合型別具有分配性質(Distributive Property),跟數學上的分配律很像。

詳細可以查詢官方對於條件型別的描述內容

貼心小提示

如果讀者看到 never 這個型別還沒領會它的真諦,請參見本系列探討 never 型別的篇章

很重要喔!如果你只認為 never 就只有 Handle Error 的情形,但你不知道 never 背後代表的深層意義,誠心建議真的要再回頭看看本系列文章

泛用函式 Generic Functions

泛用的函式,筆者特別跟其他泛用型別化名隔離掉的最主要原因也是變化性特多,相信看過上一篇介紹泛型的讀者應該也會知道 —— 泛用的函式型別之函式的參數可以註記為泛用函式所宣告的型別參數。(這句話真繞口令

不想看繞口令,請看看以下範例:

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

以上面的程式碼為範例,traverseElements 擁有一個型別參數 —— 該函式的目的是要遍歷一個陣列:

  • 第一個參數 values 對應型別為 Array<T>
  • 第二個參數為一個回呼函式,型別為 (el: T, index: number) => void

假設我們想要遍歷一個數字型別的陣列,於是可以這樣做:

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

另外,我們當然可以簡化上面的程式碼為:

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

看起來已經簡化到一個境界,但實際上我們還可以再簡化成:

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

讀者應該會發現,筆者把除了 traverseElements 的泛用型別參數的註記外,其他的註記都刪掉了

問題是,筆者在很久很久以前的函式型別篇章有明確講到:

若宣告一個函式的時候,該函式的參數必須要進行註記,不然會無緣無故被 TypeScript 判定為 any 型別

不過!筆者也提到:

某些情況下,我們不需要對函式的參數進行積極註記的動作(參見陣列與函式篇章

其中,最大的原因是因為 —— 泛用型別的參數被指定後,函式內部的參數型別推論也跟著被固定

讀者請以 TypeScript 編譯器的角度試想:

今天有人告訴你 traverseElements 的定義如上面的範例程式碼所示,另外又有人告訴你 traverseElements 使用時將該型別參數 T 註記為 number 型別,是不是也就等同於 —— values 這個 traverseElements 的參數之型別被鎖定為 Array<number> 以及 callback 這個參數的型別被鎖定為 (el: number, index: number): void

也就是說,就算 callback 裡面的參數代入的是:

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

這種參數沒有被註記過後的函式,只要你看到 traverseElements 之型別參數被代入 number,你就可以間接推論該回呼函式的參數型別呢?(callback 函式之參數型別推論結果如圖八)

https://ithelp.ithome.com.tw/upload/images/20191009/20120614FhpJW4KLjb.png
圖八:就算 callback 函式裡面的參數沒有被註記,el 這個參數也會被自動推論為 number

這也是通常 Array.prototype 系列的方法,裡面只要有包含需要代入回呼函式的情形,通常也不需要註記回呼函式的參數之型別:

早在你宣告一個陣列時,藉由隱藏在陣列裡的泛用機制,編譯器早就可以推論出回呼函式的參數型別

https://ithelp.ithome.com.tw/upload/images/20191009/20120614fBeBpEoDXX.png
圖九:泛用型別的妙處在於,編譯器可以直接預測你必須要填入的參數之型別,以 Array<string> 型別的值為例,使用 Array.prototype.map 方法就會拋出 callbackFn: (value: string ...) => unknown 這個提示性說明。

https://ithelp.ithome.com.tw/upload/images/20191010/201206148fvu1LJb6q.png
圖十:只要將 Array<T> 轉換成另類型別,以這個例子來看,Array<number> 型別的值使用 Array.prototype.map 方法的提示性說明變換成 callbackFn: (value: number ...) => unknown,彈性真的很高。

所以筆者得出一個結論:泛用型別可以推論未來的型別使用的可能性,這也就是為何讀者不需要每一次都要進行積極註記的動作,有時候泛用型別的機制就可以讓 TypeScript 推論出正確的結果喔!

重點 5. 泛用函式的宣告 Declaration of Generic Functions

泛用的函式宣告 —— 必須要將型別參數馬上宣告在函式名稱的後面。若某函式所擁有的型別參數有 TP1TP2、...、TPn,則:

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

另外,函式的參數以及輸出型別可以為泛用函式所宣告的型別參數或型別參數的各種組合:

https://ithelp.ithome.com.tw/upload/images/20191009/201206143mpqNbgvPv.png

重點 6. 泛用型別參數與型別推論的關聯 Relation Between Generic Type Parameter & Type Inference

泛用的型別參數註記最大的用途在於 —— 可以預測未來的程式碼之型別推論的可能性

重點 6 短短的一句話就是使用泛用型別的最大好處,這也是為何寫 TypeScript 時,你不需要汲汲營營地將每個變數或函式的參數進行積極註記的動作,筆者後面都會來回利用 TypeScript 的泛用型別的這個優勢寫程式碼。

另外,泛用函式之型別參數的宣告也可以使用重點 3 與 4 講過的預設型別(Default Type)與型別限制(Type Constraint),這一點筆者就不再贅述,用法寫法差不多。

小結

這一篇都在打泛用型別的基礎以及一些功能,斤斤計較的地方看起來也很多,不過有了這些基礎,筆者可以開始寫泛用型別的應用囉~

]]> Maxwell Alexius 2019-10-23 17:02:37
Day 42. 通用武裝・泛用型別 X 型別參數化 - TypeScript Generics Introduction https://ithelp.ithome.com.tw/articles/10226113?sc=rss.iron https://ithelp.ithome.com.tw/articles/10226113?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

《通用武裝》篇章概要

本系列即將邁入後半段(現在才邁入後半段會不會有點晚?)—— 泛用型別(Generics)的介紹。

筆者翻閱很多資料發現,泛用型別儘管看似困難(這是要讓讀者不想學嗎?),但用途事實上真的很多,甚至會延伸到 ES6 的一些 Feature。

筆者在第三篇章的 UBike 地圖案例就有提到 —— ES6 Promise 與 Map 事實上都有用到泛用型別的註記(Type Annotation)表示行為。

以下大概是筆者設想可能會講到的東西:

  • 泛用型別(Generic Types)的基礎行為
  • 各種泛用型別在普通型別、介面與類別的推論與註記機制
  • 預設泛用型別參數 Default Type Parameter
  • 泛用型別限制 Generic Type Constraint
  • 函式型別的參數不需要註記的特殊情形(補第一篇章《前線維護》的坑)
  • ECMAScript 提供的功能層面(Utility Aspect)與 TypeScript 泛型的結合應用
    • ES6 Promise
    • ES6 Map/Set
    • ES6 Generators & Iterators
    • ES7 Async-Await
  • 迭代器模式 Iterator Pattern
  • 非同步編程,如:Callback Hell、Promise Chain、Async-Await
  • 更多介面與類別的結合實作 + 泛型應用
  • 條件型別 Conditional Type(這東西要看筆者情形可能不會講到,但又很想講,ㄊㄋㄋㄉ

同理,筆者在此發出一個聲明:第四篇章的內容順序不一定會按照上面的順序講解,筆者會按照適合的方式進行編排喔~~~

另外,如果是臨時看到這裡但剛學習 TypeScript 的讀者,除非你有 Java、C# 等靜態語言的學習背景,否則還是建議至少把第一篇章《前線維護》部分讀完,因為型別系統的推論與註記機制在本篇章討論的佔比又會回到很大又很變態的篇幅大小。

[2019.10.22 16:49 新增訊息]

由於最近處理的事情變多,連載速度會開始變慢,所以第四篇章還是未完成狀態 XD,但確定會超過 Day 50

另外,本篇章由於圖的品質跟第三篇章有得拼,數量更是多了些,因此速度變慢也是正常的 QQ,請讀者給筆者時間整理一下。

貼心小提示

本系列內容預設編譯器設定 tsconfig.json 裡的 noImplictAnystrictNullChecks 為啟動的狀態。

如果對於這兩個選項有問題可以參見這一篇

以下本篇章第一篇文 — 正文開始

泛用型別的介紹 TypeScript Generics Introduction

泛用的概念 Concept of Generics

開篇就從最簡單看起來也很笨的的例子來舉。

https://ithelp.ithome.com.tw/upload/images/20191008/20120614xHY7zxNPxY.png

Identity 這個型別化名的宣告裡,有一個被稱為型別參數(Type Parameter)的東西 —— 也就是 Identity<T> 裡面的 T 這個東西。

原理跟普通的函式概念差不多,只是可以把泛用型別看成是型別版的函式,代入的 Identity<T> 裡面的 T 為什麼樣的型別 —— T 就會成為那個型別

比如說,Identity<number> 裡面的 T 被取代為 number 型別,將其註記到任何變數會得到如圖一的推論結果:

https://ithelp.ithome.com.tw/upload/images/20191008/20120614bwToFVYCDY.png
圖一:Identity<number> 中,把 T 取代為 number,則 Identity<number> 的結果就等於 number

基本上,泛用型別就是將型別化名進行參數化的動作,帶入不同的型別作為泛用型別的參數,就會得出不同的型別樣貌。

重點 1. 泛用型別的概念 Concept of Generic Types

泛用型別的意義最主要是將型別化名進行參數化(Parameterize)的動作,使得型別化名擁有更多的彈性與變化性。

泛用化名 Generic Type Alias

筆者提到一個很重要的點:泛用型別就是將型別化名進行參數化的動作;也就是說,任何型別化名都可以轉換成泛用型別。

從筆者這樣的描述可以推論:型別(Types)介面(Interface)類別(Class)都可以轉換成泛用型式。

以下筆者就舉幾個例子:

https://ithelp.ithome.com.tw/upload/images/20191008/201206148VQMRExMsn.png

Dictionary<T> 為一個泛用型別,代表的就是一般的鍵值對(Key-Value Pair)的物件格式。其中,如果 T 被代入為 boolean 型別,則如果值裡面含有非 boolean 型別的值就會被 TypeScript 警告。以下就舉幾個例子,展示給讀者看。(程式碼如下,檢測結果如圖二)

https://ithelp.ithome.com.tw/upload/images/20191008/20120614m178KHsSyG.png

https://ithelp.ithome.com.tw/upload/images/20191008/20120614V6HHKdLWhI.png
圖二:因為去旅遊韓國愉快的原因是 string 型別 —— 不符合 Dictionary<boolean> 指定的 boolean 型別,就會被 TypeScript 和諧掉(甘去韓國愉快旅遊 P 事

至於泛用介面與泛用類別的使用情形~ 筆者把他們保留到後續篇章,因為這個討論串一開,筆者一定會不小心啪拉啪拉一連串把本篇擴張到沒完沒了到很難收尾的狀態(深感尷尬),筆者在本篇先把泛型的簡介內容都些丟出來給讀者做心理準備,後面講到一些泛型的應用對讀者來說也比較容易適應。

重點 2. 泛用型別化名 Generic Type Alias

泛用型別的表現形式不單單只有型別(Type)而已,介面與類別皆可以轉換成泛用型式

在泛用型別化名的名稱後面接上的 <> 內容就是型別參數(Type Parameter)的宣告。

型別參數可以有複數個,只要在 <> 裡用逗號分隔就可以了。

詳細的宣告過程細節、規則與程式碼公式化結果(筆者一貫的手法)會在後面的篇章慎重地呈現出來。

泛用函式與泛用參數 Generic Functions & Generic Function Parameters

泛用的形式基本上是很全面地(Universal),連函式本身也可以變成泛用的形式,但是變化性可能對剛接觸泛型的讀者深感困惑。以下筆者把常見的函式型別的泛用型式刻意分成兩種。

函式型別的泛用形式 Function Type Generic Representation

https://ithelp.ithome.com.tw/upload/images/20191008/20120614cuN9SlH1Bg.png

首先,以上的案例宣告一個函式的型別化名 operator —— 這個 operator 有一個名為 T 的型別參數,而剛好 operator 裡面對應的函式型別的參數以及輸出也是 T 型別,就以 operator<number> 為範例的話,p1p2 就會被限制為 number 型別,而輸出的型別也會是 number 型別。

所以以下的 addition 這個變數註記為 operator<number> 就代表 —— p1p2 以及函式輸出必須為 number 型別;而 stringConcatenation 則是 operator<string> 就代表 —— p1p2 以及函式的輸出必須為 string 型別。

https://ithelp.ithome.com.tw/upload/images/20191008/201206145AhV31PjwJ.png

事實上,以上的程式碼還有地方可以簡化,這部分就放在後續篇章討論不然會很難收尾 XD

函式本身就是泛用型式 Function Itself is Generic

https://ithelp.ithome.com.tw/upload/images/20191008/20120614sppIZsgZP7.png

這裡並非是用泛用型別化名註記在變數上,然後再指派一個函式進去;以上的程式碼呈現的方式是 —— 你可以在宣告函式的時候就把它變成泛用的形式

這個 identityFunc 有宣告一個型別參數 T,函式本身只有一個參數,對應型別是 T,而輸出的結果被註記為型別參數 T,也就是說:如果你呼叫該函式時是 identityFunc<string> 這樣呼叫的,輸入必須填數 string 型別,輸出也是 string 型別。

重點 3. 泛用函式表現行為 Generic Function Representation

若宣告的函式型別為泛用形式,泛用的型別參數除了可以註記到函式內部的變數外,還可以註記在函式的參數(Parameter)與輸出(Output)上。

多重泛用參數 Multiple Generic Type Parameters

理所當然,我們還可以使用多重的泛用參數,這其實在本篇的重點 2 的最後一句話有稍微提及,不過筆者暫且就展示給讀者看一下:

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

以上的 TypeConversion 型別有兩個型別參數,分別是 TU

isPositive 為例,它被註記到的 TypeConversion 它的 Tnumber 型別,U 則是 boolean 型別;而 TypeConversion 的輸入為 T 型別,輸出為 U 型別,isPositive 將輸入為 number 型別的東西,根據數學的正數判斷,轉換成 boolean 型別的值。

另外的 anythingToString 則是將 T 設定為 any 型別,U 則是 string,代表輸入可以為任何型別值,但輸出必須是 string,裡面的實作內容也僅僅是對輸入呼叫 toString 這個方法進行轉換。

內建的泛用型別 Built-in Generics

事實上,泛用型別在 TypeScript 裡本來就有存在的痕跡,只是有沒有發覺到而已。

最簡單的就是陣列型別 —— Array<T> 這種型別 —— 它是內建的,等效於 T[]

也就是說,用以下的方式表示也 OK:

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

以上的 MyArray<T> 是為了不要重複定義 Array<T> 而選擇用其他化名取代。實際上除了基本的 T[] 表示形式,也可以直接用 Array<T> 代表某陣列。

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

讀者可以試著檢測看看以上的程式碼,筆者就不貼出結果了。

其中,又以 ECMAScript 新增、偏向功能層面(Utility Aspect)的應用跟泛用型別的機制又息息相關,這又讓筆者不得不搬出 ES6 Promise、Map 與 Set 等東西與泛用型別的結合等應用,這些都會在後續篇章進行探討 —— 畢竟筆者寫作當下不希望讀者會了泛用型別,但是卻不曉得應用或者是哪些地方會看到存在 —— 事實上泛用型別處處存在,尤其對型別的推論(Type Inference)有非常大的影響

沒有泛用型別的話,型別系統少了一半的推論能力也不足以為奇

感覺就是 —— 沒有泛用型別,這型別系統也就爛爛的 —— 所以本篇章也會剖析型別推論除了認讀者的型別註記(Type Annotation)以及根據語法的結構判斷型別推論(Syntatic Structure)外,泛用型別對於型別推論隱藏的巨大影響。

理解這裡面的機制事實上對於用 TypeScript 寫程式,除了會感到特別有趣外,還可以提升寫程式的效率。(如洪荒之力非常多!)

條件型別 Conditional Types

事實上,這也是從泛用型別延伸出來的變體,條件型別(Conditional Types)筆者很少看到有中文的文章在探討(不然就是筆者笨到連 Gxxgle 搜尋都不會用),英文討論條件型別的文章,恩... 不算少,但是也很少人正視這個東西,以為又是 TypeScript 額外延伸出來的功能。

但這並不完全是 TypeScript 的新功能,只能說算是部分新、但又是從泛用型別延伸出來的,因此筆者沒有把條件型別列入型別的一種表現形式的原因主要是因為 —— 它是衍伸物,並不太需要刻意立下一個新的型別種類。

以上廢話太多,不過筆者暫且舉一個還蠻不錯的條件型別案例,而且這本來就是 TypeScript 內建的,不過這些都被官方稱為 Utility Types,冠上的是這個 Utility 這個名稱,實質上是用條件型別的方式去實踐,但本質還是泛用型別的一種延伸出的表現行為。

官方真是麻煩,就好好講說是內建的條件型別也可以,偏偏再多冠上一個 Utility Types 名稱。

“好啦,趕快舉例!筆者不要發牢騷,廢話真多!”

Required 就是一種還蠻好用的條件型別。

很久的之前探討過的選用屬性部分,只要被屬性旁標註為 ? 就代表不一定需要該屬性,可視為不存在。

另外,介面的宣告與註記行為,只要變數被註記到某介面就必須實作全部的功能,並且根據物件完整性的理論,該變數:

  • 不能多或少一個功能
  • 功能對應之型別不能夠不符合介面定義的行為
  • 可以被完全覆寫,但覆寫的格式與每個功能對應的型別都不能出錯

這應該對讀者來說已經是朗朗上口的原則(如果你完整看完本系列的話)。

但是 Required<T> 非常有趣,它的意思是 —— 它會將所有 T 裡面的選用屬性轉成必要的屬性。(圖三為檢測結果;圖四為錯誤訊息)

https://ithelp.ithome.com.tw/upload/images/20191009/20120614625WgIwaoZ.png

https://ithelp.ithome.com.tw/upload/images/20191009/20120614bLca0gYfrc.png
圖三:如果被冠上 Required 這個條件型別,內部的 PersonalInfo 介面會把所有的選用屬性轉成必要屬性

https://ithelp.ithome.com.tw/upload/images/20191009/20120614JU85NNQnnr.png
圖四:TypeScript 很明確地告訴你,Required<PersonalInfo> 的註記下,少了 hasPet 這個屬性

事實上,筆者這裡沒有講條件型別的寫法 —— 因為比泛用型別更複雜......很多 XDDDD。

所以筆者認為這個主題應該會放到本篇章很後面才會講到,但坦白說,讀者真的需要再讀也可以,除非你對於這東西感到興趣。

小結

本篇開篇大致上介紹完讀者會看到的泛型大概會在哪裡出現~

泛型應用挺多的 —— 沒學過或聽過的讀者最好要有心理準備。XDDDDDDDD

]]> Maxwell Alexius 2019-10-22 16:58:08
Day 41. 戰線擴張・模擬戰 - UBike 地圖 X 外觀模式 - Façade Pattern in TypeScript https://ithelp.ithome.com.tw/articles/10225727?sc=rss.iron https://ithelp.ithome.com.tw/articles/10225727?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

還記得單例模式 Singleton Pattern嗎?今天會用到喔!

本篇文承接上一篇文,因此如果是跳到這篇的話可以先從上一篇或者是 UBike 地圖範例的一開始看起喔~

貼心小提示

想要本範例的程式碼 —— 可以參見 Maxwell-Alexius/Iron-Man-Competition 這個 Repo 喔!

筆者在這個系列講過的模式有:

事實上在講抽象工廠模式時,順便有提到的 Factory Method Pattern 也是設計模式的一種。

所以嚴格來說,今天要講述第五種模式 —— 外觀模式 Façade Pattern 的應用。(有些資源會翻譯成門面模式,就只是名稱不同罷了)

本篇也要順便對前一篇的 UBike 地圖範例進行重構的部分,所以請筆者務必要把前幾篇內容稍微理解喔~

模擬戰 — UBike 地圖應用(四)

LeafletJS 地圖邏輯的重構

對於本 UBike 地圖範例來說,光是 LeafletJS 部分的邏輯就很複雜,筆者就把程式分成幾個部分。

1. Leaflet 地圖的個體建構

使用 L.map 建構地圖,並且還摻雜 markerLayer 的參數宣告;對於讀者可能感到不起眼,但是筆者認為地圖(Map)與圖層(Marker Layer)本身是兩個不ㄧ樣的東西。

https://ithelp.ithome.com.tw/upload/images/20191007/20120614VgoTU06Tig.png

2. 主程式 —— 繪製 UBike 站點

這裡就非常複雜,除了將資料從網路上抓出來過濾外,裡面做了幾件事情:

  1. 將每筆 UBike 站點資料轉換成 Marker 物件
  2. Marker 物件會綁定對應的 Tooltip 以顯示站點資訊
  3. 將一系列的 Marker 丟到圖層裡並顯示到地圖上

https://ithelp.ithome.com.tw/upload/images/20191007/20120614XK3aAs14lQ.png

筆者認為這邊有三個主角在互動:

  • Map 個體本身
  • Marker Layer 圖層
  • Marker 站點的記號

3. 更新地圖點位

這裡就是對 <select> 這個標籤註冊一個事件,並且每一次更新時除了會移除 Marker Layer 外,會呼叫 updateUBikeMap 重新更新地圖。

https://ithelp.ithome.com.tw/upload/images/20191007/20120614UTjVMnJN67.png

這裡只有牽扯到 Marker Layer 這個主角,但事實上因為有呼叫 updateUBikeMap 所以其實其他的 Map 相關的角色都有被牽扯到。

重構 Leaflet Map 的前置作業

UBike Map 地圖的主角以及介面的宣告

反正回過頭來,筆者腦袋中想像的 Leaflet 地圖部分有四大主角:

  • map 物件 —— 使用 L.map 產出來的地圖的個體(Instance)
  • Initializer —— 負責初始化地圖的邏輯,必須填入 MapConfig 相關的值
  • MarkerLayer —— 負責將所有的點位(Marker)匯集後,渲染到地圖上
  • Marker —— 站點的點位

本篇會善用介面與類別 —— 就像是筆者在《機動藍圖》篇章探討策略模式一樣,帶領讀者善用介面與類別實作出還算不錯的程式碼。

但筆者不保證這會是最完美的解法,程式碼沒有完美不完美,就只有可能比較好或比較不好,層面又分很多種,可讀性(Readability)亦或者是效率(Performance);況且需求一定持續性地變更,因此今天程式碼重構結果 —— 有可能因為需求改變,而有被修改的可能。

首先,筆者新增一個檔案資料夾名為 /ubike-map 放在 /src 裡面。並且新增 map.d.ts 檔案,宣告 InitializerMarkerLayerMarker 的介面,並且安妥地被 CustomMap 這個命名空間包覆:

https://ithelp.ithome.com.tw/upload/images/20191008/20120614AICnclIuOV.png

介面 CustomMap.Initializer 只有三個成員:

  • map 代表 Leaflet Map 本身的個體,唯讀模式
  • config 代表筆者自己定義的 MapConfig 這個型別的設定,唯讀模式
  • initialize 為一個方法,專門初始化地圖

介面 CustomMap.MarkerLayer 有五個成員:

  • map 代表 Leaflet Map 本身的個體,唯讀模式
  • layer 代表 Leaflet 本身的 LayerGroup 這個型別,唯讀模式
  • addMarker 代表將某個點位加進這個 MarkerLayer
  • addMarkers 則是代表加入複數個點位
  • clear 代表清空所有的點位

介面 CustomMap.Marker 有兩個成員:

  • marker 代表點位本身連結到的 Leaflet Marker 物件
  • bindTooltip 代表直接綁定 Tooltip,需要帶入一個字串代表 Tooltip 顯示的提示內容

讀者應該會發現,筆者刻意將 InitializerMarkerLayer 指名必須提供 L.map 的個體,那是因為要維持彈性,不會把地圖個體寫死在實踐該介面的類別。

另外,讀者當然可以在 Initializer 多新增 setMap 這個功能負責換掉地圖的個體或者是 changeConfig 代表換掉地圖的設定後再重新 initialize 也可以,看讀者能夠把想像力發揮到什麼地步都可以 —— 但前提就是,功能儘量不要寫死,寫程式到最後越寫越大,可能會需要一些長遠性解法。

此外,筆者認為由於 UBike 地圖的應用,也僅僅只會出現一張地圖,所以可以把 Leaflet 建立的地圖個體本身做成 MapSingleton 這個單子物件(Singleton)。(參見單例模式篇章

不過筆者認為 MapSingleton 沒有實踐介面的需求,所以就不寫 MapSingleton 的介面。但如果功能越變越大,筆者可能就會積極地採取介面方式去規定功能的架構。

實踐主角介面 Class-Interface Implementation

首先,筆者簡單地把 MapSingleton 生出來,這是對單例模式練習的好時機,本程式碼放置在 /src/ubike-map/MapSingleton.ts 裡:

https://ithelp.ithome.com.tw/upload/images/20191008/20120614HbueFjfSZz.png

MapSingleton 存放的就是 L.Map 這個型別的個體,但要注意的是:TypeScript 編譯器本身會提醒你有可能是 null 的情形,因此筆者刻意在 constructor 裡面放一個 console.warn 提醒使用者 Map 建立的過程很有可能出問題。

所以根據 MapSingleton,你可以使用 MapSingleton.getInstance() 就可以取出唯一的 Leaflet 地圖個體喔~

再來是 MapInitializer 這個類別實踐 CustomMap.Initializer 這個介面。筆者刻意包裹 CustomMap 這個命名空間(Namespace)的原因很簡單:Initializer 這個名詞應該有機率出現在其他地方的應用,因此可能會有命名衝突問題,因此筆者選擇使用命名空間。

然而,避免命名的衝突有很多種方式,你也可以選擇使用 ES6 default exportInitializer 輸出出去後,載入到其他檔案時用其他命名就好也可以,隨便讀者取用,不過筆者在這邊想要順便示範 Namespaces 可以用到的地方在哪裡。

以下是 MapInitializer 的實踐:

https://ithelp.ithome.com.tw/upload/images/20191008/20120614S7qR8h9f1X.png

裡面的 initialize 方法不覺得很像 UBike 地圖模擬戰第一篇的初始化地圖過程嗎?我們可以將初始化地圖的邏輯獨立出來,以後想要調整初始化的過程就可以放在這邊~

當然,初始化的過程可能還可以分好幾種,譬如:你想要初始化不同地區、不同的地圖樣式等等,你甚至可以採取策略模式,宣告出 CustomMap.InitializeStrategy 介面,並且定義不同的 InitializeStrategy 相關類別並連接到 MapInitializer 這個類別裡,善用設計模式的過程中,儘管一開始限制感到重重,但運用起來事實上是非常自由的

第二個要實踐的是 MapMarkerLayer 這個類別,對應的是介面 CustomMap.MarkerLayer 這個介面。

https://ithelp.ithome.com.tw/upload/images/20191008/201206143F2KL7HR7A.png

MapMarkerLayer 的實踐過程應該比 MapInitializer 單純許多,沒有太多花樣,就是控制 Marker 的載入與圖層的清除。

最後是 MapMarker 這個類別,對應實作介面為 CustomMap.Marker 這個介面。

https://ithelp.ithome.com.tw/upload/images/20191008/20120614PcfvgM0aO0.png

讀者應該會發現,筆者刻意對建構子設為 private 模式並額外宣告 create 這個 MapMarker 的靜態方法,用另一種方式建立 Leaflet 的 Marker 物件 —— 筆者選擇 MapMarker.create(...) 這種方式去創建物件。另外,MapMarker 類別提供的 bindTooltip 方法除了對 Marker 進行 Tooltip 的綁定外,還預設了 Tooltip 被觸發的行為,等於是把主程式裡面每一次宣告新的 UBike 點位然後綁定 Tooltip 的邏輯整理進去了。

但不要看到私有建構子以為 MapMarker 是單例模式喔,在這裡並不是單例模式!因為筆者開放了 create 這個方法,反而是可以建立無數個 Marker 個體 —— 這應該算是工廠方法(Factory Method)的一種變體。

此外,有些讀者可能認為,這裡可以用抽象工廠模式去建立 Marker,不過因為產品種類太少(才 UBike 的點位這一種產品),因此筆者認為沒有需要使用 Abstract Factory 模式的必要 —— 倒是可以使用普通的 Factory Method 模式。

貼心小提示

並不是要鼓勵讀者一開始使用設計模式,而是當架構成長到一個地步,需要更好的架構時,使用設計模式就更有效果。

除非,地圖點位可能包含 —— 公車、火車、捷運等等站點的匯集,此時可能就有 Abstract Factory 模式的必要性喔!

四大主角因此完畢了,但是這四個主角只是本篇鋪梗的一部分而已,真正要介紹的幕後大主角是:名為 UBikeMapFacade 這個實踐外觀模式(Façade Pattern)的類別

(就好像天使還有分小天使、大天使還有大麥克天使,不是大麥克雞塊

外觀模式的實踐 Façade Pattern Implementation

首先,筆者誠心建議要特別去找 Façade 這個單字的發音,因為這是法文單字,代表一個人的外觀或建築物的外表 —— 英文的同義字是 Visage。

另外,外觀模式 —— 誠如其名,Client 端(使用者端)看得到的就是一個程式碼包裝功能的外觀 —— 可以比擬為開車時看到的儀表板、打檔位的棒子、然後會轉來轉去、滑溜溜的方向盤(請勿亂想)等等都是操作汽車的外觀,但你看不見內部的機械結構、引擎、電路等等的細節內容,這些都被包裝成你看到的操作介面。

外觀模式就是把複雜的內部結構簡化為單一介面 —— 而且不只是這樣,它還有明確的別種意涵在本篇有呈現到:你可以在複雜的功能下定義一系列的子介面(Subclasses/Sub-interfaces),然後整合子介面再變成單一的介面供 Client 端使用

以本篇 UBike 地圖為範例,原本比較複雜的功能是由 Leaflet 這個套件所提供,但是被筆者定義的子介面簡化了,這些子介面包含:MapInitializerMapMarkerLayerMapMarker

而我們可以將這些子介面與 Leaflet 提供的功能進行整合,創造出一個外觀(Façade)供客戶端程式碼使用,在者裡的客戶端程式碼就是指 UBike 地圖系列範例裡的 index.ts 內部的內容。

所以筆者想要再定義一個專屬於 UBikeMapFacade 這個類別,專門負責渲染 UBike 站點在地圖上。而 UBikeMapFacade 就只有兩個成員,非常簡單:

  • pinStops 代表將 UBike 站點都渲染到地圖上
  • clearStops 代表將渲染過後的 UBike 站點從地圖上清除

這是筆者辛辛苦苦地將 UBike 地圖的應用,以外觀模式的方式呈現的類別、介面與套件的關係圖。(如圖一)

https://ithelp.ithome.com.tw/upload/images/20191008/201206149gU0HFKboW.png
圖一:外觀模式下,UBikeMapFacade 就是一個很單純的介面,整合前面四大主角以及 LeafletJS 的功能

以下的程式碼就是實踐 UBikeMapFacade 的內容,放置在 /src/MapFacade.ts 檔案裡。

https://ithelp.ithome.com.tw/upload/images/20191008/201206145Ffml40GNI.png

看起來是很長串程式碼,但仔細看事實上很單純 —— 引用剛剛宣告過的子類別介面,然後操作這些介面達到將 UBike 點位渲染到地圖上的過程。

這樣的好處是 —— 剛剛宣告過跟 CustomMap 相關的類別內部的功能都是專注在地圖上的運作,而非 UBike 點位的渲染過程,而 UBikeMapFacade 可以專心在操作子類別介面,渲染跟 UBike 點位相關的邏輯,因此不需要再重複處理地圖渲染的過程與邏輯:達到類別各司其職,單一職責原則的實現。

另外,UBikeMapFacade 的建構子裡面還需要提供所謂的 tooltipTemplate 這個方法成員,因為要讓客戶端可以自訂 Tooltip 要呈現的內容為何,而這個方法為一種回呼函式,可以接收到要渲染的 UBike 站點的點位資訊 —— 也就是前幾篇有宣告過的 UBikeInfo 型別。

使用外觀類別 Put Façade in Use

在原來的 index.ts 檔案裡,你可以將地圖的初始化過程簡化為對 UBikeMapFacade 的初始化過程喔!

https://ithelp.ithome.com.tw/upload/images/20191008/20120614qTqfIGTvgr.png

再來是,將資料取出的過程中,本來過濾資料後要開始一連串對地圖的渲染操作,都被 UBikeMapFacade 簡化為只要填入 UBikeInfo[] 型別就可以在地圖裡面進行渲染的動作。

https://ithelp.ithome.com.tw/upload/images/20191008/20120614MIDWmpOdCG.png

省去的程式碼有點多,讀者看到這裡應該也會感到愉悅,好多地圖相關的操作邏輯都被處理掉。

最後就只是把地圖清除點位的邏輯換成是用 UBikeMapFacade 裡提供的 clearStops 成員方法進行站點的清除。

https://ithelp.ithome.com.tw/upload/images/20191008/20120614W18xLGN8DG.png

以下是 index.ts 整頓過後的結果。

https://ithelp.ithome.com.tw/upload/images/20191008/20120614kYcJM9nNqD.png

筆者另外要提醒一下:

外觀模式最大的優點是:它可以隱藏使用的內部使用的套件與機制

仔細觀看上面的程式碼,你不會看到 leaflet 套件出現的蹤跡,你只會看到地圖相關的東西只有設定 MapConfig 以及筆者定義的 UBikeMapFacade

你甚至也沒看到 InitializerMarker 等等子介面,外觀模式在客戶端的程式碼就是那麼的直觀,它是一種高度抽象化的結果 —— 你不會看到實作細節,你只要處理好主程式的行為就夠了

本篇唯一重點. 外觀模式 Façade Pattern

外觀模式著重在於對於複雜系統的一層包裝,使得客戶端可以忽略內部實作細節,專注於高度抽象化層級的行為描述

外觀模式不僅僅是用在包裝複雜系統功能這一用途,也可以選擇將內部的複雜系統進行一層層漸進式抽象化宣告一系列子介面,然後再慢慢收斂到一個外觀類別(Façade)的實踐

外觀模式最大的優點在於,它可以隱藏內部的實作細節跟引用的套件,客戶端也就不會有誤用內部功能而造成程式運行過程可能發生的侵入式破壞導致的錯誤

事實上,外觀模式非常常見,比如:

  • 一個語言的編譯器介面,實際上內部是一連串針對語言的編譯過程,包含:Lexer(語法解析)、Parser(敘述式或表達式的解析)、Code Generator(機械碼的產出)等等,但使用者並不會看到內部的實作長什麼樣子,而是依靠編譯器提供的外來介面進行編譯的動作 —— 編譯器就是一個 Façade
  • CLI(Command-Line Interface)等工具就是一種 Façade
  • 套件的實作提供的 API 就是一種 Façade
  • RESTful API 可以看作是連接資料庫、處理 Request 等等功能,整合出來的外觀,故也可看作是一種 Façade

讀者試試看

由於篇幅關係,筆者想說這個可以讓讀者練習。

讀者可以試著利用 UBikeMapFacade,能不能實踐出這個功能 —— 當使用者選擇不同的行政區,就可以將地圖自動對焦到該行政區。

你可能會需要筆者之前整理的 districtsLatLngMap 以及 L.Map 提供的 flyto 方法。並且可以選擇宣告新的介面,叫做 CustomMap.MapUtility 之類的介面並實踐出子類別後,在 UBikeMapFacade 呼叫該子介面方法讓地圖可以隨時轉換焦點。

小結

今天開開心心地總算可以把這個範例結束掉~

筆者也在此宣布 —— 總算把第三篇章《戰線擴張》解決了~(放鞭炮

第四篇章《通用武裝》篇章應該會比第三篇更精彩,應用不僅僅只是泛用型別而已,筆者還要介紹 ES6 Promise 以及更多東西的應用喔~~~

]]> Maxwell Alexius 2019-10-21 17:11:19
Day 40. 戰線擴張・模擬戰 - UBike 地圖 X 使用 LeafletJS - Using LeafletJS with TypeScript https://ithelp.ithome.com.tw/articles/10225073?sc=rss.iron https://ithelp.ithome.com.tw/articles/10225073?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

  1. 你會如何善用型別推論與註記的機制呢?
  2. 什麼情形可能會出現 any 型別推論出來的行為?如果出現了,要如何處理這類型的案例?

另外,本篇文承接上一篇文,因此如果是跳到這篇的話可以先從上一篇看起喔~

貼心小提示

想要本範例的程式碼 —— 可以參見 Maxwell-Alexius/Iron-Man-Competition 這個 Repo 喔!

以下正文開始

模擬戰 — UBike 地圖應用(三)

今天主要是先把功能給完成,但不代表今天就會結束掉本篇章系列,最後一篇會進行程式碼重構的動作 —— 不過在重構之前,沒有一個完整的功能前,基本上要談到重構的議題,也是沒辦法的。

貼心小提示

另外有一個名詞叫做 Premature Optimization —— 過早優化。儘管對一些比較資深的讀者,可能已經早就聽過它了,不過初入軟體設計的讀者可能還沒聽過,在此就補充一下。(筆者本身也是沒多少經驗

軟體設計比較麻煩的地方應該是 —— 判斷程式是否該優化或者是重構的時間點,過早地將程式碼進行優化的動作反而會導致程式設計上的彈性被緊縮掉。

在還未將功能設計好的前提下,重構程式碼可能會導致功能還沒實踐完畢,就要面對種種設計上的限制,導致破壞掉原來重構出的架構的機率上升。另一種情形可能是在一開始設計軟體的規格時,可能沒有想到後續會出現什麼樣種類的問題等等,也就造成整個軟體的規格又必須改掉,於是有重構沒重構反而沒差,導致浪費時間。

讀者有空可以搜搜看並理解這句話的含義 —— “Premature optimization is the root of all evil.” by Donald Knuth

快速前情提要

上一篇筆者已經把資料處理部分結束:

  1. 宣告 SourceUBikeInfo 這個型別對應 UBike 源頭資料的格式
  2. 宣告 UBikeInfo 為筆者在這個應用程式裡需要用到的格式
  3. 宣告 Districts 這個複合型別將所有台北市行政區的名稱字串 union 起來
  4. 將所有的型別整理到 ./src/data.d.ts 這個檔案
  5. 由以上的型別導出了 fetchData 這個函式 —— 負責從台北市政府開放資料中的 UBike 即時資料系統抓出資料

重述規格

筆者在 UBike 地圖想要實踐的是:

  1. 選擇台北市行政區
  2. 當區域被選擇了,就會更新地圖
  3. 地圖會出現該區域的 UBike 點位
  4. 每個點位如果被滑鼠滑過去,會出現 Tooltip 顯示 Ubike 點位相關的資料,包含:所在行政區以及該站點名稱、現在還有的 UBike 數量以及總 UBike 數量

實踐選擇行政區的 UI

由於筆者想要動態將選項從 districts(行政區的陣列,為 Districts[] 型別)轉換成 HTML 表單相關的元素,必定會需要用到 document.createElement 相關的功能。

貼心小提示

相信會看本系列的讀者們(除非你是真的只有接觸 NodeJS 而沒前端經驗),普遍來說都應該熟悉原生 JS 操作 DOM 的流程。因此真的不清楚 DOM 的操作基礎可以上網查查看 DOM Manipulation 相關的資源啊!

首先,筆者更新一下 index.html 的內容 —— 使用 CSS Absolute Position 將選擇行政區域的 UI 放置在網頁的右上方,並且使用 <select><option> 這兩個元素做為行政區表單選項。

https://ithelp.ithome.com.tw/upload/images/20191006/20120614ctOi5xSjQ2.png

這是目前瀏覽器應該會出現的畫面,筆者刻意將顯示的倍率放大。(如圖一)

https://ithelp.ithome.com.tw/upload/images/20191006/20120614mLJtjFghia.png

以下筆者就簡單地在 index.ts 裡,利用 districts 將所有的行政區選項轉換成 DOM。另外,如果讀者正在跟本篇文章的內容,記得要把 index.html 裡的 <option name="信義區">信義區</option> 砍掉,因為我們要改採動態方式新增 <option> 標籤喔!

https://ithelp.ithome.com.tw/upload/images/20191006/20120614Bznp7Ap8Oi.png

這裡也會出現 TypeScript 的警告訊息喔!(如圖二)

https://ithelp.ithome.com.tw/upload/images/20191006/20120614V7sMjGmLXl.png
圖二:$selectDistrict 可能為 null

ㄧ樣,我們可以使用 Type Guard 解決這個問題:

https://ithelp.ithome.com.tw/upload/images/20191006/20120614LOS8IkEqju.png

我們的問題就解決掉了,TypeScript 判斷我們的 $selectDistrict 這個變數存取的確定是 HTMLElement 型別。(如圖三)

https://ithelp.ithome.com.tw/upload/images/20191006/20120614LlQQZG5KCn.png
圖三:沒有任何錯誤訊息,經過型別檢測就限縮掉 $selectDistrict 之型別為 HTMLElement

使用 LeafletJS 畫出 UBike 點位

筆者在模擬戰 — UBike 地圖的第一篇提到,如果臨時對於 LeafletJS 的功能有疑惑的話,除了可以參考官網的 Doc 外,還有另一招就是翻閱 Leaflet 套件的 Declaration File 喔

技巧到現在還不清楚的讀者,強烈建議看前幾篇文章(已經被 Reference 至少三次囉),後續筆者不會再重複講

首先,由於會需要在不同的行政區畫一連串的 UBike 點位,因此筆者習慣會將所有產生的點位 —— 也就是 Leaflet 的 Markers 匯集在一起,這時候會需要一個變數 —— 儲存聚集所有點位的 LayerGroup

於是筆者先定義一個變數 markerLayer 並且積極註記為 LayerGroup 這個屬於 LeafletJS 提供的型別。這裡使用遲滯性指派(Delayed Initialization) —— 因此一定要積極註記避免 any 型別的出現

https://ithelp.ithome.com.tw/upload/images/20191006/20120614Z6AyoI6pkn.png

接下來,就是實踐主要的功能。

貼心小提示

這邊的實作自由性很高,讀者當然也可以選擇不要使用筆者的做法達到相同的功能,這裡舉一個例子:實踐產生一個 Marker 的函式,並且在外面就可以用 for 迴圈對 UBike 點位進行迭代、匯集、然後再轉換為 LayerGroup 也是可以的。

以下筆者直接使用 fetchData(記得要從 fetchData.ts 載入進來)開始寫下轉換點位到 Leaflet Marker 的動作:

https://ithelp.ithome.com.tw/upload/images/20191006/20120614hSj2psmgSb.png

不過,我們必須先將行政區名稱從 <select> 元素取出來 —— 通常是使用 <select> 這個 DOM 的 value 屬性:

https://ithelp.ithome.com.tw/upload/images/20191006/20120614OjpkymHDws.png

但這會出現一個問題:HTMLElement 並沒有 value 屬性。(如圖四)

https://ithelp.ithome.com.tw/upload/images/20191006/20120614ISByDlZqLt.png
圖四:HTMLElement 型別並沒有 value 屬性

這裡就要考驗讀者對於 DOM 本身的認識 —— 事實上並不是 HTMLElement 提供 value 這個屬性,而是 <select> 元素的 HTMLSelectElement 型別才會提供 value 這個屬性。

也就是說,之前在宣告 $selectDistrict 這個變數時,更好的做法是 —— 積極註記為 HTMLSelectElement

但是這裡要注意一點:$selectDistrict 原本的型別推論結果為 HTMLElement | null 代表 TypeScript 認為這裡有潛在 Bug 提醒開發者說:“你可能並沒有真正地選擇到元素,getElementById 裡的 ID 可能打錯字所以才會沒選到”。

因此,我們不僅僅要把 $selectDistrict 註記為 HTMLSelectElement 型別,另外還要將它和 null 進行複合,這應該才是正確流程,而不是使用 HTMLSelectElement 進行型別壟斷(此為筆者自創名詞,Type Monopolization)的動作:

https://ithelp.ithome.com.tw/upload/images/20191006/20120614RQ0JFEpTxB.png

因此,編輯器也沒有出現警告訊息了。(如圖五)

https://ithelp.ithome.com.tw/upload/images/20191006/20120614hRCS2HSMc6.png

重點 1. 型別註記 V.S. 型別壟斷 Type Annotation V.S. Type Monopolization

型別壟斷為筆者自創名稱 —— 目的是描述型別註記誤用的一種情形,絕對不是官方或其他文章會出現的觀念。

通常,一個型別如果出現了複合的情形,又以 union 為主,如果進行型別註記時 —— 除非你可以抱持 100% 信心可以對型別進行積極註記而取代使用 Type Guard 進行型別限縮,否則貿然地對型別積極註記為複合型別中的其中一種情形而忽略掉其他狀況,此現象被筆者稱之為型別壟斷(Type Monopolization)—— 為強烈建議禁止的行為。

以下以變數 A 為範例,若 A 被 TypeScript 判定,型別推論為 T | U

https://ithelp.ithome.com.tw/upload/images/20191006/20120614Gwp8prouup.png

A 被強行註記為 T 或者是 U而非經過 Type Guard 等安全性處理,此時就犯了型別壟斷的行為。

回過頭來,筆者繼續實作 fetchData 以後,將 UBike 資料轉換成 Leaflet Marker 的程式碼內容。

首先我們必須根據目前選到的行政區對 UBike 資料進行過濾的動作 —— 使用 Array.prototype.filter 這個方法。

https://ithelp.ithome.com.tw/upload/images/20191006/20120614EIc4jj5oZf.png

筆者再次強調 —— 儘管讀者沒有看到以上的程式碼做任何型別註記,但筆者之前就有探討過某些情形是不需要對函式的參數進行註記的,而這些機制也會和泛用型別的機制有關,如果忘記的讀者請記得要好好看一下本系列函式型別篇章以及函式與陣列的相關篇章

filter 裡面的回呼函式的 info 參數會被自動推論為 UBikeInfo,所以並不是說筆者沒有應用到 TypeScript 的型別系統,而是 TypeScript 的型別系統在為筆者工作,確保筆者 100% 不會寫出會出現潛在 Bug 的程式碼。(如圖六)

https://ithelp.ithome.com.tw/upload/images/20191006/20120614zIb2M6khyQ.png
圖六:就算參數 info 沒有被註記,此時的型別推論會自動鎖定 UBikeInfo 型別

第二個步驟就是將過濾出的點位轉換成 Leaflet Marker,這一次筆者使用 Array.prototype.map 方法。

https://ithelp.ithome.com.tw/upload/images/20191006/20120614NpNpaSVEMz.png

這裡提醒一下,如果臨時忘記套件提供的 API 的功能,記得 TypeScript 提供給你的優勢,也就是型別系統;而 TypeScript 會根據 Leaflet 套件的 Declaration File 告訴你 new L.Marker 裡面的參數該填入什麼。(如圖七)

https://ithelp.ithome.com.tw/upload/images/20191006/20120614KyJnYlLodj.png
圖七:new L.Marker 裡面的細節

L.Marker 顯示的內容為:

constructor Marker<any>(
  latlng: L.LatLngExpression,
  options?: L.MarkerOptions | undefined
): L.Marker<any>

代表它除了是 Marker 的建構子函式外,第一個參數必須為 L.LatLngExpression,正好是筆者的範例程式碼 UBikeInfolatlng 對應的型別;第二個參數為 L.MarkerOptions,如果讀者好奇或者臨時忘記有哪些參數可以帶進去,可以用筆者教過的方式,直接查找 Declaration File。(圖八為 L.Marker 的定義;圖九為 L.Marker 第二個參數 —— MarkerOptions 的定義)

https://ithelp.ithome.com.tw/upload/images/20191006/20120614DCHAyFusnz.png
圖八:藉由筆者之前教過的技巧,可以在 VSCode 裡從 L.Marker 找到原本的定義

https://ithelp.ithome.com.tw/upload/images/20191006/20120614EMcggRIxDg.png
圖九:此為 MarkerOptions 的介面定義

如果你會這個技巧,就可以節省時間不用上網查 Doc,直接在編輯器裡就可以查到 Declaration 以及該套件的 API 規格。

除了將 UBikeInfo 轉換成 Marker 外,還需要對 Marker 新增 Tooltip 顯示 UBike 站點的資料與自行車借用狀態。

貼心小提示

Tooltip 是一種提示性視窗,通常就是滑鼠滑到某個功能時,會跳出來的視窗。

https://ithelp.ithome.com.tw/upload/images/20191006/20120614laxS7aqWst.png

marker.bindTooltip 以及一些細節就留給讀者去探索,跟剛剛筆者在解說 new L.Marker 的過程差不多。

以上的程式碼除了對每一個 Marker 新增 Tooltip 外,Leaflet Marker 還提供事件註冊的 API —— 因此筆者註冊兩個分別為 mouseovermouseleave 事件,代表顯示或關閉 Tooltip。

最後,將 Marker 使用 L.layerGroup 包在一起後丟進一開始有宣告過的 markerLayer 變數(為 LayerGroup 型別),並且將其加到地圖裡。

https://ithelp.ithome.com.tw/upload/images/20191006/20120614GkzsRxipL4.png

這樣子,我們就完成基本的程序囉!打開瀏覽器來看,出現預設的區域 中正區 以及該區的 UBike 站點,甚至還可以用滑鼠檢視該地區的站點資訊喔!(如圖十)

https://i.imgur.com/s2pUPlT.gif
圖十:UBike 地圖站點的結果

監聽行政區欄位的事件

儘管已經做好了初步的準備,但筆者還沒有將 $selectDistrict 這個 <select> 元素註冊事件 —— 如果行政區被改變的話,應該要更新地圖的。

首先,由於建立 Leaflet Marker 的程式碼要被重複使用,因此筆者先把它包成函式並先呼叫一次,為的是要初始化地圖的狀態:

https://ithelp.ithome.com.tw/upload/images/20191006/20120614b7ZCF91Ehv.png

由於 updateUBikeMap 參數需要為 Districts 型別,因此除了要記得將 Districtsdata.d.ts 載入進去外,currentDistrict 記得要註記為 Districts

另外,將 $selectDistrict 註冊一個 change 事件並嘗試更新地圖資訊:

https://ithelp.ithome.com.tw/upload/images/20191006/201206148T8GRyphTp.png

以上的程式碼完成了!打開瀏覽器測試結果如圖十一。

https://i.imgur.com/VcMV68e.gif
圖十一:切換行政區時,可以更新整體的地圖狀況

小結

本來筆者還想要多寫一個功能,就是當使用者選擇行政區時,要藉由上一篇宣告過的 districtLatLngMap 取的經緯度後,再讓地圖聚焦到那個座標,這樣子也會比較 User Friendly —— 不過筆者認為,讀者可以先試試看如何實踐這個功能。(提示:map.flyTo 這個方法)

下一篇筆者就先切入程式碼重構部分,敬請期待~

]]> Maxwell Alexius 2019-10-20 15:39:34
Day 39. 戰線擴張・模擬戰 - UBike 地圖 X 資料處理 - Data Processing using Type Alias https://ithelp.ithome.com.tw/articles/10224986?sc=rss.iron https://ithelp.ithome.com.tw/articles/10224986?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

是否會使用 Webpack 建立 TypeScript 專案的環境呢?

另外,本篇文承接上一篇文,因此如果是跳到這篇的話可以先從上一篇看起喔~

貼心小提示

想要本範例的程式碼 —— 可以參見 Maxwell-Alexius/Iron-Man-Competition 這個 Repo 喔!

直接進入正文開始

模擬戰 — UBike 地圖應用(二)

在進入 LeafletJS 的使用之前,上一篇已經將簡單的地圖給實踐出來了,筆者快速把上一篇的主程式碼帶過。

https://ithelp.ithome.com.tw/upload/images/20191005/20120614D7XHxY9Cbu.png

其中,筆者已經將 mapConfig 的內容與型別註記整理到 map.config.ts 這個檔案,這樣子主程式不會塞太多型別註記的東西外,如果之後要做調整,就可以直接更改 map.config.ts 裡面的值。

https://ithelp.ithome.com.tw/upload/images/20191005/20120614AESAAjHpnS.png

快速看過後我們趕快進到下一關!

資料處理 —— 善用型別化名 Data Processing with Type Alias

通常遇到要視覺化的應用,一定會經過痛苦的資料處理過程。

題外話,這裡附上筆者資料視覺化的作品集,但對筆者而言還不算完整,因為筆者個人目標是把所有的 Chart 都畫出來 XD,但也要看筆者個人時間上的安排。(糟糕偏題了)

今天的目標就是要把台北市的 UBike 資料簡化到我們需要的格式 —— 當然,原本的資料名稱實在是很難看!(如圖一)

https://ithelp.ithome.com.tw/upload/images/20191005/201206140YgJuB2XEJ.png
圖一:台北市政府的開放資料,對於 UBike 的自行車即時資料的描述內容

光是看欄位名稱:sbi 代表目前的自行車數量、sarea 為行政區域等等,實在是太抽象。今天筆者來 Run 一次自己認為如何用 TypeScript 進行資料處理。

先把請求管道建立起來 Data Request

當然,第一步驟就是確保我們能夠接收到資料,以下筆者新增 fetchData.ts/src 資料夾內,裡面內容為:

https://ithelp.ithome.com.tw/upload/images/20191005/201206141OKdMPRhhg.png

首先 URL 自然而然是台北市開放 UBike 資料的接口連結,另外 fetchUBikeData 的函式參數,筆者建議不要寫死,因為有可能請求的來源不同。

以下是在 index.ts 裡面引入該函式後,並且進行測試時,檢視資料的狀況。

https://ithelp.ithome.com.tw/upload/images/20191005/2012061468jqYMNARF.png

敏銳的讀者如果讀過編譯器設定篇章 —— 由於我們使用 ES6 Promise 這個物件 —— 此為功能層面(Utility Aspect)而非語法層面的東西,我們必須要在 tsconfig.json 裡的 "lib" 選項改成:

{
  "compilerOptions": {
    /* 略... */
    "lib": ["dom", "es2015"]
    /* 略... */
  }
}

方才可使用 Promise 這些 ES6 以後的功能。

貼心小提示

Promise 物件的詳細推論機制,由於牽扯到 Generics 泛用型別機制,因此會在第四篇章講到喔!

儲存之後打開瀏覽器可以發現我們成功地將資料接出來了。(如圖二)

https://ithelp.ithome.com.tw/upload/images/20191005/20120614kiyKjAEPkP.png
圖二:至少我們先確定資料可以出來了

善用 TypeScript 型別系統並且練習寫 Doc

通常筆者習慣遇到這種亂糟糟的資料,一定會把來源資料的格式,以文件的方式寫出來(省去日後還要上網查詢的時間)。

所以筆者在 fetchData.ts 裡,按照台北市的 UBike 即時資料格式 —— 運用型別化名(Type Alias)寫下來,將它化名成 SourceUBikeInfo

https://ithelp.ithome.com.tw/upload/images/20191005/20120614VsCwVeWUcf.png

讀者可能認為以上的格式有寫沒寫都沒差,因為輸出的值全部都是 string 型別 —— 但是筆者建議儘量敘述源頭資料的狀況 —— 筆者這裡告訴 TypeScript 在資料還未處理前,全部的資料都是 string 型別,圖二的開發者工具中的 Console 裡面的內容確實全部都是對應到 string 型別。

第二個步驟就是定義我們的 App 想要轉換成的資料格式,筆者於是再次宣告另一種理想的格式型別化名 UBikeInfo 如下:

https://ithelp.ithome.com.tw/upload/images/20191005/20120614wNTHnOCLkf.png

第二種 UBikeInfo 是我們希望輸出的型別:

  • availableBikes 必須從 SourceUBikeInfo 裡的 sbi 欄位取用,並且將 string 轉成 number
  • totalBikes 必須從 SourceUBikeInfo 裡的 tot 欄位取用,並且將 string 轉成 number
  • latLng 使用 leaflet 內建的 LatLngExpression 元組型別,必須從 SourceUBikeInfolatlng 兩個值 —— 除了轉換成 number 型別外,還要組織成 [number, number] 格式
  • regionName 則是對應 SourceUBikeInfo 裡的 sarea 欄位,不需進行型別轉換
  • stopName 則是對應 SourceUBikeInfo 裡的 sna 欄位,也不需進行型別轉換

於是筆者修改 fetchUBikeData 這個函式的內容:

https://ithelp.ithome.com.tw/upload/images/20191005/20120614IK37dgHem8.png

看起來很複雜,筆者分兩段講。

首先,第一個部分:由於本來台北市 UBike 資料長得很奇怪,是 { retVal: [...], retCode: number } 格式,而我們只需要 retVal 這個屬性。

此外,retVal 裡面的值是這種格式:

{
  0001: SourceUBikeInfo,
  0002: SourceUBikeInfo,
  /* 以下略... */
}

筆者希望轉換成 SourceUBikeInfo[] 這種陣列,於是需要使用 Object.keys 結合 map 方法產生成 SourceUBikeInfo[] 的陣列形式。

https://ithelp.ithome.com.tw/upload/images/20191005/20120614d26l9bU9NX.png

另外,由以上的程式碼,筆者刻意註記為 retVal[key] as SourceUBikeInfo,理由是 —— 還記得之前筆者強調過:出現 any 型別的情形,其中就以 I/O 相關的機制常見,而 TypeScript 哪會知道你 fetch 到的外部資料格式為何,它當然會自動推論為 any 型別

因此這並不是在 TypeScript 專案裡樂見的行為。

我們必須在資料轉換的過程中,開始進行型別的積極註記動作喔

第二步驟:開始進行 SourceUBikeInfoUBikeInfo 的資料格式轉換。

https://ithelp.ithome.com.tw/upload/images/20191005/201206142McRA3b8Xd.png

讀者可能覺得這段程式碼好亂,但是這是資料格式轉換必經的過程 —— 不過這裡可以藉由型別的積極註記為 UBikeInfo 的話,不管過程再亂,我們只需要關注資料被轉換的結果有沒有被 TypeScript 監測

另外,多虧函式的推論機制,我們的 fetchUBikeData 函式的輸出推論結果自動被變成 Promise<UBikeInfo[]>。(如圖三)

https://ithelp.ithome.com.tw/upload/images/20191005/20120614ZMw6Nmri5s.png
圖三:推論為 Promise<UBikeInfo[]>

有些看到這裡的讀者冒出問號:“恩恩恩!?什麼是 Promise<UBikeInfo[]>?難道不是 UBikeInfo[] 而已嗎?”

事實上這是所謂的泛用型別(Generic Type)—— 這會在第四篇章《通用武裝》被筆者討論到

筆者這邊不負責任先提示:Promise<UBikeInfo[]> 代表的意思是,只要使用 fetchUBikeData 輸出的 Promise 物件,並且使用 then 方法:

fetchUBikeData()
  .then((A) => ...)

參數 A 之型別會被自動推論為 UBikeInfo[]

貼心小提示

這裡讀者若不知道 ES6 Promise 物件可以暫時看一下社群上的資源。

由於本篇討論的是應用,而非 Promise 物件的主題,所以並不會在本篇介紹過多跟 Promise 甚至是泛用型別相關的東西。

然而,Promise 物件將會在第四篇章《通用武裝》篇章介紹,目的是要讓讀者知道 Generic Types 的威力以及常見性 —— 儘管泛用型別是進階主題,但是這個東西會比讀者想像中還要常遇到。

所以我們可以確定,資料轉換過後的型別註記的部分也在 fetchData.ts 全部處理完畢。

因此在 index.ts 裡:

https://ithelp.ithome.com.tw/upload/images/20191005/20120614Dvy31tQqxy.png

你可以發現我們的 data 參數被推論結果為 UBikeInfo[],如圖四。

https://ithelp.ithome.com.tw/upload/images/20191005/20120614D60pQN9lIr.png
圖四:data 自動被推論為 UBikeInfo[]

這樣的好處就是,如果我們隨便從該陣列資料拉出一個值,VSCode 就會自動幫我們偵測可以使用的屬性呢!(如圖五)

https://ithelp.ithome.com.tw/upload/images/20191005/201206143ko1wLIIB4.png
圖五:VSCode 自動幫我們判斷我們可以使用的屬性

除了利用型別系統的註記機制外,然後在主程式能夠不靠註記而只靠型別推論就得知可以使用的功能 —— 這就是筆者想要從 TypeScript 開發過程中想要達到的效率。

打開瀏覽器,我們的資料已經被轉換成好讀的格式。(圖六)

https://ithelp.ithome.com.tw/upload/images/20191005/201206142Us3BGj98R.png
圖六:格式變得美美的~

台北市行政區資料與座標轉換

另外,筆者認為我們還需要把所有台北市的行政區資料提供出來 —— 就稱之為 districtData.ts,並且將其放置在 /src 這個檔案資料夾位置。以下是 ./src/districtData.ts 的內容:

https://ithelp.ithome.com.tw/upload/images/20191005/20120614vzCWGMzZYm.png

第一個 districts 很簡單,就是一連串的所有行政區。

第二個就比較麻煩一些,但也不會麻煩到哪裡去。districtLatLngMap 使用的是 ES6 Map 這個資料結構 —— 筆者依然在這裡使用類似 ES6 Promise 的型別格式,也是使用泛用型別。

但 Map 可以接受的泛用型別有兩個 —— 第一個代表鍵(key)、第二個則是值(value),它可以這樣被使用:

https://ithelp.ithome.com.tw/upload/images/20191005/20120614uYAmhRJyZb.png

貼心小提示

同理,ES6 Map 的推論註記行為會在第四篇章討論到!

不過呢,更精確的型別註記應該要先宣告 Districts 這個型別化名為所有行政區的名稱的 union

https://ithelp.ithome.com.tw/upload/images/20191005/20120614iHgNyZg2SZ.png

貼心小提示

當然,如果覺得這樣很麻煩,讀者也覺得沒有必要將 Districts 寫得這麼制式化,可以選擇退化為 string 型別也 OK。

然後將 districtsdistrictLatLngMap 改成:

https://ithelp.ithome.com.tw/upload/images/20191005/20120614Eo1zQcKqv4.png

另外,因為我們將台北市行政區所有可能的值設定為 Districts 這個型別,這也代表剛剛的 UBikeInfo 裡的 regionName 可能也必須從 string 改成 Disticts

可是這樣又要再把 Districts 這個型別額外建立一個檔案,然後再把該化名載入到 fetchData.ts 檔案,不如乾脆我們把所有的型別定義都匯聚在一個宣告檔(Declaration File)好了。

於是筆者在 /src 裡面額外新增 data.d.ts 這個檔案 —— 專門宣告型別的定義。以下是 data.d.ts 的內容:

https://ithelp.ithome.com.tw/upload/images/20191005/20120614eK9bVlKRej.png

於是你可以將 DistrictsSourceUBikeInfoUBikeInfo 的型別宣告從 fetchData.tsdistrictData.ts 拔除,並且從 data.d.ts 載入進去。

不過,讀者也可以選擇不要用 data.d.ts 檔案,而是普通的 data.ts 檔案,但改成 export type ... 的方式將型別化名(Type Alias)輸出出去也是可以的

以下是目前 fetchData.ts 的程式碼大致上的狀況。

https://ithelp.ithome.com.tw/upload/images/20191005/20120614KrqKJoon05.png

以下是目前 districtData.ts 的程式碼大致上的狀況。

https://ithelp.ithome.com.tw/upload/images/20191005/201206146QADWEgZ1C.png

這樣子我們就可以把型別化名宣告部分整理到其他的檔案,若讀者想要查詢 UBikeInfo 等等實際上的內容,可以ㄧ樣按照筆者教過的技巧:

  1. 滑鼠滑到 UBikeInfo
  2. 按下 Command(Mac 系統)或 Ctrl(Windows 系統)
  3. 滑鼠點下去

就會立馬導到你所定義型別化名的地方喔。

最後還是在提醒一下:想要參考完整的程式碼,可以點擊這邊

小結

今天主要把資料處理的動作都交代清楚了,讀者應該可以很輕易地體會到 TypeScript 型別化名的好處,協助我們確保型別不會弄錯外,我們還可以藉由型別系統寫出 Documentation —— 創造出屬於專案的 Declaration Files 呢!

下一篇我們繼續本案例,將進度推展下去~!

]]> Maxwell Alexius 2019-10-19 18:16:56
Day 38. 戰線擴張・模擬戰 - UBike 地圖 X Webpack 環境建構 - TypeScript Webpack Integration https://ithelp.ithome.com.tw/articles/10224782?sc=rss.iron https://ithelp.ithome.com.tw/articles/10224782?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

  1. 什麼是宣告檔 Declaration Files?為何宣告檔很重要?
  2. 如何載入第三方套件在 TypeScript 專案裡?
  3. 如何執行 TypeScript 在 AMD 模式下的編譯成果?

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

今天筆者又要講讀者應該會更常用的部分 —— 使用 Webpack 結合 TypeScript。

讀者可能覺得:“作者真是莫名其妙,為何一開始就不講這個?還要花很多篇時間講 Namespaces 或單純打包檔案等等東西。”

筆者之所以這麼做的原因是,要先讓讀者打好一些基礎 —— 像是宣告檔 Declaration Files 的目的與由來、自己如何建立簡單的 TypeScript 環境等等,畢竟在進行簡單的語法測試或學習時,你不太需要那麼大型的環境。

另外,筆者直接進入 Webpack 單元講也是可以,只不過讀者提前知道宣告檔的存在 —— 並且有能力善用工具 —— 可以獨立查詢套件的型別宣告規格的話,要開始跟一些專案應用層面的內容會比想像中簡單喔!

本篇文章就進入正文開始吧~

模擬戰 — UBike 地圖應用(一)

建構 Webpack 結合 TypeScript 環境

這應該是大部分讀者想要知道的東西,筆者也總算可以從前幾篇很痛苦的設定來設定去的泥淖中,準備從這一篇解脫。

筆者稍微簡介一下 Webpack,避免有些讀者可能還是對 Webpack 這東西有些疑惑。(圖一為官方網站對於 Webpack 的示意圖)

https://ithelp.ithome.com.tw/upload/images/20191005/20120614sE1KTpxW6Q.png
圖一:解釋 Webpack 最簡單的架構圖

可以從圖一得知,事實上 Webpack 不僅僅只有打包純 JavaScript 相關的檔案而已,它連任何靜態資源,諸如:CSS、Sass、Image 等等相關的檔案都可以打包在一起。

而打包專案一切的方式就是用所謂的 Loader 進行,比如說:想要將 CSS 檔案打包進去,就會有所謂的 css-loader;想要編譯 Sass 相關的檔案,就會有 sass-loader 之類的東西 —— 而如果今天要將 TypeScript 的檔案編譯並且打包進去,官方就有出所謂的 ts-loader

因此,筆者今天就先示範如何建構單純的 Webpack 結合 TypeScript 的環境吧!當然,讀者可以點這裡檢視 Webpack 官方網站以及參考如何將 TypeScript 結合到 Webpack 的相關設定喔!

首先,在任何想要建構環境的資料夾中下一系列的指令:

// 到你想要建構環境的資料夾內
$ cd PATH_TO_DIR

// 初始化 package.json
$ npm init -y

// 初始化 tsconfig.json
$ tsc --init

下載 Webpack 相關套件

首先,筆者下載兩個跟 webpack 相關的套件:

$ npm install webpack webpack-cli --save-dev

另外,因為我們需要讓 TypeScript 與專案結合,必須要重新下載 typescript 與下載打包時需要的 Loader —— 也就是 ts-loader

$ npm install typescript ts-loader --save-dev

讀者可能覺得:“為何要重新下載 typescript 模組?這東西不是早在本系列第一天就已經下載過了,而且是用 npm 模組的 global 模式下載的?”

事實上 Webpack 認得的 TypeScript 編譯器並不會從 Global 的 NPM 模組參照,而是在專案內部參照。因此每一次建構 Webpack 結合 TypeScript 的環境時,必須額外下載 typescript 在專案內部

專案設定

習慣上,開發時我們會將主程式都放在名為 /src 的資料夾;打包專案時則會將結果輸出到 /build/dist 的資料夾(又或者是看讀者有什麼習慣)。

以下分別新增兩種專案資料夾,並且額外新增 index.ts/src 資料夾裡,代表主程式撰寫的地方喔~

// 建構 /src 與 /dist 這兩種不同的檔案資料夾
$ mkdir src
$ mkdir dist

// 新增 ./src/index.ts 檔案
$ touch ./src/index.ts

index.ts 裡隨便寫一行 console.log

https://ithelp.ithome.com.tw/upload/images/20191005/20120614YipM7G1OQq.png

另外,我們也需要設定 TypeScript 編譯器的檔案:

{
  "compilerOptions": {
    /* 略... */
    "target": "es5",
    "module": "es6",
    "outDir": "./dist/",
    "rootDir": "./src/",
    "strict": "true",
    "noImplicitAny": true,
    "strictNullChecks": true
    /* 略... */
  }
}

以上是筆者設定過的東西,其他如果有一開始預設的設定被啟動可以放著。

另外,建立 webpack.config.js 並且填入以下內容:

https://ithelp.ithome.com.tw/upload/images/20191005/20120614RHMq4jogGl.png

以下筆者稍微解釋 webpack 的設定檔到底寫了些什麼:

  • entry 部分代表的是 Webpack 要打包的檔案輸入位置,也就是 ./src/index.ts
  • module 裡面通常都是放置 Loader 相關設定 —— 其中 TypeScript 相關檔案都會經由 ts-loader 進行編譯處理的動作
  • output 則是設定 Webpack 打包過後的專案輸出點 —— 以上面的設定來說,它會把檔案打包到 ./dist 資料夾內,並且取名為 bundle.js

編譯到測試開發流程

接之前筆者有提過我們可以使用 lite-server 簡單地 Host HTML 檔案並且監控靜態檔案的變更 —— 如果該 HTML 檔引入的 JavaScript 檔案有被變動時,就會自動幫我們刷新頁面。

首先,我們當然得先要有 index.html 來測試我們編譯過後的 JavaScript 檔案,以下是 index.html 的程式碼內容。

https://ithelp.ithome.com.tw/upload/images/20191005/20120614jy4EAqeEoW.png

另外,我們除了想要使用 lite-server 外,通常使用 webpack 打包專案時,可以使用 webpack -w 開啟 Watch 模式 —— 只要專案一有變動,Webpack 就會自動重新打包並更新輸出結果。

不過這樣子又會遇到必須同時開兩個終端機,分別執行 lite-serverwebpack -w 這兩個指令,於是我們也可以使用筆者之前提到的 concurrently 這個套件 —— 協助我們同時執行這兩種指令。

首先,下載 lite-serverconcurrenly 這兩個套件:

$ npm install lite-server concurrently --save-dev

並且將 package.json 裡的 scripts 修改成:

{
  /* 略... */
  "scripts": {
    "start:watch": "webpack -w",
    "start:serve": "lite-server",
    "start": "concurrently npm:start:*"
  },
  /* 略... */
}

我們就可以使用 npm start 同時執行 webpack -wlite-server 這兩種指令。

以下是下達 npm start 指令時,VSCode 編譯器的結果。(如圖二)

https://ithelp.ithome.com.tw/upload/images/20191005/20120614B81JFr9bgl.png
圖二:一連串執行的過程並且成功地將專案打包出來

另外,lite-server 會自動打開瀏覽器,並且鎖定 localhost:3000 這個 Port。(如果本來的 3000 Port 被佔據了,則會打開 3001,依此類推)(如圖三)

https://ithelp.ithome.com.tw/upload/images/20191005/20120614PAsUMLN73r.png
圖三:打包後清楚地印出 Hello World! 字串

以上就是完整的 Webpack 結合 TypeScript 基礎的環境開發流程!

本篇唯一重點. 使用 Webpack 結合 TypeScript 建立環境

除了 Webpack 基本的套件與設定外,最主要需要注意的是:

  • 必須要下載 typescript 模組到專案裡,因為 Webpack 的 ts-loader 必須仰賴專案內部的編譯器,而非全域
  • TypeScript 檔案對應的 Loader 是 ts-loader
  • 通常使用 Webpack 時,就不太管 tsconfig.json 裡面的 outFile 這個選項,因為 Webpack 會幫你處理好整個打包流程,你也不需要再為其他模組規範擔心來擔心去的
  • 記得啟動 tsconfig.json 裡跟語法或型別監測相關的設定 —— 例如 noImplicitAnystrictNullChecks

UBike 地圖範例簡介

既然環境都設定完畢了,筆者想順便簡單應用目前所學到的東西,示範小專案如何應用 TypeScript 的各種 Feature:諸如型別、類別、介面等等語法的協作。

本範例筆者想要示範的是使用 LeafletJS 打造台北市的 UBike 即時地圖 —— 使用的自然是台北市開放資料中的 UBike 台北市公共自行車即時資訊

如果對 LeafletJS 不熟悉的讀者們,可以將它想成類似於 Google Map 的套件(但不是 Google Map XD)。(圖四為 LeafletJS 官方的網站截圖)

https://ithelp.ithome.com.tw/upload/images/20191005/20120614YfdmolK4Yv.png
圖四:LeafletJS 官方網站

另外,由於本篇內容已經花了一半的篇幅在解說 Webpack + TypeScript 的環境建構流程,因此下半篇只能先把進度推到先把主要地圖建構出來。不過光是這樣的簡單過程,筆者就可以扯到關於套件的型別宣告檔以及一些技巧,這都是在前一篇都討論過的東西,因此這裡會再次示範,不失為一個練習的好機會。

下載 LeafletJS

由於官方的 LeafletJS 的套件 leaflet 是用原生 JavaScript 的程式碼實作的,因此我們也必須同時下載 @types/leaflet

$ npm install leaflet @types/leaflet

由於 LeafletJS 的地圖也有官方的 CSS 檔案需要載入,這裡筆者選擇用它們的 CDN 載入到 HTML 檔案裡。此外,我們也會需要提供一個標籤負責作為地圖的容器,因此在 HTML 檔案裡筆者選擇使用 <div id="map"></div> 作為地圖的容器。以下是 index.html 目前的結果。

https://ithelp.ithome.com.tw/upload/images/20191005/20120614QUJwxywLzg.png

基本地圖的建構

當然,如果對於 LeafletJS 提供的功能有任何問題可以參考官方網站的 Doc。這裡筆者就按照自己寫程式的過程一一記錄下來。

首先,筆者在 index.ts 寫下產生地圖的程式。

https://ithelp.ithome.com.tw/upload/images/20191005/20120614wNGFIMVCq9.png

不過一開始就碰到問題了,TypeScript 對於 taiwanCoord 變數的使用有些警告。(如圖五)

https://ithelp.ithome.com.tw/upload/images/20191005/201206140zmVwPFoyt.png
圖五:很明顯地,setView 方法要求第一個參數的型別為 LatLngExpression 而非 number[]

讀者可能覺得疑惑,明明沒有任何 LatLngExpression 的蹤跡為何會出現這種奇怪的訊息。

其實重點在下一句話:Type 'number[]' is missing the following properties from type '[number, number]': 0, 1

其中,不覺得 [number, number] 這個格式很熟悉嗎?這不就是在很久以前討論過的元組型別嗎?

如果讀者還記得的話,筆者當時有說過:

元組型別一定要進行積極註記,不然會被 TypeScript 自動推論為陣列相關的型別

因此筆者將 index.ts 中的 taiwanCoord 變數宣告的時候註記 LatLngExpression,不過這時候 VSCode 提供的 Auto-Complete 功能非常好用(如圖六)。

https://ithelp.ithome.com.tw/upload/images/20191005/201206146FPKIXbDtV.png
圖六:VSCode 會自動彈出開發者可以註記的型別

其中最重要的一點在後面的提示字:Auto import from 'leaflet' —— 也就是說,當筆者選擇這項功能的時候,VSCode 會自動將 LatLngExpression 自動幫我們載入進去,這是非常好用的功能!

https://ithelp.ithome.com.tw/upload/images/20191005/20120614CnLRmCt9r8.png

此時編輯器內部不會出現任何錯誤訊息(如圖七),此外瀏覽器可以顯示出地圖的結果(如圖八)!

https://ithelp.ithome.com.tw/upload/images/20191005/20120614YeIwu17P9f.png
圖七:編輯器內部除了沒有錯誤訊息外,Webpack 正在幫我們打包檔案呢!

https://ithelp.ithome.com.tw/upload/images/20191005/20120614gSTdcSBNRA.png
圖八:地圖出來了~

建立設定檔並隔離型別註記 Type Annotation Isolated From Main Program

首先,筆者必須說,因為太多亂七八糟的設定摻雜在一起,包含:

  • taipeiCoord 代表的是地圖初始的聚焦點
  • zoom 代表預設的地圖縮放等級
  • 'map' 字串地圖裝載的容器之標籤的 ID
  • tileLayer 方法裡有一個是代表地圖的背景 URL

因此筆者額外建立 map.config.ts 這個檔案在 /src 資料夾裡並填入這些資訊:

https://ithelp.ithome.com.tw/upload/images/20191005/20120614vGfPOukaKv.png

並且在 index.ts 裡引入 map.config.ts(當然,讀者可能也會想要用 JSON 格式,不過後面筆者會說明為何採用 .ts)。

https://ithelp.ithome.com.tw/upload/images/20191005/2012061490MN8KZw6T.png

不過!這裡ㄧ樣會遇到剛剛的問題 —— coordinate 會被推論為 number[] 而非 LatLngExpression,因此我們可以選擇註記 coordinateLatLngExpression;又或者,乾脆在 map.config.ts 宣告該設定檔的 JSON 物件格式並積極註記它,以下是 map.config.ts 的結果。

https://ithelp.ithome.com.tw/upload/images/20191005/20120614iMcEwQk3WZ.png

這樣子,原本的 index.ts 檔案裡的 coordinate 就會被自動推論為 LatLngExpression 了!(如圖九)

https://ithelp.ithome.com.tw/upload/images/20191005/20120614jBsijFmMdS.png
圖九:將型別註記整理到其他地方,主程式就不會特別繁雜,而且 TypeScript 還會自動幫我們載入進來的變數之型別推論結果。

以上就是筆者選擇用 map.config.ts 而非 .json 格式的原因 —— 主程式檔案不會充滿繁雜的註記,但是仍舊在 TypeScript 型別系統的監控下

注意,這一句話是重點:

必須要讓 TypeScript 專案掌控在型別系統之下

很多人以為使用 TypeScript 就是一股腦兒地要讓全部變數型別等等都要被註記下去,但事實上 —— 光是善用 TypeScript 的型別推論有很多優點:

  1. 可以省掉我們的開發時間,不需要每次宣告東西就必須積極註記東西
  2. 程式碼不會滿滿都是型別註記 —— 剛入門 TypeScript 的人看到滿滿的註記,就算看得懂可能心裏也會有壓力感到怕怕的(筆者看到型別註記充滿在檔案裡,除非型別註記是簡潔地,否則也是會感到混亂)
  3. 不用特別註記,TypeScript 就會自動監控專案

這是要善用 TypeScript 的優勢,而不是被工具逼迫一定要每次宣告新的東西就得進行註記

不用打開瀏覽器上網就可以查到文件內容

如果讀者有 Follow 前一篇文章講到的技巧 —— 可以藉由型別推斷出來的結果,使用 VSCode 就可以追溯型別被宣告的原始碼所在位置

首先,假設我們想要知道 Leaflet Map 的個體到底可以使用什麼功能,我們可以將滑鼠指向 L.map 那個方法。

再來是:

  • Mac 系統就按下 Command 鍵
  • Windows 系統就按下 Ctrl 鍵

底下出現底線。(如圖十)

https://ithelp.ithome.com.tw/upload/images/20191005/20120614JXvQ7BEFul.png
圖十:L.map 被顯示出來額外資訊

點下去就會出現很精彩的 —— LeafletJS 套件的型別宣告檔(Declaration File)。(如圖十一)

https://ithelp.ithome.com.tw/upload/images/20191005/201206143qy5ZfcZlC.png
圖十一:這是 L.map 的型別宣告

這裡讀者就可以不用上網查資料就可以知道 —— 原來 L.map 後面還有 MapOptions

重複剛剛的步驟,檢索 MapOptions。(如圖十二)

https://ithelp.ithome.com.tw/upload/images/20191005/20120614S27jP8s2bF.png
圖十二:檢索 MapOptions 的所有功能

哇噻~你不需要上網查,你就知道 MapOptions 裡面可以填入什麼樣的資料,以下筆者列舉幾個:

  • preferCanvas? —— 筆者猜測可能是將 Container 繪出地圖的過程改採用 HTML5 Canvas,而非 SVG 模式
  • zoomControl? —— 如果為 false 代表該地圖不能被縮放
  • dragging? —— 如果為 false 則該地圖不能被滑鼠拖曳
  • 還有更多其他選項

所以筆者要再強調一次:

你如果會使用型別宣告檔查詢功能的話,花在瀏覽器查詢 Doc 的時間會大大減少

這就是筆者強調 Declaration Files 重要的地方 —— 它們可是套件的規格所在地呢!

小結

筆者今天光是講簡單的地圖建構就花了另外五千字講解 —— 善用工具的技巧是筆者首要推廣的目標

下一篇我們就緊接著開始開發更多 UBike 地圖相關的功能喔~

]]> Maxwell Alexius 2019-10-18 15:21:22
Day 37. 戰線擴張・第三方套件 X 支援的引入 - 3rd-Party Package & TypeScript Declaration File https://ithelp.ithome.com.tw/articles/10224070?sc=rss.iron https://ithelp.ithome.com.tw/articles/10224070?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

今天不用想,馬上看下去!

今天筆者要講本篇章系列比較重要的部分 —— TypeScript 的型別宣告檔 Declaration Files。

因此不用多說,正文開始

第三方套件與型別宣告檔 3rd-Party Package & TypeScript Declaration Files

原生 JavaScript + TypeScript?

事實上,要結合原生 JavaScript 與 TypeScript 協作 —— 理論上是可以的

畢竟 TypeScript 主要是從原生 JavaScript 並且採用大部分的 ECMAScript 標準的語法而出來的語言。

但是,如果直接使用原生 JS 與 TypeScript 結合起來的話,基本上:

少了 TypeScript 的型別系統 —— 根本殺雞焉用牛刀

使用 jQuery 為例

筆者以下就以 jQuery 為範例,示範一下 TypeScript 和原生 JS 會遇到的狀況。

首先,筆者將會從頭開始建構簡單的環境,讀者如果熟悉這些動作應該可以快速掠過:

// 前往測試的資料夾
$ cd PATH_TO_DIR

// 初始化 JS 專案
$ npm init -y

// 初始化 TypeScript 專案
$ tsc --init

// 建立 index.ts
$ touch index.ts

// 建立 index.html
$ touch index.html

另外,由於我們要使用 jQuery 作為範例,筆者建立 index.html 並且填入以下的程式碼。

https://ithelp.ithome.com.tw/upload/images/20191004/20120614gXjZppq1CK.png

其中,除了引用 jQuery 的 CDN 外,以上的範例跟之前的計數器範例一模ㄧ樣:當按鈕被按下去時,應該要紀錄按按鈕的次數並且更新資訊。

因此,筆者在 index.ts 裡撰寫簡單的程式碼。

https://ithelp.ithome.com.tw/upload/images/20191004/20120614pAF4pHnxe7.png

讀者應該也會覺得,這一定會被 TypeScript 警告,因為我們沒有宣告任何跟 $ 有關的型別或指派任何的值。(編輯器顯示的錯誤狀況如圖一)

https://ithelp.ithome.com.tw/upload/images/20191004/20120614KiJCOocH99.png
圖一:編輯器上面每個 $ 都出現錯誤提示

但是這個檔案照樣可以被 TypeScript 編譯 —— 只要是原生 JS 的語法符合的情形下,再加上 tsconfig.json 裡的選項 noEmitOnErrorfalse 的前提,硬是要把檔案編譯出來是可以的!

但編譯結果會丟出一連串噁心的錯誤。(如圖二)

https://ithelp.ithome.com.tw/upload/images/20191004/20120614iXcTkIQjHH.png
圖二:TypeScript 完全不知道 $ 這個東西到底是什麼,它似乎提示著我們要下載 jQuery 的型別宣告檔 Declaration Files,這點筆者後面會講到喔!

然而用瀏覽器打開 index.html,還真的可以執行。(如圖三)

https://i.imgur.com/7YHNfeC.gif
圖三:使用瀏覽器測試時,jQuery 照樣可以運作;然而,開發者工具右方的 Source 部分顯示編譯過後的結果,有編譯沒編譯似乎都沒差

型別定義與宣告檔的作用 Type Definition & Declaration Files

為了應對這種來自於第三方套件,想要從原生 JS —— 不需要重寫成 TypeScript 但依然能夠和 TypeScript 檔案協作的話 —— 型別定義(Type Declaration)就必須派上用場!

首先,最陽春的作法就是直接在 index.ts 裡,使用 declare 關鍵字將 $ 的型別宣告出來告訴 TypeScript 編譯器。

https://ithelp.ithome.com.tw/upload/images/20191004/20120614DL4IKXuitj.png

讀者可以看到,只是多了一行 declare 就把型別資訊定義出來了 —— 另外,有 declare 跟沒 declare 的差別在於,儘管 declare 的變數沒有指派任何東西進去,TypeScript 依然認定為該會由某個開發者不需要知道的地方提供出來,而這裡指的某個地方就可能存在於外來套件模組等等。

所以編輯器此時的狀況是乾乾淨淨的,沒有任何錯誤。(如圖四)

https://ithelp.ithome.com.tw/upload/images/20191004/201206149kVpVu5YFU.png
圖四:沒有出現任何跟 $ 相關的錯誤,因為已經告訴 TypeScript $ 這個東西為 any 型別

至於編譯過後的結果等等,讀者可以自行測試,照樣可以在瀏覽器上執行編譯結果喔~

想當然,這應該不是筆者與讀者們樂見的解法,但是筆者還沒講到 Declaration Files 的概念。

通常我們不會把 declare 相關的型別定義程式碼和主程式放在普通的 .ts 檔案 —— 而是會拆開來分成主程式的 .ts 檔外,其餘的型別定義程式碼部分會塞在 .d.ts 檔案 —— 這些就是所謂的 Declaration Files。

所以筆者可以選擇建立一個名為 jquery.d.ts 檔案並且將剛剛的 declare 表達式塞進去。(如圖五)

https://ithelp.ithome.com.tw/upload/images/20191004/20120614FKxkAmgr7u.png
圖五:將 $ 的型別宣告放在 jquery.d.ts 裡時,index.ts 就算沒有宣告跟 $ 相關的變數或型別,TypeScript 依舊認得 $

重點 1. 宣告檔的目的 The Purpose of TypeScript Declaration Files

有些本身使用原生 JavaScript 撰寫出來的第三方套件,若不想要再額外寫 TypeScript 版本的話 —— 可以改採撰寫宣告檔(Declaration Files)—— 目的是要能夠讓 TypeScript 在原生 JavaScript 撰寫出來的程式碼,包裝其型別系統的定義,以利於 TypeScript 與原生 JavaScript 進行合作。

重點 2. 使用宣告檔與型別定義的宣告 Using Declaration Files & Declaring Type Definition

型別定義的宣告通常不會和主程式放在普通的 .ts 檔案,而是會額外放置在 .d.ts 檔案

可以使用 declare 關鍵字宣告型別的定義:

https://ithelp.ithome.com.tw/upload/images/20191004/20120614TgGpoWXZ4R.png

詳細資訊可以看官方 TypeScript Declaration Files 的 Doc

正確引入第三方套件

以下開始,筆者要介紹正確的方式引入第三方套件。通常熱門的套件,就像本篇重點 1. 所提及到的,由於直接改寫成 TypeScript 版本的成本太大,因此會改採用撰寫 Declaration Files 的方式供專案使用。

通常這些專案的 Declaration Files 都匯集在 GitHub 的 Definitely Typed 這個 Repo 裡面喔!筆者這邊提供 jQuery 的 Declaration File 供讀者參考。

另外,通常如果要下載具有 TypeScript Declaration File 版本的套件 —— 那些套件的名稱通常會有 @types 這個前綴字。

所以,如果我們想要下載 jQuery 以及其定義檔,必須要下這個指令:

$ npm install @types/jquery

https://ithelp.ithome.com.tw/upload/images/20191004/20120614KHjUyJLaRd.png
圖六:下載 @types/jquery 套件

其中,如果你翻看 node_modules 裡面的內容,你會發現 —— 只要任何套件含有 Declaration Files,都會在 node_modules 裡面多了一層 @types 這個檔案資料夾。(圖七)

https://ithelp.ithome.com.tw/upload/images/20191004/20120614uxSO18xdch.png
圖七:node_modules 存放的是我們在專案下載的模組,其中 @types 資料夾存放的是 Declaration Files

不過這裡有一些使用 TypeScript 上必須注意的點:通常 @types 系列套件只有型別宣告檔而沒有實際上實作的程式碼

@types/jquery 似乎沒有包含原生 JS 版本的程式碼 —— 如果你仔細看圖七的檔案資料分布,裡面沒有任何 .ts.js 檔案,全部跟 JavaScript 相關的檔案都是 .d.ts 結尾,也就是 TypeScript 的宣告檔

單純只有宣告檔但沒有實際的程式碼的實作來源是沒辦法讓專案動作的,就好比今天你下載的東西是只有一個黑盒子的外殼,但卻沒有實作的細節內容

因此,筆者還是得下載原生 JS 版本的 jQuery 模組:

$ npm install jquery

那讀者可能會問:“恩... 難道所有的套件都得這麼做嗎?必須下載 TypeScript 版本的型別宣告檔外,還需要原生 JS 的 Copy?”

事實上是不一定喔!

就以 RxJS 這個套件來說,它已經全面轉戰到 TypeScript 這個語言進行開發了 —— 想當然,你可以只下 npm install rxjs,它就已經附贈了 .d.ts 相關的 TypeScript 型別宣告檔。

圖八、圖九為筆者臨時下載 rxjs 這個套件給讀者看裡面的內容,讀者不需要在本範例執行此步驟。

https://ithelp.ithome.com.tw/upload/images/20191004/20120614PBlIHb0Sdd.png
圖八:筆者下載 RxJS 這個套件

https://ithelp.ithome.com.tw/upload/images/20191004/20120614LFCslTmbiw.png
圖九:RxJS 套件就算沒有出現在 @types 資料夾下,它本身就附帶了 .d.ts 檔案!

因此,要不要下載原生的套件與對應的 @types 宣告檔是不一定的,要看該套件的情形

而 jQuery 的案例就是除了基本的原生 JS 版本需要下載外,連同 @types/jquery 必須得下載。所以筆者還是得多下:

$ npm install jquery

好的,筆者接下來可以使用 jQuery 套件裡面的功能。在 index.ts 檔案裡面直接進行載入的動作:

https://ithelp.ithome.com.tw/upload/images/20191004/20120614RvrLE9Pgvi.png

另外,Declaration Files 的主要意義就是讓原生 JS 包裝一層 TypeScript 的型別系統,因此如果你使用 jQuery 的 $ 時,TypeScript 就會根據 @types/jquery 提供的宣告檔,顯示你可以使用的功能!(如圖十~十二)

https://ithelp.ithome.com.tw/upload/images/20191004/201206141JDqlBxyrZ.png
圖十:$ 被型別推論的結果

https://ithelp.ithome.com.tw/upload/images/20191004/20120614sVO3qXFtFX.png
圖十一:$() 裡面告訴你,你可以填入 HTMLElement 型別的值

https://ithelp.ithome.com.tw/upload/images/20191004/20120614A3IDSuRnKR.png
圖十二:使用 $(document) 時,可以接的方法內容,為編輯器的 Auto-Complete 功能

另外,如果你想要查詢宣告那些型別的實際程式碼位置 —— 以查詢 $ 之型別宣告位置為例,首先將滑鼠指到 $(這是廢話),然後:

  • 如果是 Windows 系統可以按下 Ctrl
  • 如果是 Mac 系統可以按下 Command(⌘)

ㄧ樣會在 $ 底下出現底線,直接點下去之後就會 Pop 出該型別被宣告的地方。(如圖十三)

https://ithelp.ithome.com.tw/upload/images/20191004/20120614f63U7VTAth.png
圖十三:從我們撰寫的程式碼,查詢套件原本型別被宣告的地方是可以如此地輕鬆

這個功能筆者特別強調的原因是 —— 可以藉由這個方式查詢套件本身的型別結構外,通常 Declaration Files 上面都會有套件的 Documentation —— 也就是說,你可以直接下載完套件、使用裡面提供的功能同時,如果遇到臨時不知道到底套件的功能要接收的參數為何,你可以直接查詢該功能的 Declaration 而不需要再打開瀏覽器上網查它們的文件

這個技巧如果會的話,除了可以大大節省上網查 Doc 的時間,甚至還可以一窺套件內部的結構長相呢

筆者記得這個技巧本來就在選用屬性篇章有提到,這裡又再使用了一次,因此這個技巧可是很重要的呢~

重點 3. 引入第三方套件 Integrating 3rd Party Packages

引入第三方套件的原則不一定,通常熱門的套件都會有相對應的 Declaration Files,而這些宣告檔都匯集在 Definitely Typed 這個 GitHub Repo. 裡。

第三方套件的型別宣告檔,通常都會以 @types/<package> 的格式作為名稱。然而,@types/<package> 不會包含原套件的 JS 程式碼,因此有可能必須同時下載套件的原始碼。

另外,有些本來就有使用 TypeScript 作為開發的套件,本來就會有對應的型別宣告檔,此時就不需要再額外下載 @types/<package> 相關得檔案。

因此,引入第三方套件都是得看該套件本身狀況來決定下載的

重點 4. 查詢套件提供的功能對應到的型別宣告

若臨時想要查詢第三方套件提供的功能之型別宣告所在的地方,有可能是因為:

  • 你想要查看該功能必須填入的參數格式或型別
  • 你想要查看該功能的型別推論與註記的機制為何

你可以使用滑鼠指向該功能的同時:

  • 如果是 Windows 系統,按下 Ctrl
  • 如果是 Mac 系統,按下 Command(⌘)

並且按下滑鼠,此時 VSCode 編輯器就會打開該型別宣告的內容。

這個技巧超重要,甚至到下一個《通用武裝》篇章,會一而再、再而三地使用!

打包程式碼並且使用 RequireJS 執行它

最後筆者要示範 —— 將檔案進行打包後並且在瀏覽器上執行

筆者將 tsconfig.jsonmodule 設定為 system 模式外,並且將 outFile 設定為 index.js

{
  "compileOptions": {
    /* 略... */
    "module": "amd",
    "outFile": "index.js"
    /* 略... */
  }
}

另外,使用 tsc 將我們檔案進行編譯的動作。(編譯結果如圖十四)

https://ithelp.ithome.com.tw/upload/images/20191004/20120614qQYd4jTFTU.png
圖十四:amd 模式下編譯出來的結果

通常選擇哪一種 module 編譯模式,必須要有相對應的 Module Loader 協助我們執行檔案。而 amd 模組模式可以對應的 Module Loader 就是 RequireJS

首先,下載 RequireJS 的檔案,並且筆者將它命名為 require.js。圖十五為目前的檔案資料夾狀態。

https://ithelp.ithome.com.tw/upload/images/20191004/20120614eL358kKnjm.png
圖十五:從 RequireJS 官方下載了檔案

另外,我們必須更改 index.html 的內容。

https://ithelp.ithome.com.tw/upload/images/20191004/20120614KPidG0iUUn.png

主要要注意的是:

  • 必須先載入 require.js,否則瀏覽器會噴出錯誤訊息,因為它會找不到 define 這個東西的定義,而 define 就是 require.js 宣告的東西
  • 載入完 require.js 與編譯過後的 index.js 後,必須要先設定 RequireJS —— 將 jquery 的路徑指向 node_modules 裡的 jQuery 檔案,那是因為 RequireJS 必須知道 index.js 用到 jquery 所在的位置才能夠載入進去
  • 最後才執行 index.js 檔案 —— require(['index'])

打開瀏覽器後可以正常執行。(如圖十六)

https://i.imgur.com/2mnUq6G.gif
圖十六:使用 RequireJS 執行 AMD 模式編譯過後的結果

小結

筆者今天講到的東西感覺特別多,最主要是在強調 TypeScript Declaration Files 的使用以及優勢

另外,執行打包過後的專案也是很重要的課題,因此筆者今天也示範如何簡單使用 RequireJS 執行打包過後的專案。

下一篇筆者就來講簡單的小專案實作喔~

]]> Maxwell Alexius 2019-10-17 19:53:35
Day 36. 戰線擴張・戰線分散 X 組織集中 - TypeScript Namespaces Import/Export Mechanism https://ithelp.ithome.com.tw/articles/10223853?sc=rss.iron https://ithelp.ithome.com.tw/articles/10223853?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

  1. 命名空間的用意是什麼?
  2. 如何運用 TypeScript Namespaces 組織不同區塊的程式碼?
  3. 命名空間融合(Namespaces Merging)有沒有需要注意到的點?

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

筆者認為今天的文章應該會很噁心(至少對筆者來說)。

事實上,讀者應該使用到本篇提到的使用情形,應該很少,不過為了完整性,筆者還是補齊一下。(畢竟,ES6 Import/Export 還是常用)

不過筆者免不了地想大吼一下:“好想趕快進入《通用武裝》篇章啊 —— 泛用型別的東西真的精彩好多啊!啊!啊!啊!”

但是這邊東西不講又不行(本篇一切都是 TypeScript 版本歷史過程下遺留的塵埃,煙蒂掉了一大堆髒兮兮的,有些讀者可能讀完本篇後會和筆者有類似的體會),反正就冷靜地進入正文~

正文開始

命名空間載入/輸出的機制 Namespaces Import/Export Mechanism

引用命名空間的方式 Importing Namespaces From Another File

還記得在上一篇,筆者講到命名空間是可以被重複宣告的 —— 另外,如果複數個宣告為同一個命名空間的結果會融合成一個,這在官方被稱為 Declaration Merging

今天筆者ㄧ樣用簡單的範例來說明。其中,筆者先建立一個名為 Circle.ts 的檔案,內容如下:

https://ithelp.ithome.com.tw/upload/images/20191002/20120614uRKRsW9CJ3.png

這應該對讀者來說挺簡單的,裡面有 PI 代表圓周率,以及計算圓的面積與周長的功能。

再來,如果我們要引用外部的命名空間 —— 根據 TypeScript 官方說明使用 Namespace 的方式 —— 筆者把官方在 Namespace 裡的 Splitting Across Files 某部分說明截下來:

Once there are multiple files involved, we’ll need to make sure all of the compiled code gets loaded. There are two ways of doing this.

First, we can use concatenated output using the --outFile flag to compile all of the input files into a single JavaScript output file.

(...中間略)

Alternatively, we can use per-file compilation (the default) to emit one JavaScript file for each input file. If multiple JS files get produced, we’ll need to use <script> tags on our webpage to load each emitted file in the appropriate order.

(以下是筆者覺得很麻煩的翻譯時間)

有兩種方式可以使用:

  1. 使用 outFile 這個編譯器設定將所有檔案打包在一起,也就是在本系列前幾天談到的一些內容。
  2. 如果是前端的話,可以直接把所有的檔案使用 <script> 引用進去,但順序很重要呢!

貼心小提示

理解 TypeScript 編譯器的某些設定是很重要的~除非有經驗的讀者早已熟悉,若跳過編譯器設定系列的內容建議趕快補齊呢!

前端引用法 Front-End Import Technique

筆者就先從比較簡單(但實際上不太好用?)的方式介紹,也就是官方所講的第二種方式 —— 在 HTML 檔案使用 <script> 引入模組。

1. 普通引用情形 Normal Case

以下筆者簡單地寫出測試的程式碼:

https://ithelp.ithome.com.tw/upload/images/20191002/20120614p9ZNZ33OEC.png

另外,筆者忘記提到一點,就算你把命名空間隔離到其他檔案,TypeScript 還是會認得那些命名空間,這應該算是 TypeScript Declaration 的 Magic 吧?(筆者如是說

所以圖一是 index.ts 的結果 —— 使用 Circle.ts 裡的 Circle 命名空間還是認得出來;圖二則是你在使用命名空間時,TypeScript 會很貼心地提示 —— Circle 有哪些可以用的屬性或方法,算是所謂的編輯器 Auto-Complete 的 Feature。

https://ithelp.ithome.com.tw/upload/images/20191002/20120614WwCIvHriIS.png
圖一:左方為 index.ts、右方為 Circle.ts —— 就算在別的檔案使用 Circle,TypeScript 還是認得出來,因此不會出現警告喔

https://ithelp.ithome.com.tw/upload/images/20191002/20120614RwNSKvopbF.png
圖二:筆者打上 Circle 幾個字,後面就會出現提示性的功能,顯示 Circle 命名空間有的屬性與功能

筆者接下來按照 TypeScript 官方的說明,直接用預設模式進行編譯(使用 tsc),所以編譯出來應該會是分開的兩個檔案喔。(如圖三所示)

https://ithelp.ithome.com.tw/upload/images/20191002/20120614hGZBOmDWR2.png
圖三:預設的 TypeScript 編譯器,編譯出來的結果

並且建立一個 index.html 檔案,將兩個 JavaScript 檔案引用進去。

https://ithelp.ithome.com.tw/upload/images/20191002/20120614zpGeySXd6B.png

打開 index.html 可以看到結果如圖四。

https://ithelp.ithome.com.tw/upload/images/20191002/20120614K3rt77IcPQ.png
圖四:瀏覽器的 Console 輸出結果

其中,如果檢視 Circle.js 編譯過後的結果(如圖五),你會發現它就是簡單的 IIFE 函式包裝起來的 —— 一般 JavaScript 在宣告一個簡單模組時會用到小技巧,只是被 TypeScript 用一行 namespace 的宣告進行包裝而已。

https://ithelp.ithome.com.tw/upload/images/20191002/20120614ZM54N6jkNR.png
圖五:檢視瀏覽器的 Source,其中 Circle.js 被編譯過後的結果純粹只是簡單的 IIFE

貼心小提示

筆者當初學習原生 JavaScript 的時候,個人 naively 以為 IIFE 是不太會用到的東西 —— 因為無法想像會在什麼情況需要用到 Anonymous Function 然後馬上呼叫(Invoke)它。(簡單來說,當時的筆者菜味很重卻不自知

事實上,IIFE 好用的地方在於 —— 它可以將變數作用域進行隔離的動作,因此用到的時機比想像中多很多,想要組出更多更複雜的系統 —— 使用原生 JavaScript 無可避免地(Inevitably)會遇到 IIFE 這種東西

不過這也要怪當初設計語言的人將變數作用域設計成只有分全域(Global Scope)與函式作用域(Functional Scope),所以才會需要使用 IIFE 建構出乾淨的變數作用域。筆者沒有想要婊誰,但想要耍 P、婊一下當初設計 JavaScript 的人 XDDDDDD

所以學過原生 JavaScript 卻和(過往的)筆者抱持 “反正 IIFE 我也不會用到” 的心態的讀者,筆者誠心建議至少要了解一下 IIFE 的功用。

然後你就會涅槃到另一個境界請筆者停止講 P 話

2. 多組不同命名空間 Multiple Different Namespaces Distributed Among Different Files

另外,我們當然可以再新增更多檔案分佈不同的命名空間 —— 筆者另外新增名為 Rectangle.ts 的檔案,裡面的內容也是宣告一個名為 Rectangle 的命名空間。

https://ithelp.ithome.com.tw/upload/images/20191002/20120614bu66yYW4BH.png

以下筆者更改一下 index.ts 裡的測試程式碼。

https://ithelp.ithome.com.tw/upload/images/20191002/20120614HUF9L8feEp.png

想當然,index.html 勢必得更新。

https://ithelp.ithome.com.tw/upload/images/20191002/20120614vlqJZU9L8H.png

不過別忘了,記得先下 tsc 編譯出結果再打開 index.html 喔!(測試結果如圖六)

https://ithelp.ithome.com.tw/upload/images/20191002/20120614QKACfjX4FQ.png
圖六:完全執行到正確的結果

3. 相同命名空間的融合 Identical Namespace Distributed Among Different Files

另外,命名空間融合的案例也是可以的

這裡筆者建立一個名為 MyMath 的檔案資料夾 —— 將原本的 Circle.tsRectangle.ts 丟進去,並且額外再包裝一層命名空間 MyMath

https://ithelp.ithome.com.tw/upload/images/20191002/20120614pvlvCaMpp8.png

https://ithelp.ithome.com.tw/upload/images/20191002/20120614G3bhtMVgVf.png

圖七為目前的檔案資料夾的分布狀態。

https://ithelp.ithome.com.tw/upload/images/20191002/20120614yzHpL2Chd1.png
圖七:Circle.tsRectangle.ts 裡的命名空間都被包覆一層 MyMath,而兩個檔案都被放在 MyName 這個資料夾

最後,筆者附上 index.ts 的內容。

https://ithelp.ithome.com.tw/upload/images/20191002/20120614T67Jf27NHV.png

讀者如果測試本範例,一定會發現就算檔案被塞到更裡面的資料夾,TypeScript 依舊會記得所有的命名空間被宣告的結果 —— 並且也會根據上一篇提到的命名空間融合(Namespaces Merging)的規則,將重複宣告的命名空間進行融合。

當然,同類型的命名空間可以交互使用輸出的功能外,其他的功能(包含變數、函式、類別、型別的宣告)都不能互相覆寫 —— 除非是介面,因為介面也有介面融合的規則

前一篇重點沒看完至少也要看一下!

另外,index.html 也因此必須更新。(筆者知道更新內容超多,但這是為了展示給讀者看不同的案例下使用命名空間的標準流程)

https://ithelp.ithome.com.tw/upload/images/20191003/20120614nvDNxcktZm.png

以下圖八是檢測的結果。(筆者強烈提醒:記得要先用 tsc 編譯才測試啊!!!)

https://ithelp.ithome.com.tw/upload/images/20191003/20120614K7tpKE9OAe.png
圖八:檢測結果符合!

重點 1. 前端引用命名空間模組 Front-End Namespaces Import

若命名空間散佈在不同的檔案裡,且每個命名空間若有重複宣告時,符合命名空間融合的原則,則在普通編譯模式下 —— 編譯結果為每個 TypeScript 的檔案會對應出一個原生的 JavaScript 檔案。

其中,前端部分可以使用 <script> 標籤引入 Namespaces,但必須注意順序,通常會先把所有的命名空間相關的檔案載入後再載入主程式。

不過通常我們不會這麼麻煩地一個個將命名空間的檔案使用 <script> 標籤載入進去 —— 如果十個檔案必需載入進去,除非你可能會寫 Shell Script 自動化程序去遍歷所有跟 Namespaces 相關的檔案(但依然還是有順序搞錯的風險),否則你也只能手動將十個檔案硬刻進去。

所以這個方法才會被筆者說不太可行的辦法 —— 小型專案可能還可以接受

另外,這個方法也僅僅只能用在前端,後端如果硬用 NodeJS 執行當然會出錯,因為找不到模組!(如圖九)

https://ithelp.ithome.com.tw/upload/images/20191003/20120614WrwEdgpnK3.png
圖九:使用 NodeJS 後端執行一定會出錯

專案打包法與參照指令 Bundle Technique & Reference Directives

打包的方式應該是大家普遍會用的手法,不過讀者可能還是會問:“為何筆者還要花大篇幅講解很沒用的前端引用法?”

理由是:純粹比較好講解 XD,又可以快速驗證多個不同檔案拆開來時,命名空間的規則會不會跟著改變 —— 從剛剛驗證的過程得知的結論自然是不會有任何變化

很明顯地,outFile 這個編譯器設定又被派上用場。認真閱讀本系列的讀者一定知道,outFile 有使用上的限制:

必須要在模組規範為 amdsystem 下才能進行打包成單一檔案的動作,也就是說,編譯器設定裡的 module 選項只能為 'amd''system',預設的 commonjs 是錯誤的喔!

因此筆者將 tsconfig.json 裡面得編譯器設定為:

{
  "compileOptions": {
    /* 略... */
    "module": "amd",
    "outFile": "./result.js",
    /* 略... */
  }
}

但是這還不夠

今天還要介紹另一個很邪門的東西 —— Triple Slash Directive,但筆者實在不喜歡直翻成中文,所以才另起一個中文名稱:參照指令(Reference Directives)。

其實用久了,你也自然而然不會覺得這個東西邪門

首先,由於想要在 index.ts 載入 MyMath 這個命名空間,你必須使用參照指令。

修改過後的 index.ts 如下。

https://ithelp.ithome.com.tw/upload/images/20191003/20120614TKAlB2UXIo.png

讀者可能覺得奇怪,還要一個個載入 Namespace,而且是用很奇怪的語法:

/// <reference path="<path-to-file>" />

筆者不開玩笑,而且多一條或少一條斜線就會視為不見,這是 Triple Slash Directive 的特性

另外,如果想要詳細看 Triple Slash Directive 的其他應用,可以直接點本連結,因為本系列會用到應該也就只有少數幾篇。不過筆者還是提醒,它還有其他功能,有興趣可以去翻看~

回過頭來,如果你嘗試使用 tsc 去編譯並且使用 node 執行。(如圖九)

https://ithelp.ithome.com.tw/upload/images/20191003/20120614eqdznOxL4H.png
圖九:成功地使用 node 執行了檔案

另外,以下展示實際上 result.js 被編譯出來的結果。(程式碼有些長,不過請大致上看好結構就可以了!)

https://ithelp.ithome.com.tw/upload/images/20191003/20120614ZS6518vW67.png

可以看得出來,編譯的結果就只是把 CircleRectangle 用 IIFE 的方式載入進去。

讀者試試看

這邊讀者可以試試看:把 index.ts 的參照指令拿掉後,直接編譯並且執行 result.js 會發生什麼事情。

另外,如果 module 換成 system 模式,編譯出來的結果會不會動作?

另外,筆者就不示範將 result.js 載入前端 HTML 檔案測試囉~ 光是看到它是按照 IIFE 的格式產出就可以知道它一定可以在前端使用。

重點 2. 專案打包法載入命名空間 Bundle Technique to Import Namespaces

除了在前端一個個使用 <script> 方式引入命名空間外,可以選擇啟動編譯器設定 outFile 並且調整 moduleamdsystem 模式,將專案打包成一個檔案,同時可以在前端執行外,後端 NodeJS 也可以執行。

然而想要打包成單一個檔案,必須使用參照指令(Triple Slash Directives)引入命名空間模組,其語法如下:

https://ithelp.ithome.com.tw/upload/images/20191003/20120614ZOTVaRmK9c.png

多或者少一個斜線,亦或者是結尾的 /> 斜線少掉也不行

你真的會需要用到 Namespace 嗎?

事實上,普通的專案裡,我們不太需要用到 TypeScript Namespaces,因為 JavaScript 的解法就是用 IIFE 的方式解決外,ECMAScript 的 Import/Export 語法也解決了很多跟模組相關的問題。(請參照這篇 StackOverlow

你可能還會問說:“那 TypeScript 為何還要出什麼 Namespaces 呢?”

其實回答這個問題之前,事實上筆者沒有講到 —— TypeScript Modules 這一個東西。

你沒聽錯!

TypeScript Module 與 TypeScript Namespace 這兩者是完全不同的東西,在很久很久以前的時候(約 2014 年時灰姑娘沒換玻璃鞋、白雪公主還沒吃毒蘋果、愛麗絲還沒夢遊仙境時),那時候 Module 被稱為 External Module、Namespace 則是 Internal Modules。

筆者看到也是覺得 WTF,不過你想想看:2014 年那時候 ES2015 的標準還沒出來,所以 TypeScript 必須要有特定的解法去實踐模組的載入與輸出系統,於是 TypeScript Module 與 Namespaces 出來了。

所以對於現在的專案開發來說 Namespace 對於 TypeScript 或現在的 JS 開發而言是一種 Old Fashion 的手法,幾乎沒有出現了。

筆者還是想抱怨:“哎... 歷史遺留下來的 Legacy Feature。”

那麼筆者為何要讓大家知道有 TypeScript Namespace 這個東西呢~ 因為第三方套件 —— 有些的定義檔 Definition Files 就使用 Namespace 包裝一系列型別宣告相關的程式碼,然後就沒有再改,因為怕改了之後造成連鎖反應(Chain Reaction),讓相依的其他套件壞光光

如果這時想看原始碼還看不懂 Namespace 語法的話,想當然就尷尬了。

鐵人所見略同

引用自 —— 【 Day 27 】在 React 專案中使用 TypeScript - 命名空間(namespace) by Kira - TypeScript 初心者手札系列(精華版系列)

“換句話說,目前實際開發上似乎很少使用命名空間,大部分使用模組來組織程式碼結構,事實上真是如此嗎?可能要有實際開發經驗的前輩們來解答了!”

筆者卡這一關也卡很久,不過產出了這篇文之後,讀者讀過能夠回答這個問題了嗎?

小結

筆者總算把 TypeScript Namespace 的內容基本上涵蓋完畢了!

下一篇要講到很重要的 TypeScript 定義檔 —— Definition Files 外,還會簡單地示範如何引入第三方套件喔~。

這又比 TypeScript Namespace 的重要程度高幾百倍也不為過!敬請期待~

]]> Maxwell Alexius 2019-10-16 16:26:17
Day 35. 戰線擴張・命名空間 X 組織分明 - TypeScript Namespaces Introduction https://ithelp.ithome.com.tw/articles/10223480?sc=rss.iron https://ithelp.ithome.com.tw/articles/10223480?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

讀者認為目前對 TypeScript 編譯器的設定的了解程度如何呢?

如果還沒理解完畢的話,可以先翻看最近這幾天的文章喔!

今天的東西筆者認為讀者不一定會需要用 —— 因為 ES6 提供的 import / export 語法除了被 TypeScript 採用外,而且還可以被轉譯成不同的模組形式(利用 tsconfig.json 裡的 module 設定)。

但是筆者強烈建議讀者必須要知道 TypeScript Namespaces 的目的是為了能夠理解第三方套件到底如何融入到 TypeScript 專案

在結合第三方套件時,如果遇到別的套件的型別定義有出現雷或者是錯誤,有時候可以藉由 TypeScript Namespaces 的機制,重新詮釋適合自己專案的型別定義與設定

畢竟使用 TypeScript 的宗旨就是善用 TypeScript 提供的型別系統(Type System)來協助專案的開發與維護

本篇開始以後應該算是《戰線擴張》篇章系列中最重要的一段(不過讀者還是得知道編譯器設定的基礎 XD),因此我們從理解 TypeScript Namespaces —— 正文開始

命名空間模組 TypeScript Namespaces Introduction

什麼是命名空間?

筆者剛開始學程式(不是指剛開始學 TypeScript 喔)聽到命名空間這個詞,完全不知道到底在搞什麼。看到 C++ Namespace 時,問了這個問題然後完全被一句話恍惚帶過:

喔~ 防止程式的衝突啊,不然咧?

聽完當下很想直接揍那個人一拳

事實上,筆者後來學程式久了之後,大概會這樣詮釋:

重點 1. Namespaces 的意義

由命名 Name 與空間 Space 兩個單字組成。(廢話

命名空間主要的目的是組織並且包裝程式碼。

其中,每個人寫的程式碼、套件、框架等,在各種變數、函式、類別等等的取名上可能會有重複,譬如:某 A 與 B 套件可能同時用到名為 cache 的變數,這樣一來,如果兩個套件一起使用會產生嚴重的衝突 —— 來自於變數命名上的衝突

為了將不同的程式碼區塊進行隔離(防止交叉感染交互污染),於是宣告名為 Namespace 的區塊 —— 每個程式碼、套件等可以建立起自己的空間,自由使用命名而不需要擔心會不會誤用到其他套件或程式碼已經命名過的變數、函式等。

宣告命名空間 Namespaces Declaration

首先,筆者本篇準備要示範的程式碼內容如下:

https://ithelp.ithome.com.tw/upload/images/20191002/2012061429hGwQRxzs.png

相信讀者應該很熟悉這些函式到底在做什麼,不過筆者還是簡單說明:

  • PI 為常數,代表圓周率
  • AreaOfCircle 為計算圓面積的函式,必須填入一個參數 radius 代表圓的半徑
  • AreaOfRectangle 為計算長方形面積的函式,必須填入兩個參數 widthheight 代表長方形的邊長
  • CircumferenceOfCircle 為計算圓周長的函式,必須填入一個參數 radius 代表圓的半徑
  • CircumferenceOfRectangle 為計算長方形周長的函式,必須填入兩個參數 widthheight 代表長方形的邊長

筆者就不對以上的程式碼進行測試了,直接切入正題。

首先,以上的範例平常使用可能不會有太大的問題,僅僅只是計算幾合圖形的各種資訊而已。

然而,如果你引入各種不同的套件,譬如繪圖相關的套件,免不了會有跟圓或者是長方形相關的計算與變數等。

假設剛好引入的套件裡也含有 PI 這個變數,這樣就尷尬了 —— 它會跟剛剛的範例程式起衝突,尤其 PI 在以上的程式碼又是被定義為常數(Constant)。(不過現在要遇到會跟專案本身起衝突的套件應該幾乎沒有了,因為都可以被 IIFE 隔開掉)

於是我們希望可以建立起不同的命名空間防止污染到程式碼的全域,這時候本日主角 —— namespace 派上用場啦~

https://ithelp.ithome.com.tw/upload/images/20191002/20120614JfgxnUty61.png

讀者應該可以看到,筆者只是簡簡單單地使用 namespace 關鍵字配上一個名稱 MyMath 宣告出一個命名空間,並且將所有的程式包裝起來。

通常要使用 namespace 裡面提供的功能,可以將 namespace 視為 JSON 物件的感覺(但 Namespaces 不是 JSON 物件!),使用點(.)呼叫出命名空間內部的屬性與方法。不過,如果你想要呼叫出 MyMath.PI 的話還是會出現錯誤!(如圖一)

https://ithelp.ithome.com.tw/upload/images/20191002/201206146VT5vpUaW8.png
圖一:MyMath.PI 不屬於 MyMath,這是怎麼一回事?

原來 —— 如果想要讓命名空間提供各種功能的話,必須使用 export 關鍵字 —— 因此筆者將所有的 MyMath 裡面的變數與方法進行輸出的動作。(實際上,讀者當然可以選擇輸出部分的功能)

https://ithelp.ithome.com.tw/upload/images/20191002/201206149mZjSfmuZU.png

這樣一來,TypeScript 的警告訊息就消失囉~(簡單測試結果如圖二)

https://ithelp.ithome.com.tw/upload/images/20191002/20120614lgQ7u9cUk2.png
圖二:簡單的測試結果,可以使用 MyMath 命名空間提供的各種功能

重點 1. 命名空間的宣告 TypeScript Namespaces Declaration

若想要宣告某命名空間 N,該空間若想提供變數 X 與方法 M,則宣告時,必須將 XM 使用 export 關鍵字進行輸出的動作。

https://ithelp.ithome.com.tw/upload/images/20191002/20120614y80IB4L00J.png

若想要使用命名空間 N 輸出的功能,則可以使用點 . 來呼叫。

https://ithelp.ithome.com.tw/upload/images/20191002/20120614cVVIFWqgUp.png

巢狀命名空間 Nested Namespaces/Multi-layer Namespaces

當然,命名空間有很彈性的功能 —— 可以使用巢狀式的命名空間來整理程式碼。如以下的範例:

https://ithelp.ithome.com.tw/upload/images/20191002/20120614r8wKaswSYx.png

很遺憾地,因為命名空間有嚴格的規則:

凡任何要從命名空間輸出的功能,必須要使用 export 關鍵字進行輸出

也就是說 —— 呼叫 MyMathV2.Circle,光是這樣又會出現警告!(如圖三)

https://ithelp.ithome.com.tw/upload/images/20191002/20120614aVHMB1osnn.png
圖三:結果想要呼叫 MyMathV2.Circle 命名空間內的功能,但是因為 Circle 命名空間沒被輸出,所以被視為不存在

因此筆者快速將 export 標註在 CircleRectangle 上。(以下程式碼測試結果如圖四)

https://ithelp.ithome.com.tw/upload/images/20191002/20120614bacHtPLSki.png

https://ithelp.ithome.com.tw/upload/images/20191002/20120614DDACazVYhS.png
圖四:成功地使用 MyMathV2.CircleMyMathV2.Rectangle 兩個不ㄧ樣的模組

重點 2. 巢狀命名空間 Nested Namespaces

可以運用巢狀命名空間將程式碼整理到不同的區塊,並且任何內部的命名空間要被外部使用時,必須標註 export 關鍵字。

任何東西想要輸出就必須標註 export 關鍵字

筆者再更近一步延伸 —— 型別、介面甚至是類別的宣告也必須使用 export 關鍵字!

https://ithelp.ithome.com.tw/upload/images/20191002/20120614AJa13sF3F3.png

因此使用以上的程式碼,測試後結果如圖五。

https://ithelp.ithome.com.tw/upload/images/20191002/20120614mZCuMQGKEP.png
圖五:所以連型別系統的東西與類別都可以被存放在命名空間裡

重點 3. 命名空間可以輸出的功能 Exportable Utilities From Namespaces

只要是變數(Variables)、函式(Functions)、型別(Types,也包含 列舉型別 Enumerated Type)、介面(Interfaces)、類別(Classes)與命名空間(Namespaces)都是可以在命名空間裡使用 export 進行輸出。

唯一不能輸出的是值(Value)本身。(例如:你不能在命名空間直接 export 123;,這樣會出現警告)

命名空間的融合 Namespaces Merging

介面的融合 Interface Merging 概念很像,不過官方統稱為宣告的融合 Declaration Merging

其實就只是將 namespace 裡面的東西拆開來,但是命名空間的名稱都會是ㄧ樣的!所以剛剛在 MyMathV2 所列舉的例子跟以下 MyMathV3 是等效的!(圖六是以下的程式碼測試的結果)

https://ithelp.ithome.com.tw/upload/images/20191002/20120614xoANHgXkMS.png

https://ithelp.ithome.com.tw/upload/images/20191002/20120614LMApkYxthV.png
圖六:測試 MyMathV3 的結果

不過這裡就要和讀者探討一個問題了:

如果想要在交互使用同個名稱的命名空間下,卻分配到不同的區塊的功能,會出現什麼樣的結果?(這就是所謂的亂搞

筆者為了減少討論案例的複雜度,以下會以 Circle 這個命名空間進行範例延伸討論:

https://ithelp.ithome.com.tw/upload/images/20191002/20120614FsXl7MJ2r6.png

1. 同個命名空間下,能不能交互使用輸出的功能

以下的程式碼,筆者額外宣告同為 Circle 的命名空間 —— 其中,第二次宣告的命名空間有使用到第一個命名空間宣告的 PI

https://ithelp.ithome.com.tw/upload/images/20191002/201206147CxAxZufF5.png

結果答案是:可以。(測試結果如圖七)

https://ithelp.ithome.com.tw/upload/images/20191002/20120614b4wHQHDoEy.png
圖七:同個命名模組下,可以交互使用輸出的功能!

2. 同個命名空間下,能不能交互使用沒有輸出的功能

以下的程式碼,筆者取消掉第一次宣告的命名空間 Circle 裡面的 PI 的輸出:

https://ithelp.ithome.com.tw/upload/images/20191002/20120614QIFAKtdIxi.png

結果答案是:不行。儘管是同個命名模組下的宣告,但是要能夠在不同地方使用功能,照樣得遵守輸出功能的原則 —— 在變數、函式等等要輸出的東西旁邊標註 export 關鍵字。(測試結果如圖八)

https://ithelp.ithome.com.tw/upload/images/20191002/201206149TF1WwFU4e.png
圖八:如果 PI 沒有被輸出,就算是在同名稱的命名空間下,不能使用 PI

3. 同個命名空間下,能不能覆寫前一個命名空間輸出的功能?

以下的程式碼,筆者覆寫掉第一次宣告 Circle 命名空間時的 area 方法:

https://ithelp.ithome.com.tw/upload/images/20191002/20120614ezaSRCbKaZ.png

測試結果是:不行。因此重複宣告同一個函式是錯誤的。(測試結果如圖九)

https://ithelp.ithome.com.tw/upload/images/20191002/20120614C5T9WX5DCu.png
圖九:答案是不能宣告重複的函式Duplicate function implementations

但請讀者別急著下結論!

如果是介面(TypeScript Interface)這種具備可融合的特性

https://ithelp.ithome.com.tw/upload/images/20191002/20120614QXv1MCjBeH.png

讀者可以自行驗證,以上的程式碼在 TypeScript 裡面不會出現警告。

因此本題的答案是:不一定 —— 如果是一般變數、函式、類別甚至是型別(Type)的宣告就會出現類似 Duplicate declaration 相關的錯誤。

然而,因為介面具有可被融合的特性,儘管在不同區塊之同一命名空間下,介面也是可以被融合呢

重點 4. 命名空間的融合 Namespace Merging

若某命名空間 N 在不同的地方有被重複宣告,則 N 所提供的功能為所有不同地區宣告的 N 輸出的功能進行聯集的結果。

https://ithelp.ithome.com.tw/upload/images/20191002/20120614jmb37Aytoc.png

另外,不同區塊的 N 可以交互使用各自輸出的功能

然而,不同區塊的 N 不能覆蓋之前宣告的 N 所提供的功能包含變數(Variables)、函式(Functions)、類別(Classes)與型別(Types),但介面(Interfaces)屬於例外 —— 因為介面具備融合的特性,這時候並不是覆蓋過往宣告的介面,而是和過往介面的宣告進行融合的動作

最後的最後,筆者還是得額外提醒一下 —— 介面融合與命名空間融合的差別:

重點 5. 介面融合 Interfaces Merging V.S. 命名空間融合 Namespaces Merging

  1. 介面宣告的是規格命名空間則是一系列功能的包裝,主要意義在於防止全域的污染
  2. 兩者皆可以動態擴充,然而擴充的意義不同 —— 介面擴充的意義就是規格的擴充;命名空間則是包含介面,任何功能的擴充(如:變數、函式、類別等等的宣告)
  3. 介面可以進行函式的超載;然而,命名空間因為單純只是對實際的功能進行輸出的動作,因此函式自然根本不會有什麼超載或者是很神奇的功能(不過這個應該是廢話
  4. 命名空間裡面可以包含介面的宣告與輸出,反之則不行(這應該也是廢話,誰會在介面裡面宣告 Namespace?

小結

今天主要是先讓讀者熟悉基本的 TypeScript Namespaces 的語法特性,因為下一篇就要進到更難纏一點的單元 —— 將程式碼分配到不同的檔案進行載入的動作 —— 這是學習開發基本 TypeScript 專案的基礎之一;然而,就廣義層面來說,讀者如果有微調第三方套件的模組所宣告的型別定義檔(Definition File)的需求,那麼 TypeScript Namespaces 的概念可就真的得打好基礎呢,這後面會再提到。

另外,讀者可能也會對於 TypeScript Namespaces 與 ES6 Import/Export 的使用差別感到疑惑,不過這一點下一篇的後面會講到 Namespaces 的由來,應該就會知道為何會多出這個東西了~

]]> Maxwell Alexius 2019-10-15 14:47:48
Day 34. 戰線擴張・專案語法 X 嚴格把關 - TypeScript Compiler Syntatic Checks Configurations https://ithelp.ithome.com.tw/articles/10223400?sc=rss.iron https://ithelp.ithome.com.tw/articles/10223400?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

  1. 前端的 Debug 技巧有哪些?
  2. 編譯過後的檔案通常會有對應的 Source Map 檔,其中 Source Map 到底是在做什麼呢?

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

筆者知道這邊一直討論編譯器設定有些人覺得很無聊。

但想要開開心心學 TypeScript,有時候一些潛藏的功能理解了就會事半功倍。

今天筆者就要來介紹更多可以加入的語法檢測選項,讀者可能會疑惑:“恩... TypeScript 不是光靠型別推論與註記就飽了嗎?還有附帶語法檢測?”

對,這邊就比較屬於常聽到的 Linter 層面設定的東西 —— 如果開發一些專案應該有都會有相對應的 Style Guide,尤其筆者常用的是 AirBnB 廠商出的 JavaScript Style Guide;而 Linter 最主要的目的是實踐 Style Guide 上面的規則應用到專案裡 —— 提升程式碼的品質(Code Quality)。

所以記得,不是型別系統提供的功能,今天要講的是偏向於 Linter 層面的 TypeScript 編譯器設定。當然還會稍微討論一些跟 Style Guide 有關的東西喔!

[2019.10.14 14:41 新增訊息]

筆者最近的身體修養狀況開始逐漸不錯(筆者參加鐵人賽其實是在養病的時候參加的 XD),可能一個禮拜之後會開始繼續找工作。

然而筆者希望可以維護文章品質 —— 完整把系列推播完畢,鐵人 30 天後的延伸系列ㄧ樣會儘量以一天一篇文的方式推播,但如果真的也沒辦法趕上一天一文,會以兩天一文的方式推播,不過這應該也是幾個禮拜以後的狀況。

語法檢測把關 Syntatic Checks

TypeScript 官方有沒有 Style Guide?

事實上,官方並沒有推出正式的 Style Guide,請參見這帖 StackOverflow

不過仔細想想背後的原因,在 StackOverflow 上面的帖子講得很清楚:

The TypeScript team doesn't issue an "official" style guide for other projects using TypeScript. The guidelines for working on the compiler itself are both too specific and not broad enough for general use;(... 略)

Any JavaScript style guide that is up-to-date for ES6 is going to cover nearly all TypeScript constructs except for type annotations,(... 略)

TSLint is a good choice for enforcing style rules around types / type annotations.

筆者簡單說明:TypeScript 的本質就只是在原本的 JavaScript 包裝一層型別系統與更多延伸語法(Class、Decorators 等等),所以普通的 JavaScript Linter 大部分就可以 Cover 掉所有的語法檢測的各種狀況。

另外,TypeScript 唯一可能需要被語法檢測的部分也就只有型別系統的註記語法(Type Annotation)有沒有符合規定而已 —— 所以才會出現 TSLint 這個東西。

這裡必須要額外注意的點是:TSLint 目前有被 Deprecate 的可能,因為要直接跟 ESLint 進行結合。(如圖一)

https://ithelp.ithome.com.tw/upload/images/20191001/20120614RfdFEP8TBd.png
圖一:官方的 TSLint 宣告,在 2019 年的某個時間點將會把 TSLint 結合到 ESLint

因此如果你看到這個 Issue 的訊息,裡面就是在講述 TSLint 結合到 ESLint 的過程。(如圖二)

https://ithelp.ithome.com.tw/upload/images/20191001/20120614cJvU70IVSR.png
圖二:正式的 TSLint -> ESLint 的 Proposal

有興趣的讀者可以自行看看。所以也有可能讀者看到本篇時,TSLint 早就被 ESLint 結合了也說不定喔!

語法相關設定 Syntatic Checks Related Configurations

因此本篇正式進入跟語法檢測有關的功能,第一個就是要講一開始筆者常用的 strickNullChecks —— 這東西也被筆者拖得夠久了,算是系列文章的債務?

1. Nullable Types 看待為為原始型別 —— strickNullChecks 設定

什麼叫做將 Nullable Type 看待為原始型別

難道從一開始,Day 02. 提到的原始型別筆者講錯了嗎?(難道筆者錯了嗎!?

事實上,筆者在《前線維護》篇章系列時,強調過筆者習慣將 strictNullChecks 啟用,因為這會使得 nullundefined 自成一個型別 —— 因此這裡筆者就得反過來討論:strickNullChecksfalse 的狀態會是什麼。

以下是簡單的範例程式碼。

https://ithelp.ithome.com.tw/upload/images/20191001/20120614IJ8sH28MZb.png

讀者沒看錯,就是這麼蠢的範例。

讀者如果仔細讀過本系列原始型別篇章,遇到遲滯性指派(Delayed Initialization)的案例時,變數 a 還在未確定之前就被使用 —— 一定會觸發 TDZ 錯誤(Temporal Dead Zone)。

因此編輯器內部的結果會出現 TypeScript 的錯誤訊息。(如圖三)

https://ithelp.ithome.com.tw/upload/images/20191001/20120614Pba59Z0Qy1.png
圖三:變數 a 還未被指派正確的值前,就被使用了

所以如果你強行用 tsc 進行編譯會出現錯誤訊息的提醒。(如圖四)

https://ithelp.ithome.com.tw/upload/images/20191001/20120614Wqv4kh5P50.png
圖四:錯誤訊息跟編輯器內部狀況一模ㄧ樣

但是如果將 strickNullChecks 設定為 false,編譯結果會是成功的。(可以使用 tsc --strickNullChecks false 來編譯檔案)(結果如圖五)

https://ithelp.ithome.com.tw/upload/images/20191001/20120614LS7NMwZNxt.png
圖五:使用 --strickNullChecks false 的結果是,會忽略變數本身為 Nullable Types 的狀態

讀者發現了嗎?

如果將 strickNullChecks 設定為 false,則會讓 Nullable Types 視為任何型別可以出現的一種形式

反過來說,如果 strickNullCheckstrue,則 Nullable Types 會被強行變成一種型別看待,不會隱身在各種型別裡。也因此,Nullable Types 唯有幾個可以被指派到的地方除了是 Nullable Types 以外,就是any 型別

貼心小提示

這是筆者遇到的狀況 —— 就算沒在 tsconfig.json 啟用 strictNullChecks,以筆者當前的環境來說 —— 預設值似乎是 true,但官方文件寫的是 false。(圖六為官方文件的截圖;圖七為沒有動 tsconfig.json 的結果;圖八為 strickNullChecks 刻意被改成 false 的結果)

但筆者懷疑的情況有兩種:

  1. 筆者的環境很不乾淨(如果是這樣,筆者面對讀者會很尷尬,連自己環境都搞不好
  2. TypeScript 官方沒更新

不過筆者傾向於可能是情形 1,筆者自己的環境問題,因為其他的選項都有按照 tsconfig.json 預設的規則來進行。不過這一篇要展示筆者親身測試過的篇章,因此才會稍微繞了幾圈,請讀者見諒。

所以筆者還是要跟讀者講說 —— 記得好好把需要的編譯器選項都開啟;有開啟的話,也就不需要管說會不會有潛藏的雷點。

https://ithelp.ithome.com.tw/upload/images/20191001/20120614IySYfLA13D.png
圖六:strictNullChecks 在官方預設是 false,可是筆者在自己的環境每次測的時候都是預設為 true

https://ithelp.ithome.com.tw/upload/images/20191001/20120614sZwZNnLNLM.png
圖七:明明官方說 strickNullChecks 預設值是 false,但是卻還是有 strickNullChecks 的 Feature

https://ithelp.ithome.com.tw/upload/images/20191001/20120614qooeWT28qX.png
圖八:刻意將 strictNullChecks 關掉,警告訊息就消失了

所以呢~如果讀者還是看不懂或不知道 strictNullChecks 是怎麼一回事,你可以想成:strickNullChecks 啟用的話,nullundefined 會被視為獨立的原始型別,而本系列就是採取 strictNullCheckstrue 的前提下進行解說的!

重點 1. Nullable Types 為獨立原始型別 —— strictNullChecks

strictNullChecks 模式下,會將 Nullable Types 視為獨立的原始型別,因此:

  • 任何被註記為 Nullable Types 的變數只能接收該 Nullable Types
  • Nullable Types 的值除了能夠被指派到有註記到 Nullable Types 的變數外,也可以被指派到 any 型別的變數

2. 禁止隱性的 any 型態 —— noImplicitAny

讀者這下子應該知道《前線維護》篇章的重要性了,還記得 Implicit Any 是指什麼案例嗎?XD

貼心小提示

如果真的忘記的話,趕快看看函式型別篇章補一補!

不過這也代表讀者對型別推論與註記方面的了解還不夠扎實喔,建議把《前線維護》篇章系列好好看過喔!

以下是測試 noImplicitAny 機制的範例程式碼。(也很短沒錯

https://ithelp.ithome.com.tw/upload/images/20191001/20120614BxQ2f0Zo4r.png

其中,Implicit Any 的情境下 —— 意旨在宣告的函式,裡面的參數若沒有進行積極註記,就會被 TypeScript 推論為 any 型別,但也因此使得 TypeScript 沒辦法追蹤函式到最後的輸出型別。因此會出現圖九的警告訊息。

https://ithelp.ithome.com.tw/upload/images/20191001/201206141R5r4fpeMX.png
圖九:Implicit Any 會被 TypeScript 主動警告

貼心小提示(第二彈)

事實上,這邊筆者測試時覺得怪 —— 明明官方 Doc 也是說 noImplicitAny 預設值是 false,但筆者怎麼測預設值都是 true 的狀態,看剛剛的圖九就知道了。

不過筆者還是先歸咎有可能是自己的環境問題導致,所以還是告誡讀者:每一次建立 TypeScript 專案時,記得要好好檢查自己的編譯器 tsconfig.json 設定是不是正確的喔

(以下圖十為官方 Doc 截圖;圖十ㄧ為 noImplicitAny 設定為 false 的狀態;圖十二為 noImplicitAny 設定為 true 的狀態)

https://ithelp.ithome.com.tw/upload/images/20191001/20120614qJy2p2rSPl.png
圖十:官方 Doc 明確將 noImplicitAny 的預設值設定為 false

https://ithelp.ithome.com.tw/upload/images/20191001/20120614uYCFNRfJdS.png
圖十一:這是 noImplicitAnyfalse 的狀態,TS 照樣會關照你,但不會強制叫你改

https://ithelp.ithome.com.tw/upload/images/20191001/201206143XUwr9WJUi.png
圖十二:筆者在自己環境的測試結果 noImplicitAnytrue 的狀態跟平常沒兩樣

重點 2. 隱性 any 型別狀態的預防 —— noImplicitAny

若將 tsconfig.json 裡的 noImplicitAny 啟動時,任何函式的宣告裡 —— 參數若被 TypeScript 判定為 any 型別時,就會警告開發者有潛在的錯誤 —— 在這裡也就是所謂的 Implicit Any 的案例。

3. 型別推論檢測相關設定 —— strict* 系列

筆者認為讀者真的需要這些檢測再去查詢用法 —— 因為筆者認為目前的型別檢測設定事實上是足夠應付普通專案的

以下就列出相關的型別檢測設定:

  • strictFunctionTypes
  • strictBindCallApply
  • strictPropertyInitialization

4. 額外語法與型別推論檢測相關設定 —— no* 系列

同第 3 點所述,筆者依然認為目前的設定足夠,不過讀者會想使用這裡的設定的機率應該會大過於第三點裡的設定。

以下就列出相關的語法檢測設定:

  • noUnusedLocals —— 如果有變數沒有被使用就會出現警告
  • noUnusedParameters —— 如果函式裡的參數沒有被使用,就會發出警告
  • noImplicitReturns —— 如果函式裡有出現路徑是沒有回傳值就會發出警告
  • noFallthroughCasesInSwitch —— 每個 switch 裡面的 case 判斷敘述式一定要有 break 語法,不能有 case 沒有 break 以至於會執行到下一個 case
  • noImplicitThis —— this 如果被 TypeScript 判定為 any 型態時就會發出警告

這些就是筆者會斟酌使用在專案裡面的設定。

筆者也認為這些設定並不太需要認真解說,讀者也可以試試看這些設定的效果。

小結

筆者到這裡可以正式宣布對 TypeScript 編譯器設定的解說告一段落

有些筆者可能會問:“恩... 不是還有實驗性功能相關設定嗎?”

哦~這個嘛~

要等筆者寫到《進化實驗》篇章,也就是第五篇章才會揭曉。(E04,到底要寫到什麼時候?XDDD

下一篇,筆者要回歸講解 TypeScript 更多的功能囉~!敬請期待~

]]> Maxwell Alexius 2019-10-14 14:54:53
Day 33. 戰線擴張・專案除錯 X 源碼對照 - TypeScript Compiler Debug Techniques https://ithelp.ithome.com.tw/articles/10223348?sc=rss.iron https://ithelp.ithome.com.tw/articles/10223348?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

  1. 如何確保出現錯誤時,防止 TypeScript 編譯器產出專案結果?
  2. 描述 rootDiroutDiroutFile 三種設定。
  3. outFile 的使用時有什麼限制?跟 outDir 差別在哪?

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

今天要講一些跟除錯相關的東西。

讀者可能會問:“奇怪,在 VSCode 上面 TypeScript 不是會幫你檢測語法?難道還有其他除錯的方法?”

方法其實超多種,只是有沒有被發現或有沒有用對罷了,想要提升作業效率 —— 除錯技巧也是一門可以被優化的地方。

事實上還有另一個好用的東西叫做 Source Map —— 筆者就不多說,請讀者繼續看下去。

另外,今天也會統整通常在 JS 專案上 Debug 的途徑有哪些~

以下直接正文開始吧~

專案除錯技巧 Project Debug Technique

專案相關設定 Project Related Configurations —— 第三彈

沒錯,我們還沒講完專案相關設定,但這是最後一彈了!XD

簡易環境建置 —— 使用 lite-server

但在我們講今天的專案設定前,筆者必須進行簡單的環境建置動作。

讀者可以進到任何想要測試專案的檔案資料夾位置:首先將 package.json 進行初始化的動作,並且下載一個名為 lite-server 的套件 —— 專門快速 Host 簡單的環境讓 HTML/CSS/JS 檔案協作。

// 進到任何資料夾,也可以臨時 mkdir 創建再進去
$ cd PATH_TO_TEST_DIR

// 初始化 package.json
$ npm init -y

// 下載 lite-server 套件
$ npm install lite-server --save-dev

下載完後,進到 package.json,將 scripts 選項改成:

{
  /* 略... */
  "scripts": {
    "dev": "lite-server"
  },
  /* 略... */
}

因此每一次執行 npm run dev 會啟動 lite-server

圖一是目前打該編輯器應該出現的結果。

https://ithelp.ithome.com.tw/upload/images/20191001/20120614zNmqijV4Bp.png
圖一:初始化專案並下載 lite-server

好的,在當前檔案資料夾,筆者建立簡單的 index.html 檔案並填入一些程式碼。

https://ithelp.ithome.com.tw/upload/images/20191001/20120614adn7UVzymU.png

以下是對 HTML 檔案的說明:

  • 有一個按鈕被綁定 ID 為 click-me-btn,等等會註冊事件更新被按到的次數
  • 有一個 P 元素裡面有 Span 負責紀錄被按到的次數
  • 引入 ./build/index.js 檔案(該檔案目前是還未編譯狀態,請繼續看下去)

另外,初始化 TypeScript 專案、建立 /src/build 資料夾以及新增 index.ts/src 裡:

// 初始化 tsconfig.json
$ tsc --init

// 建立 /src 與 /build
$ mkdir src
$ mkdir build

// 新增 ./src/index.ts
$ touch ./src/index.ts

筆者簡單在 index.ts 填入一些程式碼,負責將該按鈕註冊簡單的事件並更新按鈕被按到的次數。

https://ithelp.ithome.com.tw/upload/images/20191001/20120614DQuKayfrhE.png

不過以上的程式碼,TypeScript 會告訴你 —— $btn$counter 可能為 null。(如圖二)

https://ithelp.ithome.com.tw/upload/images/20191001/20120614jMRESRJO7W.png
圖二:元素在進行查找的時候,可能會找不到

於是有幾種寫法可以解決,一種是直接對那些 document.getElementById 進行積極註記的動作。

https://ithelp.ithome.com.tw/upload/images/20191001/20120614oAdopHYZoC.png

不過筆者傾向於另一種寫法 —— 使用 Type Guard 進行過濾的動作,因為之前在複合型別篇章提過 —— 通常遇到 union 型別,普遍解決方式就是要設定 Type Guard 進行型別限縮。

https://ithelp.ithome.com.tw/upload/images/20191001/20120614SKYyRVqdlk.png

這樣的寫法也比較合理的理由是:如果對 A | null 進行強行註記為 A 這個型別的話,就等同於你忽略了它可能是 null 的機率,造成開發過程中產出 Bug 的風險 —— 通常在這裡會變成 null 的情形,不外乎可能是元素的 ID 打錯字或者是單純 getElementById 裡面的元素 ID 打錯字,所以要讓 TypeScript 自動幫我們監測這些問題的存在。

使用 Type Guard 就會逼迫我們寫出錯誤情形的應變對策(在這裡是直接丟出錯誤訊息),後續出錯就知道問題在哪,馬上可以去查 Bug~!

另外,剛剛解完前面的問題後,TypeScript 還會提醒你 innerText 不能接收 number 型別,必須為 string。(如圖三)

https://ithelp.ithome.com.tw/upload/images/20191001/20120614EfIfs21oxR.png
圖三:TypeScript 的好處就是,它會提醒你正確的使用方法

所以必須將程式碼那一行改成:

https://ithelp.ithome.com.tw/upload/images/20191001/20120614us1KRNHXDJ.png

以下是完整的程式碼:

https://ithelp.ithome.com.tw/upload/images/20191001/20120614k2PHf56wrS.png

讀者試試看

這裡筆者想要問讀者簡單的問題,如果你還記得在本系列一開始提到型別的推論與註記的機制,為何筆者沒有對 index.ts 裡的範例程式碼的某些變數進行積極註記(Annotation)的動作?

這裡要請讀者複習必須要積極註記型別的時機:

  • 什麼時候必須積極註記?
  • 什麼時候則不需要就可以藉由型別系統的推論(Inference)就會自動幫我們監控各種變數或參數型別?
  • 什麼時候需要用到 Type Guard?

最後,在 tsconfig.json 裡修改一下 rootDiroutDir 這兩個選項設定,環境建置就完成了。

{
  "compileOptions": {
    /* 略... */
    "outDir": "./build",
    "rootDir": "./src",
    /* 略... */
  }
}

直接進行編譯檔案的動作並啟動 lite-server

// 編譯檔案
$ tsc

// 執行 lite-server
$ npm run dev

圖四是執行的結果,可以看到每次按按鈕,都會新增計數次數。

https://i.imgur.com/TiEk5CM.gif
圖四:執行結果

通常 Debug 會有幾種方式,以下一個接一個討論。

除錯技巧 1. 使用 debugger 關鍵字

這是 JavaScript 在瀏覽器本來就有的功能,可以在程式碼植入 Debugger 並且查找不同變數狀態。可能讀者在學原生 JS 時早就知道這個技巧,但筆者想要提醒的是 —— TypeScript 也可以使用 debugger

於是,我們將 index.ts 更改一下並重新編譯 —— 使用 tsc

https://ithelp.ithome.com.tw/upload/images/20191001/20120614NLWC2a6uA4.png

當你編譯過後檔案,lite-server 會偵測到 ./build/index.js 改變,自動重新對瀏覽器刷新,因此不必重新整理視窗。

回到剛剛的程式碼,我們是在按鈕註冊事件裡加入 debugger —— 也就是說,每一次按按鈕我們都會暫停 JS 的執行並且可以自由測試。(如圖五)

https://i.imgur.com/Wt2YZ8A.gif
圖五:每一次按到按鈕會在 debugger 註記的地方暫停執行,並且可以檢視此時的變數狀況!

另外,必須注意到的是圖六這個畫面。

https://ithelp.ithome.com.tw/upload/images/20191001/20120614VRCHOTuw7G.png
圖六:這是觸發 debugger 時的畫面

筆者必須將以上畫面得出的資訊下重點:

重點 1. 使用 debugger 關鍵字

debugger 為 JavaScript 本身就擁有的除錯 Feature,當注入 debugger 在程式碼的任何地方,只要瀏覽器執行到就會停在該地方,並自動將瀏覽器的開發者工具跳到原始碼觸發 debugger 的位置。

另外,此時的瀏覽器開發者工具的 Console 裡紀錄的變數狀態會跟 debugger 所在的區域同步 —— 也就是說,你可以在執行的過程檢視任何變數的變化甚至中途進行竄改、呼叫方法執行的動作。

在 TypeScript 檔案裡也可以使用 debugger,編譯過後也可以使用。

貼心小提示

不同的瀏覽器可能會對 debugger 或各種開發者工具提供的 Debug 功能有不同的行為 —— 但行為應該要差不多ㄧ致,就是操作介面的長相可能不ㄧ樣罷了。

以上的範例是使用 Google Chrome 作為瀏覽器進行測試,因此以下的所有範例也都會以同個瀏覽器作示範喔!

除錯技巧 2. 直接在開發者工具部分,標註程式碼執行斷點 Breakpoint

既然知道開發者工具有 Source 這個功能讓我們可以得知原始碼存放位置,當然不僅僅只有 debugger 可以這樣做 —— 我們可以翻閱原始碼直接進行斷點標註,使得被標注的位置,只要被執行到也會跟 debugger 有同樣效果。

因此筆者將剛剛的 index.ts 恢復到原本狀態並重新編譯。

https://ithelp.ithome.com.tw/upload/images/20191001/20120614KhMpqo9nui.png

重新編譯過後,lite-server 會自動刷新頁面。這一次筆者示範如何在程式碼內設定斷點:打開瀏覽器的開發者工具後並且找到 Source 這個 Tab。(如圖七)

https://ithelp.ithome.com.tw/upload/images/20191001/20120614daAwxd6r1c.png
圖七:打開開發者工具進入到 Source 介面,它會自動連結到 index.js 檔案

頁面跟剛剛差不多,但這一次翻閱 index.js 途中,可以對任意一行程式碼標註斷點。(如圖八)

https://ithelp.ithome.com.tw/upload/images/20191001/20120614IFmOpmFis9.png
圖八:在 index.js 裡的 console.log 這一行直接標註

如果筆者直接按了按鈕,ㄧ樣就會在被標注的那一行停止執行喔!(如圖九)

https://i.imgur.com/SqpfIg0.gif
圖九:設定斷點的效果跟 debugger 差不多呢!

重點 2. 翻閱原始碼進行除錯

通常開啟瀏覽器的開發者工具,其中 Source 的部分會存放該網頁從 Server 端取得的靜態資源,可能包含 JS 檔案、圖片檔等等 —— 想要檢閱完整的原始碼可以在這個地方搜尋。

另外,檢閱原始碼的過程中,可以對 JS 檔案標註斷點位置 —— 在執行的過程當中若執行到斷點,會停止執行並且 Console 狀態會鎖定目前的程式碼執行到的變數狀態

除錯技巧 3. 使用 TypeScript 編譯器設定 —— sourceMap

另外,有時候編譯過後的 JS 檔案在複雜的情況下會讓人看不懂,比如*列舉型別被編譯的結果就是ㄧ團噁心的 IIFE 配上物件的屬性互相指派的動作*。(如果讀者還記得列舉型別篇章討論到的東西的話)

如果不希望在檢索 JS 程式碼的過程中看到那一大串冗長的編譯結果,於是 TypeScript 編譯器設定提供一個很好的方式:使用 sourceMap

筆者這邊就把 tsconfig.json 裡,將 sourceMap 選項開啟。

{
  "compilerOptions": {
    /* 略... */
    "sourceMap": true
    /* 略... */  
  }
}

並且經過 TypeScript 編譯結果如圖十。

https://ithelp.ithome.com.tw/upload/images/20191001/20120614jkoz0rzDQ5.png
圖十:經過編譯後會發現,多出了一個 .js.map 檔案

這個 .map 檔案是所謂的 Source Map —— 代表的是連結編譯 TypeScript 檔案後與編譯前的關聯檔

其實任何專案的 minifieduglified 的結果通常也會搭配一個 Source Map,負責連結轉換前的原始碼的樣貌。

也就是說,有了 index.js.map,瀏覽器就會知道原本的 TypeScript 檔案內容會長什麼樣子

因此打開瀏覽器並進到 Source 介面,你會發現多了 src 這個資料夾,存放了 index.ts 檔案。(如圖十一)

https://ithelp.ithome.com.tw/upload/images/20191001/20120614aOZIKNftvI.png
圖十一:Source 介面裡面有一個名為 src/index.ts 的檔案

打開內容後,ㄧ樣可以在 index.ts 設定斷點,我們照樣可以進行 Debug 動作。(設定斷點如圖十二;實際測試如圖十三)

https://ithelp.ithome.com.tw/upload/images/20191001/20120614rbhWAolBQE.png
圖十二:我們也可以在 index.ts 裡面設定斷點呢!

https://i.imgur.com/F7Nv9Sr.gif
圖十三:也可以藉由 TypeScript 的原始檔案進行除錯,sourceMap 實在是很方便呢!

重點 3. TypeScript 編譯器 Source Map 的產出

若將 tsconfig.json 裡的 sourceMap 啟用,每次編譯後的 JS 檔案都會附帶一個 Source Map 檔案。

Source Map 檔案的主要目的是建立編譯前後的連結資訊 —— 因此只要擁有編譯後的檔案與對應的 Source Map 檔案,就可以回推原始檔的模樣。

藉由 Source Map 的建立,可以在開發者工具 —— 不需要檢視編譯過後產生的 JS 檔,可以用原本的 TypeScript 檔案進行除錯

小結

基本上,筆者最近三篇都在講專案相關方面的編譯器設定,這些都是學習 TypeScript 建議必須知道的編譯機制。

不過光是這一篇,為了鋪 sourceMap 設定的梗,實是有些辛苦;但一次把常見的除錯技巧列出來,對未來開發上具有加成的效益。

往後讀者若建構更複雜的專案可能也會需要撥時間研究更多內部的設定資訊,但通常筆者 Cover 到的設定應該是足夠了,其他的可能 —— 通常也會想到將 TypeScript 與 Webpack 結合建立一系列的編譯程序等等。(請等待 Day 38.)

反正,接下來筆者要進到另一個很好用的編譯設定系列 —— 也就是筆者自己認定所謂的語法檢測相關的編譯設定(Syntatic Checks Related Configs.)。

讀者云:“編譯器設定還沒講完啊!?”

對・就是這樣

下一篇再見!

]]> Maxwell Alexius 2019-10-13 15:01:41
Day 32. 戰線擴張・專案輸出 X 輸出設定 - TypeScript Compiler Output Configurations https://ithelp.ithome.com.tw/articles/10222307?sc=rss.iron https://ithelp.ithome.com.tw/articles/10222307?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

  1. 為何有些 ES6 的 Feature 諸如 PromiseObject.assign 等東西無法直接在 TypeScript 使用?要如何設定 tsconfig.json 使得 TypeScript 認得這些東西?
  2. 試舉出 JS 圈裡模組的語法規格(Module Speculation)。

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

哎!筆者其實沒想到前一篇光是講 targetlibmodule 這三個設定就耗費幾千字,這一次只能任命繼續推進度下去。

筆者今天ㄧ樣要繼續講跟專案的產出有關的 TypeScript 編譯器設定,直接進入本題。

正文開始

Build 相關的編譯器設定

專案相關設定 Project Related Configurations —— 第二彈

4. 設定專案的主目錄與專案輸出的位置 - rootDir & outDir

這個應該是不太需要說明,就是專案包含的範圍以及 Build 過後專案的位置。不過呢,筆者還是要強調裡面潛藏的一些機制。

首先,必者給讀者看簡單的 Directory 的架構。(如圖一)

https://ithelp.ithome.com.tw/upload/images/20190930/20120614b9mr2aVCDT.png
圖一:測試的專案架構

簡單解釋架構如下:

  • /build 資料夾,通常就是專案被編譯過後輸出的位置
  • /src 資料夾,通常就是專案主要程式碼所在的位置
    • 裡面包含 index.tsmodule.ts 檔案
    • 裡面還包含 /inner-folder 資料夾,存放另一個 index.ts 檔案

如果使用預設的 tsconfig.json 設定並且經過 tsc 編譯結果如圖二。

https://ithelp.ithome.com.tw/upload/images/20190930/20120614xEiG17mjQo.png
圖二:每個 TypeScript 檔案被編譯過後會產生相對應的檔案

重點 1. 預設的 TypeScript 編譯規則

在終端機當前的所在檔案資料夾位置,下達 tsc 指令,且沒有開啟任何 rootDiroutDiroutFile 相關設定,則:

TypeScript 編譯器會找當前所在檔案資料夾位置(包含資料夾本身)裡面所有 TypeScript 檔案,並一個個編譯出原生 JS 成果,其中 —— 每個原生 JS 檔案會與編譯前對應的 TS 檔案位置一模一樣

讀者應該會對 TypeScript 編譯器的這種行為感到習以為常,但同時感到不便 —— 難道就不能把編譯過後的檔案好好匯集在一個地方嗎

這一次筆者來嘗試看看這種設定:

{
  "compilerOptions": {
    /* 略 ... */
    "outDir": "./build",
    "rootDir": "./src",
    /* 略 ... */
  }
}

下達 tsc 編譯過後的結果如圖三。

https://ithelp.ithome.com.tw/upload/images/20190930/20120614m6B1PW1SWw.png
圖三:原來 outDir 這個選項可以控制檔案輸出位置呢!

不過,這裡有個雷點 —— 就算你違反了專案規則,你照樣還是可以編譯出結果。什麼意思呢?

筆者這裡的 rootDir 選項是指專案的檔案應該要被放置的位置 —— 此時的設定為 ./src;如果假設我們在 ./src 外面還有其他 TypeScript 檔案 —— 儘管違反了專案只能放置在 ./src 的規則,下達 tsc 指令依然會建構出結果 —— 不過筆者依然相信讀者不希望出現如圖四的結果。

https://ithelp.ithome.com.tw/upload/images/20190930/20120614MNhX7xPUkx.png
圖四:儘管專案被編譯出來了,但編譯的結果就是怪,連 src 以外的檔案都被編譯到

另外,儘管 TypeScript 編譯器會提醒你違反規則,但我們嫌還要把編譯結果刪掉很麻煩 —— 出錯時不希望它編譯出任何檔案的話,筆者建議可以多加下一個要介紹的設定,甚至可以在每一次初始化 tsconfig.json 時就把這一個設定加進去

5. 只要專案有任何環節出錯,一概不輸出 —— noEmitOnError

這個就很直覺,只要出現錯誤一概不輸出檔案。(實際測試結果如圖五)

https://ithelp.ithome.com.tw/upload/images/20190930/20120614IY9TRUXw10.png
圖五:noEmitOnError 代表有任何錯誤,TypeScript 編譯器就不會輸出任何東西

另外,除了這種狀況,只要程式方面語法也出現警告,同時 TypeScript 設定檔中有啟動 noEmitOnError 也不會有任何輸出 —— 這部分就可以請讀者驗證看看。

讀者試試看

試著故意在 TypeScript 專案裡面隨便寫一行被警告或者是錯誤的程式碼,只要 noEmitOnError 被啟動,任何呼叫 tsc 進行編譯的結果都會沒有輸出。

另外,還有一個選項單純就是 noEmit,代表就算你呼叫 tsc 而且專案沒有出錯,編譯器照樣不會輸出任何東西。通常這只是單純拿來做編譯測試用的選項,因此筆者這裡大概提過後,讀者可以自行試試看、知道有這種功能就好了。

重點 2. TypeScript 編譯器 noEmitOnError 設定

一但將 noEmitOnError 設定啟用,則當編譯過程中出現任何問題:包含專案設定有誤、語法層面有誤等,TypeScript 編譯結果會一概不輸出檔案。

貼心小提示

另外,其實還有一個名為 rootDirs 這個選項 —— 可以提供複數個主目錄(Root Directories),這個讀者也是知道有這個功能就好,需要的時候再來研究或看官方的 Doc

一但你使用了這項功能,筆者建議你可能還會需要深入理解 TypeScript 的 Module Resolution —— 也就是模組的運作機制,因為專案有多個主目錄的情況下,會比較需要注意多個主目錄被結合在一起,不同模組被 import/export 的一些情形喔!

同理 —— baseUrl 這個設定也屬於 Module Resolution 的範疇 —— 本系列認為(或者是筆者自認為 XD),只要 Cover 到一些必要功能的介紹,因此這裡不會延伸討論 Module Resolution 的機制

6. 打包成單個檔案 —— outFile

這個設定看似很好用,不過它有潛藏的條件:

tsconfig.json 中的 module 模組規範的選項只能為 amdsystem 這兩種

有些人可能想說:“這樣真不方便耶!為何這麼麻煩?”(有些人指的也是筆者本人,不過也可能是筆者見識短淺不 EY)

關於這個問題可以參考這個官方 Issue

這跟模組的規範有關,但筆者不太想要涉獵太多這方面的知識 —— 一方面跟本系列沒有太多關聯,另一方面 —— 儘管想要秉持會用而且理解使用該工具背後的哲學,但關於模組的規範,筆者想過之後仍然找不到細探這方面知識的原因。

貼心小提示

筆者認真想過,除非是模組方面規範的研究者或者是有做跟開發編譯器有關的專案,否則研究不同的模組規範實質上沒什麼太大意義,最後還是送讀者一句話:“用 Webpack 可以解決很多事情 XDDDD”

但筆者還是會繼續講跟打包輸出的部分話題,避免有讀者真的還是會用到。

不過筆者還是貼一下該 Issue 討論裡面的重點片段。(如圖六)

https://ithelp.ithome.com.tw/upload/images/20191001/20120614UXLrib7hbg.png
圖六:打包成單一檔案跟 CommonJS 規範似乎有衝突

筆者先示範如何讓 TypeScript 打包出單一檔案結果。以下先給大家看要示範的檔案結構與內容。(如圖七)

https://ithelp.ithome.com.tw/upload/images/20191001/20120614UGg9Z95J5N.png
圖七:其實就是很陽春的範例

先展示一下不將 module 設定為 amdsystem 模式的情形 —— 將 tsconfig.json 裡的 outFile 設定啟動並設定成 build.js,直接編譯之後,會顯示出錯誤訊息。(如圖八)

https://ithelp.ithome.com.tw/upload/images/20191001/20120614Jd4ZgRK9dP.png

筆者這裡另外示範使用 amd 模式下被編譯後的結果。(如圖九)

https://ithelp.ithome.com.tw/upload/images/20191001/20120614YyfyzwMWuP.png
圖九:amd 模式下被編譯過後的檔案

讀者可能意識到,build.js 沒有被放在 outDir 所指定的位置,那是因為 outFileoutDir 兩個設定沒有同時被 Support。(筆者感到傻眼)請參見這個 StackOverflow 帖子,裡面筆者節選重點部分講。

Unfortunately, TypeScript (at least as of version 3.3) does not support both outDir and outFile simultaneously.

(... 略)

(However to simply generate the bundled output in a different directory, yes, outFile: "subdir/bundle.js" will work just fine.)

也就是說,你如果設定 outFile./build/build.js 是可以的!

回過頭來,想要讓這個檔案可以執行,必須使用一個名為 RequireJS 的 Module Loader。如果強行用 node 去執行檔案一定會出錯。(如圖十)

https://ithelp.ithome.com.tw/upload/images/20191001/20120614lx0eGhiBiA.png
圖十:define is not defined,連 NodeJS 也都繞口令嗎?

貼心小提示

讀者可能覺得筆者會開始講如何在 TypeScript 使用 RequireJS 執行 AMD 標準下打包出來的結果 —— 筆者會講到的!

不過必須要等筆者講到 TypeScript Namespaces 才會討論如何執行打包過後的檔案。

重點 2. 輸出專案相關設定 Output Related Configurations

  • rootDir 代表的是專案的檔案必須存放的地方,如果超出 rootDir 指定的範圍,並且沒有啟用 noEmitOnError 選項 —— TypeScript 編譯器照樣會編譯出結果,但也會拋出警告訊息提醒開發者超出 rootDir 規範的範圍

  • outDir 則是指定專案被編譯過後,輸出之結果存放的地方 —— 其中,若專案有樹狀結構資料夾分佈,則會按照該樹狀結構編譯出結果

  • outFile 則是將專案打包成單一檔案,但限制是 module 選項必須為 amdsystem 模式 —— 因為跟模組的規範有關

    • 另外,outFileoutDir 這兩個是完全不相干的設定,所以 outFile 不會在 outDir 指定的資料夾產出結果
    • 若同時出現 outFileoutDir 選項,TypeScript 編譯器裝作沒看見 outDir 這個設定

貼心小提示

儘管更動的機率 —− 筆者認為很小,不過也不排除可能未來版本的 TypeScript 設定檔會改變 outFileoutDir 的行為,但那也可能是跨越大版號才可能出現的結果吧。

小結

筆者認為很雜的內容莫過於 TypeScript 設定檔,一是太多設定讓人覺得不親切、二是專案的輸出/打包應用情境實在多到炸、三是筆者深怕漏掉一些好用的編譯器設定功能,沒有交代給讀者

下一篇筆者會繼續介紹另一些比較實用的 —— TypeScript 編譯器相關 Debug 技巧與功能介紹。

]]> Maxwell Alexius 2019-10-12 20:07:30
Day 31. 戰線擴張・專案監控 X 編譯設定 - TypeScript Compiler Compile Configurations https://ithelp.ithome.com.tw/articles/10222025?sc=rss.iron https://ithelp.ithome.com.tw/articles/10222025?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

恩... 照常 Day 31. 繼續。

《戰線擴張》篇章概要

本系列進入到第三部分:《戰線擴張》篇(The Front Line Expansion)

筆者就開始進行算是專屬於本系列的延長賽篇章,ㄧ樣會帶領讀者遍歷 TypeScript 各種很有趣的功能與應用~

以下大概筆者粗估會在本篇章提到的內容:

  • 使用 tsconfig.jsontsc 指令更多的功能
  • TypeScript Namespaces
  • 使用 RequireJS 執行不同編譯模式下的檔案
  • 引入第三方套件協作 與 Definition File

看起來很少對不對~然而,筆者難免會延伸出東西,照樣不太確定會出多少篇章。(原本預估五、六篇左右應該就可以結束掉這個篇章?不過可能會寫到十篇也不知道~那就算了~寫就寫吧~

貼心小提示

就在前幾天,筆者確定第三篇章總共會有 11 篇,延伸出來的東西還有:

  • Webpack 環境的建立
  • 外觀模式 Façade Pattern

另外,筆者脫稿演出的機會很大(它 X 是有多喜歡寫?),不一定會按照以上的順序講解,反而是漸進式的介紹功能 —— 介紹到哪就往哪裡跑的概念。

那麼我們就正式進入《戰線擴張》系列~

正文開始

熟悉 TypeScript 編譯器的設定

重溫編譯器的設定檔 TypeScript Configuration File tsconfig.json Revisited

讀者應該看過本系列後,每一次建構簡單的小專案時,就會需要呼叫:

$ tsc --init

它就會幫我們建立一個名為 tsconfig.json 的檔案。其中,筆者就把 tsconfig.json 大致上長的樣子截下來給大家看。

https://ithelp.ithome.com.tw/upload/images/20190928/20120614c8wrGOtRfI.png

讀者一定覺得:“tsconfig.json 密密麻麻的,筆者該不會是要全講?”

當然是不太可能,但筆者會特別挑選必須具備的編譯器選項功能,其他可以參考 TypeScript 官方的文件:Compiler Options —— 對於編譯器的任何設定寫得非常詳盡,筆者認為本身就很足夠。

另外,還有這一篇 —— 【Day 03】 TypeScript 編譯設定 - tsconfig.json by Kira 大大 已經將常見的 tsconfig.json 裡的 compilerOptions 甚至是也有把筆者沒有寫到關於 tsconfig.json 的東西也補足了,特地推薦給讀者服用。

本系列筆者應該只會講自己會用到的東西,因此筆者將所有的編譯器設定粗略地分成四類:

  1. 專案相關設定(Project Related Config.):跟專案的編譯流程、版本以及除錯等等相關的設定
  2. 語法檢測相關設定(Syntatic Checks Related Config.):跟進階的 TypeScript 語法偵測相關的設定
  3. 實驗性功能設定(Experimental Feature Related Config.):與 TypeScript 實驗性語法相關的設定
  4. 其他(Other Config.):筆者認為的雜項,沒意外應該不會講到(因為太雜,你也不需要一下子就鑽所有的專案設定

貼心小提示

這只是筆者為了學習方便而自己定的分類方式,事實上在 tsconfig.json 裡的註解部分就有更進階的分類,但筆者在此不贅述 —— 本系列目標就是:能夠理解靈活應用就好了,不需要背一些沒用到的東西,除非自己未來要用時,再查就 OK 囉。

專案相關設定 Project Related Configurations

1. 編譯語言版本 —— target 設定

筆者首先要介紹的是 target 這個屬性 —— 代表 TypeScript 專案要被編譯到的目標 JS 版本,包含 ES3ES5ES6(或 ES2015)以及 ES2016 以上。

其中,官方預設的編譯結果為 ES3 版本,至於 ES5ES3 版本的差別,可以參考這篇 StackOverflow,筆者實際上也不太清楚差別,但鑿於本人真的覺得沒必要理解(除非是興趣使然吧),於是就跳過了。

話說,如果讀者想要用 tsc 並且註明要編譯的設定,格式如下:

$ tsc --<config-1> <config-1-value> --<config-2> <config-2-value> // ...

target 屬性為例,可以這樣使用:

$ tsc --target ES5

以下筆者編譯出不同版本的程式碼給讀者看看。(es5 編譯結果如圖二;es6 編譯結果如圖三)

https://ithelp.ithome.com.tw/upload/images/20190928/20120614D4JRwJHFr4.png

https://ithelp.ithome.com.tw/upload/images/20190928/20120614hI2Qc8iShs.png
圖二:以上是 es5 編譯之結果,有興趣的讀者可以試試看使用 es3 版本編譯結果 —— 會跟 es5 結果差不多ㄧ樣

https://ithelp.ithome.com.tw/upload/images/20190928/20120614d7iJ3Nns9q.png
圖三:es6 版本的編譯結果,有編譯沒編譯差不了多少的感覺

讀者也可以發現,TypeScript 經過編譯成原生的 JS 時,會把型別系統的註記拔除。(這句應該是廢話,不過筆者姑且提醒一下

2. 引入功能層面支援 —— lib 設定

有些初期使用 TypeScript 的讀者會發現一件很莫名其妙的事情。(以下程式碼的警告訊息如圖四)

https://ithelp.ithome.com.tw/upload/images/20190928/20120614msVlvLmUyP.png

https://ithelp.ithome.com.tw/upload/images/20190929/20120614v2golt6zp9.png
圖四:Object.assign 竟然不能用?

首先,Object.assign 屬於 ECMAScript 2015(ES6)的語法之一。(如圖五)

https://ithelp.ithome.com.tw/upload/images/20190928/20120614nlzRfrremm.png
圖五:Object.assign 為 ES6 的語法,可以參考 caniuse.com 這個網站查詢

所以這時候 lib 設定就派上用場啦!如果讀者去查詢官方的 Compiler Options,會發現 lib 可以接收的值特別多。(如圖六)

https://ithelp.ithome.com.tw/upload/images/20190928/20120614jBR6Td8vlp.png
圖六:筆者的電腦螢幕太小,整個畫面沒辦法截,由此可知 lib 裡面選項多到炸

我們必須告訴 TypeScript 專案 —— 我們需要 ES6 的 Feature,於是在 tsconfig.json 裡面,lib 選項更改為 ["es2015"],然後錯誤訊息就會不見。(更改前如圖七;更改後如圖八)

https://ithelp.ithome.com.tw/upload/images/20190928/20120614sNb7PYstGb.png
圖七:讀者仔細看,沒有將 "lib" 選項開啟,Object.assign 部分會出現警告訊息

https://ithelp.ithome.com.tw/upload/images/20190928/20120614bIgiTPIP0q.png
圖八:新增 "lib": ["es2015"] 後,Object.assign 的警告訊息立馬消失

筆者再舉另外一個常見案例。首先我們先讓 "lib" 選項被註解掉 —— 其中,window 這個瀏覽器很常見的物件是可以被使用的。(如圖九)

https://ithelp.ithome.com.tw/upload/images/20190928/20120614bnej3Duj2I.png
圖九:window 物件可以被 TypeScript 認得

由於剛剛為了使 TypeScript 專案支援 ES6 的語法,因此我們必須將 "lib" 設定為 ["es2015"] 才能使用;然而,如果真的使 "lib" 開啟並設定為 ["es2015"] 的話,又會發生錯誤!(如圖十)

https://ithelp.ithome.com.tw/upload/images/20190928/201206146ka1gwnAsP.png
圖十:window 被 TypeScript 視為 any,根本不存在

什麼!?剛剛 TypeScript 不是認得 window 這個東西嗎

其實,window 的預設型別定義部分是被放在名為 dom 的選項,也是屬於 lib 設定當中的其中一項。所以如果改成,"lib": ["es2015", "dom"] —— window 物件又會再度被 TypeScript Recognized!(圖十一)

https://ithelp.ithome.com.tw/upload/images/20190928/20120614zDnGXs5wXl.png
圖十一:原來 window 物件是由 dom 選項控制的啊

其中,lib 選項提供的功能,就是負責補足 —— 在 TypeScript 裡所謂的 Declaration Group —— 宣告性群組的東西。

讀者可以理解成:dom 裡面含有的是 windowdocument 那一類的宣告性註記;如果 lib 裡面的設定包含 dom 這個宣告性群組,它會在專案裡幫你自動宣告好:

https://ithelp.ithome.com.tw/upload/images/20190928/20120614w3GQqnC1kX.png

Declaration Group 或者是宣告性註記的意義在於 —— 將一些預設就有的 JavaScript 物件,比如:瀏覽器裡的 windowdocument 物件;NodeJS 裡的 global;ECMAScript 裡的 Promise 物件等等,提前告訴 TypeScript 它們的型別架構為何。否則 TypeScript 會視這些沒有經由提前的型別宣告性的群組的任意變數為 any 型別。

貼心小提示

declare 關鍵字會在後續篇章提及 —— 這會跟 Declaration File 相關的內容一起講解~

這裡讀者可能會問的問題是:

“像是 letconst 或一些 Arrow Function —— 明明都是 ES6 以後的語法,那為何不用 lib 裡設定 es2015 也可以動作呢?”

筆者的推測是這樣:

TypeScript 是可以接受任何語法層面(Syntatic Aspect)相關的東西

  • let & const
  • 解構式 Destructuring
  • 箭頭函式 Arrow Function
  • 類別宣告 Class Declaration
  • 屬性導出 Computed Properties
  • 匯集-散布運算子 Rest-Spread Operators
  • 迭代器產生函式的語法 Generators
  • 更多 ES6 以後的語法等

但是像 PromiseSymbolArray 的擴充方法(例如:Array.prototype.findIndex),這些都是屬於可以經過 Polyfill 就出來的東西 —— 並不是語法,而是功能層面(Utility Aspect)的擴展 —— 也就是說,這些 ES6 以後所擴展的各種型別功能都會群聚在 lib 設定裡的 es2015 選項部分,但 TypeScript 本身就有涵蓋某部分的 ECMAScript 語法標準(根據 Compatibility Table 而定)!

以下筆者就進行簡單驗證。(圖十二~圖十四)

https://ithelp.ithome.com.tw/upload/images/20190929/20120614sKuk8l8ZCA.png
圖十二:語法層面上,不需要引入 es2015 的宣告性群組就可以使用!

https://ithelp.ithome.com.tw/upload/images/20190928/20120614H8RweUk2vR.png
圖十三:功能層面上,沒有引入 es2015 宣告性群組會無效!

https://ithelp.ithome.com.tw/upload/images/20190928/201206140sepYgOTkA.png
圖十四:功能層面上,必須引入 es2015 作為編譯器 lib 設定之一,使得這些 ES6 延伸出來的功能性的物件能夠被使用

以上的驗證確認成立!因此筆者正式向讀者宣告:

TypeScript 本身內建語法層面(Syntatic Aspect)相關的東西,因此 ECMAScript 規定的語法層面的東西大部分可以使用(參照 Compatibility Table

如果在功能層面上沒有引入 es2015 的宣告性群組,TypeScript 會出現貼心的提示喔!(如圖十五)

https://ithelp.ithome.com.tw/upload/images/20190928/20120614h15f4s5krh.png
圖十五:TypeScript 主動告訴你 Promise 本身是一個型別,但被誤用成一個物件,會建議你將 es2015 的選項加入編譯器裡的 lib 設定喔!

另外要注意一件事情:因為 Generators 的語法回傳值會產生類似 Iterator 的物件,而 Iterator 本身是一種物件,隸屬於功能層面的擴展 —— 因此推得就算可以使用 Generator 語法,如果在 lib 裡面沒有新增 es2015 選項,TypeScript 依然會跟你作對。(圖十六與圖十七)

https://ithelp.ithome.com.tw/upload/images/20190929/20120614sOWihQQlJo.png
圖十六:就算 Generator Function 屬於語法層面,但因為該函式會回傳一個類似 Iterator 介面的物件,因此牽扯到了功能層面,所以 TypeScript 會跟你作對呢!

https://ithelp.ithome.com.tw/upload/images/20190929/20120614E5W8M12mzI.png
圖十七:將 es2015 加入了 lib 選項就變正常了~(真是說變就變)

貼心小提示

本篇章結尾時,筆者會為這兩個名詞:語法層面功能層面進行慎重定義的動作

3. 編譯出不同的模組系統 —— module

筆者認為, JavaScript 這個領域出現的模組系統規範實在是有夠多有夠麻煩,可能是筆者見識不廣,但是模組系統可以分成這麼多種,筆者實在是覺得很崩潰。(筆者的吶喊)筆者大致上是參考這篇文章的敘述 —— 以下的程式碼範例就是節選自它。

  1. CommonJS 規範:特點是使用 requireexports 這兩種關鍵字

https://ithelp.ithome.com.tw/upload/images/20190929/20120614Fos1DuT06k.png

  1. NodeJS 實踐 CommonJS 規範:跟 CommonJS 差別在於 —— NodeJS 是 module.exports 作為 exports 相關功能,但又比 CommonJS 的規則稍微複雜些;通常 NodeJS 的開發者應該會習慣看到這樣的語法。

https://ithelp.ithome.com.tw/upload/images/20190929/20120614BcgxSsQTKh.png

  1. 非同步模組定義檔 Asynchronous Module Definition (AMD):由 CommonJS 延伸出來的,用的是 define 關鍵字;如其名,在瀏覽器載入模組時是非同步載入,而 RequireJS 就是對於 AMD 的實踐。

https://ithelp.ithome.com.tw/upload/images/20190929/20120614Fz9fqOa5Cu.png

  1. ES6 模組載入/輸出語法(ECMAScript Import & Export):大致上寫過各種前端東西的讀者應該熟悉這種模式。

https://ithelp.ithome.com.tw/upload/images/20190929/20120614wqj9ubpX6k.png

以上是常見的模組系統的語法規範與特徵。(其實還有 UMDSystem 更多方式,但筆者認為讀者有用到再去特別查詢就可以囉...)

貼心小提示

通常 amdsystem 那一類選項被編譯過後的成果,需要用特定的 Module Loader 執行。

AMD 的標準可以用 RequireJS;而 system 設定下編譯結果可以用 SystemJS 去執行

然後筆者直接示範,兩種不同 module 的設定下 —— TypeScript 編譯的結果。(圖十八與十九為 commonjs 規範下的編譯成果;圖二十則是 es6 語法規範)

https://ithelp.ithome.com.tw/upload/images/20190929/20120614ADpBhovrpM.png
圖十八:可以看到 commonjs 的編譯成果

https://ithelp.ithome.com.tw/upload/images/20190929/20120614frx9SwrEdr.png
圖十九:commonjs 模式下的編譯結果,仔細一看有這麼一段 —— require('./module')

https://ithelp.ithome.com.tw/upload/images/20190929/20120614pRpk0EkcXJ.png
圖二十:結果使用 es6 模式編譯過後,有編譯沒編譯好像都沒差?

事實上,es6 模式並不是編譯過後沒差,原因是我們還未設定將所有檔案進行打包(Bundle)成一個檔案的動作。所以編譯結果事實上會有兩個檔案,一個是 index.js、另一個是 module.js。(如圖二十一)

https://ithelp.ithome.com.tw/upload/images/20190929/20120614t9KOZFMcnT.png
圖二十一:事實上筆者還沒有將專案設定成打包單一檔案的模式

貼心小提示

打包檔案的設定會在下一篇討論 —— 但是打包檔案並且執行就可能要等到使用 RequireJS 篇章再說。

然而,讀者若覺得自己並沒有需要打包成單一檔案自行執行,而是選擇使用 Webpack 的話,可以參考網路上的資源外,筆者也會有一篇專門在講解 Webpack + TypeScript 環境建構。

所以第三篇章基本上在環境建構或者是編譯器設定部分算是供讀者自由學習的概念 —— 但是講到 Namespace 以及模擬戰時,就比較算是重頭戲囉。

所以 TypeScript 預設的編譯模式是 —— 你有幾個檔案,就會編譯成幾個檔案的概念

重點 1. tsconfig.js 專案相關

  1. target 選項:主要目標是設定專案被編譯過後的語言版本,預設值為 ES3 版本(官方指定的);可以選擇其他版本諸如:ES5ES6(或 es2015 以上)甚至還有 ESNext

  2. lib 選項:可以指定要載入的宣告性群組,通常是指功能層面(Utility Aspect)的宣告,不是語法層面。選項眾多,常見的是 domes2015 等等選項。

  3. module 選項:代表被編譯過後,應該要採用的模組語法之規範,比如 commonjsamdes6

重點 2. 語法層面與功能層面的差異

語法層面(Syntatic Aspect)泛指單純語法解析上的特點,通常語法是自然內建在編譯器的

功能層面(Utility Aspect)泛指可以藉由語法實踐的功能,並非內建在編譯器,通常會以函式庫(Library)的方式載入,或以 ECMAScript 的觀點來看 —— 以 Polyfill 的方式載入功能。

而 TypeScript 編譯器裡的 lib 選項就是對於 TypeScript 專案的功能層面的擴充,比如可以使用 ES6 規範的 Promise 或更多 ES6 以後的特殊 Array 相關的成員方法

小結

筆者眼看字數已經破萬,決定先將本章 End 到這裡,但是筆者還沒把專案設定部分完全 Cover 啊

因此下一篇也會是跟專案編譯有關的設定~

不過像是 libmodule 這兩個設定,光是要分析就可以講出很多概念了,而這通常也是初學 TypeScript 的人可能稍微碰過但沒有仔細理解內在機制的部分,然而如果可以對編譯過程與設定瞭若指掌的話,只會對管理任何專案或客製化編譯流程有加分的作用呢!

]]> Maxwell Alexius 2019-10-11 15:28:00
Day 30. 機動藍圖・流言終結者 X 重新認識物件的複合 - Favour Object Composition Over Class Inheritance https://ithelp.ithome.com.tw/articles/10221357?sc=rss.iron https://ithelp.ithome.com.tw/articles/10221357?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

  1. 已經熟悉類別的運作流程並懂得 OOP 的基礎概念。
  2. 熟悉了策略模式(Strategy Pattern)嗎?

如果還不清楚的話,可以看筆者講述關於策略模式的篇章喔!

鐵人賽第 30 天,筆者有話想說~(筆者挺廢話的

筆者首先恭喜讀者:

在這 30 天,你已經學會了 ... 應該算四成左右的 TypeScript XDDDD(笑 P 笑

(讀者拔出手中球棒準備往作者頭上揮去)

慢著!筆者還沒說完 —— 預估可能還要 20 40 天以上才能完整補完筆者當初在本系列開端承諾的事項,哎呀...

之所以會需要更多時間的用意在於:

  1. 筆者設定範圍太大(這算筆者活該
  2. 筆者希望本系列能夠從頭到尾把讀者的 TypeScript 基礎打好,而所謂的基礎被筆者定義為

能夠靈活運用各種 TypeScript 的 Feature,不需要依靠框架也可以寫出不錯的程式碼

但請不要誤會,不是說框架這個東西不好,有時候用對工具或者是追求時效就是要基於熱心的社群幫你開發出的工具進行專案的開發。而筆者想要強調的點是 —— 如果你可以不需經由框架也能夠寫出有架構的程式碼,相信就算學習或跳槽到任何類似的程式語言,應用起來都會非常的輕鬆。

本來可以在第 20 幾天的時候就開始進入《戰線擴張》系列,但是類別可以講的應用很多 —— 對於只有 JavaScript 開發經驗而沒有實際的 OOP 經驗的讀者(其實也包含筆者本人),類別的概念會顯得有些不知道如何應用。(簡而言之就是要開天眼

接下來,筆者將為《機動藍圖》篇章系列劃下美好的句點:就從重新認識 Favour Object Composition Over Class Inheritance 這個概念~

正文開始

流言終結者・重新認識物件複合

看過本系列的讀者們,其實你早就會 Object Composition 的概念

貼心小提示

本篇對於有 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(也就是物件複合)的目的,與使用介面的理由很像:都是要降低類別物件之間的耦合度,不過實作方式跟介面有差別

  • 介面是針對規格上的宣告,而沒有直接實踐功能,不同的類別可以實踐多種不同介面,同時降低耦合度
  • 物件複合則是讓類別用不ㄧ樣的物件關聯方式,達到不需要使用類別繼承也可以降低耦合度

今天筆者想要講述的重點在於:

  1. 什麼是物件複合 Object Composition?如何地降低物件功能間的耦合度?
  2. 到底是被筆者說成怎樣?為何被(國外的)JS 圈的人曲解得很嚴重?

錯誤的案例(請讀者禁止嘗試這節的程式碼)

讀者可以上網打關鍵字搜尋:JavaScript Object Composition 諸如此類的關鍵字(結果如圖一):

https://ithelp.ithome.com.tw/upload/images/20190928/2012061453EB3GE5jU.png

筆者跟你保證,就算是放在 Medium 的文章 —— 這些藉由 JavaScript 闡述 Object Composition 的觀念完全錯誤。

再三強調,大部分找到闡述 JavaScript Object Composition 相關的主題文章對於這句話的描述:

Favour Object Composition over Class Inheritance

以上這句話是對的!

但是那些文章:

錯得非常離譜!

曲解得非常離譜!

是物件複合的概念錯誤的實踐!

本篇不特別針對是哪一篇文章 —— 讀者甚至隨便挑剛剛筆者搜尋的結果任何一篇都會差不多,請自行判斷。筆者就直接放其中一篇文章的案例:

https://ithelp.ithome.com.tw/upload/images/20190928/20120614oYM0qtxcdx.png

以上的程式碼是一種 Anti-Pattern 的完美實踐 —— 請讀者不要亂學。(除非你想煮出美味的義大利麵

首先,該範例有兩個主角:

  • Fighter 格鬥士
  • Mage 法術士

格鬥士分別擁有 namehealthstamina(代表耐力)三種屬性狀態;法術士則是擁有 namehealthmana(魔力值)。

另外程式碼有額外定義兩個模組 —— canFightcanCast,可以為 FighterMage 裡面的物件進行擴展的動作,所以才會有 Object.assign 那一行:

https://ithelp.ithome.com.tw/upload/images/20190928/20120614Va3S4AvwwO.png

這些網路上文章寫的以上範例程式碼,它們主張的是:Object Composition 的概念就是物件組合的概念。因此才會將 Fighter 物件的狀態 statecanFight 組合起來;也就是說,它們將 Object Composition 想成是 JSON 物件組來組去的概念。

讀者可以去每個文章看一下,但它們得出的共通結論就是:我們應該選擇使用物件組合的概念勝過於類別繼承。(Favour Object Composition over Class Inheritance)

會不會有點跳 Tone? XD

以下筆者一一分析為何這句話是 BS:

網路上的謠言

Object Composition 的概念就是物件組合的概念 —— 也就是說,Object Composition 可以想成是 JSON 物件組來組去的概念

推翻理論步驟 1. 找出盲點

接下來是筆者對於這些文章闡述的概念提出裡面的盲點。

首先,如果將剛剛的 FighterMage 類比成類別的話:

https://ithelp.ithome.com.tw/upload/images/20190928/20120614fnKUEWuaRm.png

由於在該範例的程式碼,為了將那些文章所謂的物件組合起來而進行 Object.assign 的動作,用以下這種方式也同樣可以達成:

https://ithelp.ithome.com.tw/upload/images/20190928/201206148lvpWg7bdL.png

直接使用物件本身的狀態 state 再被 Object.assign 代入更多屬性 —— 這個行為不就等同於直接繼承該物件的狀態嗎?然而那些文章並非用類別的語法,而是單純物件組來組去的語法,但行為跟類別繼承的模式完全沒兩樣,因為是物件直接跟其他物件進行相依賴,直接從其他物件組出新狀態而已

所以這句話是講假的嗎?

Favour Object Composition over Class Inheritance?

推翻理論步驟 2. 重新闡述物件複合的定義 Definition of Object Composition

為了重新認識 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."

筆者翻譯:物件的委任可以達到與類別繼承相當的功能,使得不同類別元件也可以被重複利用。兩個物件之間的關係中,若主要的物件需要執行的某項需求是需要另一個物件提供的功能,則主物件可以將這個需求遞給委任的物件進行處理

聽起來很模糊,但筆者就直接破題:委任的概念跟策略模式篇章採取的作風很像。

但更精確的說法是:策略模式在進行策略替換時的那個參考點,該參考點連結到的不同之策略 —— 那些策略就是被委任的物件

首先,我們回歸策略模式篇章的範例,以下筆者先把架構圖貼上來:

https://ithelp.ithome.com.tw/upload/images/20190928/20120614SJNe9BEwKJ.png

角色 Character 在這裡是主要的物件,其中想要實踐 Attack 的功能,與其直接用類別的繼承,不如設置一個參考點 —— 連結不同的 Attack 介面底下所綁定的不同的攻擊策略 —— 而那些攻擊策略就是被 Character 所委任(delegate)的對象

https://ithelp.ithome.com.tw/upload/images/20190928/20120614UPNambTyVw.png

以下的程式碼就是 Attack 策略是如何被實踐出來的:

https://ithelp.ithome.com.tw/upload/images/20190928/20120614VqaIj9hqLU.png

而這個藉由物件委任其他物件的行為,建立起物件之間的連結 —— 這才是物件複合(Object Composition)的真諦

筆者再舉一個簡單的例子,比如說我們有一個視窗類別 MyWindow

https://ithelp.ithome.com.tw/upload/images/20190928/20120614dvMRT4404f.png

它可以被這樣使用(如圖二):

https://ithelp.ithome.com.tw/upload/images/20190928/20120614c1bIaB8XUf.png
圖二:印出 myWindow 的面積與周長

假設,視窗可能分別是圓形或長方形,最粗糙的手法是 —— 分別宣告 RectangleCircle 並且綁定同一種介面,也就是 Geometry

https://ithelp.ithome.com.tw/upload/images/20190928/20120614Z4iR1DlGYA.png

然後再進行繼承的動作,變成 RectangularWindowCircularWindow

https://ithelp.ithome.com.tw/upload/images/20190928/20120614IXXu8TkWw2.png

可是這裡就出現問題 —— 要如何能夠只宣告一個 MyWindow 類別,但又能同時指定為 Rectangle 或者是 Circle 形狀的視窗呢?

另外,就算指定了幾何形狀,因為長方形跟圓形計算面積或周長的過程也都不ㄧ樣 —— 一個是需要長度 width 與寬度 height;另一個則是半徑 radius 還有 Circle.PI 這個靜態成員。

所以不能直接運用類別繼承的概念,但是可以使用物件複合的技巧 —— 也就是物件的委任(Delegation)。

首先,我們將 MyWindow 設立一個委任物件的參考點 dimension —— 再藉由該參考點,將 areacircumference 的計算請求遞給委任的參考點:

https://ithelp.ithome.com.tw/upload/images/20190928/20120614sSFr1kGnA1.png

這樣我們就可以直接測試這段程式碼。(結果如圖三)

https://ithelp.ithome.com.tw/upload/images/20190928/20120614S0uNcvTXjd.png
圖三:是不是通順很多?

而且物件委任(也就是物件複合的根本)的概念本身就可以作為策略模式的基礎,因此筆者才會在開篇章時直接篤定跟讀者講 —— 如果看過本系列的策略模式篇章過後,你早已會物件複合的概念。

推翻理論步驟 3. 錯誤分析 Mistake Analysis

筆者就要指出剛剛在推翻理論的步驟 1 提出的範例程式碼 —— 錯的或者是不太建議使用的點。

錯就錯在一開始 —— 介面設計本身就很有問題

回歸 FighterMage,設計遊戲角色時,試著想想看,我們會將角色的每個屬性刻意訂得不一樣嗎? —— Figher 擁有 stamina,而 Mage 則是對應 mana

解法一:統一規範 Character 介面,每個角色皆擁有 staminamana 屬性,但數值可以客製化。

interface Character {
  name: string;
  heath: number;
  mana: number;
  stamina: number;
}

解法二:

  • 統一規範 Character,但是設置一個名為 characterStatRef 做為參考點,另外定義 Stat 介面並延伸出 FighterStatMageStat,然後進行物件的委任(Delegation)
  • 不要直接對內部修改屬性,而是統一 attack 介面,再分別定義不同的策略,例如 DirectAttackCastingAttack

https://ithelp.ithome.com.tw/upload/images/20190928/20120614ar8P8McF3z.png

解法二是根據原著的範例程式碼看似想要朝向的方向進行實作,想當然要能夠達到那種層級的彈性,可不是只有所謂JSON 物件組來組去就能夠寫出好的設計

遇到衝突無法解決 —— 使用 Object.assign 進行繼承物件的狀態

首先,如果假設我們還多了一個 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 —— 就算你沒有用類別,也可以利用策略模式實踐出不同的攻擊策略

https://ithelp.ithome.com.tw/upload/images/20190928/201206144QdOEsuLOw.png

推翻理論步驟 4. 流言終結 Myth Bustered!

藉由剛剛一連串的推論與分析過程,我們得知:

大部分在網路上跟 JS 相關闡述的 Object Composition 的概念事實上跟類別繼承的本質沒兩樣。

筆者可以宣告那些文章闡述的 Object Composition 的概念完全是錯誤的。

https://ithelp.ithome.com.tw/upload/images/20190928/20120614LpWSIYb3hD.png

正確的概念如下:

重點 2. Favour Object Composition Over Class Inheritance

Object Composition 的觀念是藉由物件的委任(Delegation)進行物件與物件間的連結

主要的物件若需要執行特定的功能,可以將執行的步驟遞給委任的物件代理執行

Object Composition 的優勢大過於類別繼承的原因主要有:

  1. 彈性較高,關係是建立在抽象的基礎之上,而非實體繼承
  2. 隨時可以替換同質性的委任物件,只要委任物件都遵守同一個介面的規格
  3. 委任的物件可以被重複使用 —— 任何其他主要物件需要委任物件的功能,只要開出一個參考點連結到委任物件,並且將功能傳遞給委任物件就好了
  4. 大部分的設計模式都是基於 Object Composition 進行延伸
  5. 委任物件的模式就是讀者可能聽過的 Dependency Injection (依賴注入) 的實踐,可以避免物件與物件間的相依性過高造成難以功能分離與重複使用的狀況

小結

儘管鐵人賽的基本標準已經過了,但本系列文的目標還未達成~

《機動藍圖》篇章系列 —— 至少從 TypeScript 介面到類別這樣的介紹過程,筆者認為基礎的闡述 —— 一個里程碑的抵達!

然而 TypeScript 的征途還未結束呢~筆者根據自己打過的草稿,我們才走完 ... 還不到一半的歷程。(

不過讀者當然可以自由選擇要學習到哪個程度,畢竟寫軟體這種行業求的不是 100% 完美的程式碼,而是會寫、會用,至少還會改進程式碼的品質就已經是合格了。

下一篇總算要推播本系列的第三篇章 ——《戰線擴張》。

新的篇章的篇幅不會到《機動藍圖》那麼誇張 XD,但是對要和實際專案進行銜接是很重要的篇章呢!

]]> Maxwell Alexius 2019-10-10 14:41:33
Day 29. 機動藍圖・工廠模式 X 抽象工廠 - Factory Method & Abstract Factory Pattern Using TypeScript https://ithelp.ithome.com.tw/articles/10221353?sc=rss.iron https://ithelp.ithome.com.tw/articles/10221353?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

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

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

本篇原本也不在筆者的計畫範圍內,然而由於前一篇已經介紹了抽象類別(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)的概念流言 —— 又是很多天兵在網路上不經查證就散播亂七八糟的觀念導致越來越多很奇怪的程式碼出現。

]]> Maxwell Alexius 2019-10-09 15:24:51
Day 28. 機動藍圖・抽象類別 X 藍圖基底 - TypeScript Abstract Class https://ithelp.ithome.com.tw/articles/10219198?sc=rss.iron https://ithelp.ithome.com.tw/articles/10219198?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

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

  • 類別與物件的差別 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 類別的最後一部分的語法交代完畢~

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

]]> Maxwell Alexius 2019-10-08 15:09:47
Day 27. 機動藍圖・策略模式 X 臨機應變 - Strategy Pattern Using TypeScript. II https://ithelp.ithome.com.tw/articles/10220710?sc=rss.iron https://ithelp.ithome.com.tw/articles/10220710?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

大致上理解策略模式以及應用類別與介面進行實踐。

另外本篇會延續上一篇的範例,因此沒有看過可以先翻看前一篇的文章喔!

廢話不多說,正文開始吧!

善用策略模式 Strategy Pattern

前情提要

前一篇大致上利用 RPG 角色的範例,示範了如何簡單對各種不同的角色職業 Character,利用策略模式進行攻擊能力的策略選擇。

https://ithelp.ithome.com.tw/upload/images/20190925/20120614wp2iXaFmJp.png

以上就是利用策略模式狀態下,父類別 Character 藉由一個參考點(也就是父類別的成員變數 attackRef)連結到 Attack 介面。藉由 Attack 介面可以實踐出各種不同的攻擊策略,在這裡的範例是分別實踐出 MeleeAttack 以及 MagicAttack

子類別經由父類別進行繼承後 —— 由於父類別有宣告與 Attack 介面連結的參考點,子類別可以選擇其中一種 Attack 介面宣告出的策略,進行選擇攻擊的演算法,不需另外再寫一個 attack 成員方法去覆蓋父類別的 attack 方法呢。

本篇的範例程式碼可以參考這個連結

策略的切換 Switching Strategies

由上一篇可以切換演算法部分進行延伸。

假若角色可能會出現不同的攻擊方式,譬如:Swordsman 除了會 MeleeAttack (直接攻擊)外,可能還會有 StabAttack (刺擊)這種攻擊法。

首先可以藉由 Attack 介面另訂一個新攻擊策略。

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

這裡就出現一個問題了:Swordsman 預設的攻擊模式(也就是它的策略)是 MeleeAttack —— 不過還記得筆者在前一篇文章有貼過描述策略模式的一句話嗎?

Changing algorithm during runtime.

根據上面那句話的意思:如何在 runtime 期間進行切換策略(演算法)的動作呢?

換句話說,筆者不希望在建構 Swordsman 角色的時候更動它原本的設定 —— 也就是 MeleeAttack,但是要如何在程式碼跑的過程進行更換攻擊策略的動作。

其實非常簡單,可以在父類別宣告一個 switchAttackStrategy 成員方法 —— 負責進行策略的切換;這裡的範例指的是進行攻擊方式的切換。以下是簡單的實踐:

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

筆者寫一段簡單的程式碼進行測試。(如圖一)

https://ithelp.ithome.com.tw/upload/images/20190926/201206145QGKgrwpxO.png
圖一:我們可以更換角色的攻擊策略囉!

如果沒有應用策略模式,想要更換策略的話,可能得設定一個 Flag 負責記錄該角色目前的攻擊方式,然後再進行 if...else... 這一類的判斷,又會回到原來那一大串判斷敘述地獄了。

然而,經由策略模式,我們可以捨棄掉多重判斷敘述的同時,也可以達到類別與介面被重複使用的功能 —— 以 Warlock 為例,它也繼承了父類別 Character,代表 Warlock 類型的角色也可以切換不同的策略呢!

本篇唯一重點. 策略模式的優勢

策略模式最大的威力在於:新增各種策略在子類別時,不需要覆蓋父類別的實踐,就可以間接切換策略達到目的

此外,新增的策略也可以被重複加以利用而不會影響到子類別的功能實踐。

這個優勢符合了這句話的形容 —— “Changing algorithm during runtime.

運用策略模式的優勢

我們緊接著使用策略模式的優勢,設計出更多 RPG 角色可能會有的功能。以下為了更熟悉策略模式的概念,筆者會繼續帶領讀者一遍又一遍地熟悉這個設計模式的實踐流程。

設計角色裝備武器的功能 Weapon Equipment

常理的 RPG 遊戲設計時,通常角色的攻擊方式是會根據武器特性而決定的,除非你是使出技能之類的機制才會轉換攻擊演算法。

以下來試試看能不能實踐出這個功能:設計武器 Weapon 介面,其中延伸出各種不同的武器,讓角色有能力去裝備這些東西;並且角色不需要進行初始化攻擊策略,而是由這些武器去自動鎖定攻擊策略

筆者就按照前一篇講過的步驟嚴格執行。

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

首先,筆者建立 weapons 資料夾,並且新增 weapons/Weapon.ts 檔案,負責定義每個武器必須實作的特性:

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

Weapon 介面有幾個規格:

  • name 為武器名稱,為 readonly 模式
  • availableRoles 控制的是武器可以被哪個職業裝備
  • attackStrategy 則是武器綁定的是哪一種基礎攻擊策略

另外,我們分別新增三種不同的武器:BasicSwordBasicWand 以及 Dagger

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

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

https://ithelp.ithome.com.tw/upload/images/20190926/201206141Sc5CmRiOO.png

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

接下來是在父類別進行參考點的建立。不過這一次跟前一篇不同的是 —— 原本可以讓角色自由切換攻擊策略 Attack,這一次希望達到的目標則是:角色裝備武器的同時,攻擊策略根據 WeaponattackStrategy 自動進行綁定。

https://ithelp.ithome.com.tw/upload/images/20190926/201206149BAoMJkgWu.png

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

主要是角色負責進行攻擊 attack 的動作,而 attack 早就在前一篇被實現了。

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

不過,本範例是需要藉由更換武器 Weapon,因此必須設計的主要功能是 equip 方法 —— 負責接收 Weapon 類型的物件作為參數,藉以調整攻擊策略。

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

以上的程式碼,也利用了 availableRoles 進行武器能否被裝備在角色身上的檢測。

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

本範例寫的策略,自然而然是更換武器的概念 —— 我們可以在子類別初始化武器的動作。

不過這裡要注意的是 —— 原本選擇攻擊的策略已經被置換成藉由選擇武器就會自動進行攻擊策略的綁定,所以對於 SwordsmanWarlock 分別實踐之結果如下:

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

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

以上功能已經完成囉!我們開始進行驗收的動作。(以下程式碼編譯並執行結果如圖二)

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

https://ithelp.ithome.com.tw/upload/images/20190926/20120614SaAO28z1gq.png
圖二:除了可以切換掉武器外,也發現切換錯武器也會自動拋出例外呢!

圖三就是目前的 CharacterAttackWeapon 之間的關係圖。

https://ithelp.ithome.com.tw/upload/images/20190926/20120614atn7rU8kod.png
圖三:類別和介面有連結,使得類別跟不同介面綁定下的策略進行彈性的互換操作

不過,請不要被以上的實踐被死死地綁住 —— 設計一個系統的方式有很多種,重點在於要如何設計好物件跟策略的互動關係。

譬如,筆者也可以選擇不要讓角色 CharacterAttack 攻擊策略有連結,而必須讓角色藉由與武器 Weapon 的連結進行攻擊 attack 的動作!不過筆者想像中的架構結果可能會變成這樣。(如圖四)

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

必須藉由選擇的 Weapon 再去連結各種不同的攻擊策略,這樣做的好處是 —— 以 Warlock 為例,它除了可以選擇用匕首 Dagger 進行 MagicAttack 魔法攻擊外,也可以選擇 StabAttack 單純物理性地刺擊。

所以要讓策略模式的發揮方式非常多種,最終完全於你想讓系統能夠出現什麼樣的行為

小結

今天大致上再讓讀者更熟悉策略模式的好處跟彈性!最重要的就是:策略可以隨時隨地被切換、策略也可以同時被不同的類別重複使用。(前提是要正確地使用策略模式 —— 筆者知道這是廢話 XD)

下一篇,筆者要補充類別還沒講完的部分,那就是 —— 抽象類別

]]> Maxwell Alexius 2019-10-07 14:50:28
Day 26. 機動藍圖・策略模式 X 選擇策略 - Strategy Pattern Using TypeScript. I https://ithelp.ithome.com.tw/articles/10220356?sc=rss.iron https://ithelp.ithome.com.tw/articles/10220356?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

類別繼承與介面綁定的差別在哪裡?能夠描述它們各自的優缺點嗎?

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

筆者本來沒有要寫這一篇,自己卻不小心挖了這個坑,所以想說算了,就寫吧~

正文開始

策略模式 Strategy Pattern

先從問題的起點開始

首先,在介紹設計模式中的策略模式前,要先了解通常是什麼情形才會需要策略模式。

筆者就把前一篇所寫過的範例快速帶過。

https://ithelp.ithome.com.tw/upload/images/20190925/20120614U6CPgbWcMy.png

其中,以上的程式碼有個地方很冗長,那就是類別 Characterattack 成員方法的實踐內容。

一個遊戲如果不停擴充各種不同的角色種類,勢必會造成該 switch 的敘述式越來越龐大,再加上這個 Character 類別也沒有描述 —— 譬如,被攻擊的角色生命值是如何被扣損的,亦或者可能也沒有被命中敵人 —— 但要是把這個功能加上去可能只會造就越來越多義大利麵程式碼。

觀察一下程式碼,會發現一些特點:

  • 角色類別眾多,但都有相似的行為或演算法
  • 要定義新的行為,最差的結果就是很冗長的 switch...case...,亦可用 if...else if...else 敘述式
  • 如果想要擴充角色職業,必須在每一個 switch...case... 敘述式中,新增該角色職業的情形

本篇目的是 —— 除了會基本的 interfaceclass 語法外,還能夠善用它們。因此,筆者把昨天(很陽春)的 RPG 系統重新設計過一遍。筆者是額外再建一個全新的環境進行開發的動作,如果想要看本篇實作過後的完整程式碼可以點這邊

前置作業

這裡就稍微帶過筆者建立簡單環境的過程與執行的指令:

// 到任何一個選擇的檔案資料夾位置
$ cd ./<PATH_TO_CHOSEN_DIR>

// 初始化專案
$ tsc --init

// 新建 index.ts 檔案
$ touch index.ts

// 新建 build 資料夾
$ mkdir build

另外,在 tsconfig.json 裡,更改某些項目:

{
  "compilerOptions": {
    // 略...
    "outDir": "./build",
    "rootDir": "./",
    // 略...
  }
}

以上的選項是為了使編譯過後的檔案集中放在 build 資料夾裡。(編譯器詳細資訊後續會在第三篇章《戰線擴張》進行解析,這也是 Day 31. 以後的文章了)

另外,每一次必須要手動編譯並且執行檔案實在是很辛苦,因此筆者額外初始化 package.json 並下載一些套件協助開發:

// 初始化 package.json
$ npm init -y

// 下載一些模組
$ npm install concurrently nodemon --save-dev
  • nodemon 偵測到 JS 檔案被修改的話,就會重複用 node 執行該 JS 檔案
  • concurrently 則是會同時執行 package.json 裡不同的 script 定義的指令

修改 package.json 裡的 scripts 選項:

{
  // 略 ...
  "scripts": {
    "start:watch": "tsc -w",
    "start:run": "nodemon build/index.js",
    "start": "concurrently npm:start:*"
  },
  // 略 ...
}
  • start:watch 執行的是 tsc -w —— 代表只要 TypeScript 編譯器偵測到 TS 檔案有變動,就會重新編譯
  • start:run 則是每一次 JS 檔案被產出來(所以檔案有被修改),就會重新用 node 執行該檔案
  • start 裡面的 concurrently 會同時執行以上兩個不同指令

到目前為止打開 VSCode 專案大致上應該會長這樣。(圖一)

https://ithelp.ithome.com.tw/upload/images/20190925/20120614kiUwtjsGE2.png
圖一:專案裡面除了 tsconfig.json 檔案以外,也有 package.json 設定檔與一些被更改的資訊

如果下 npm start 就會同時監測專案裡 TS 檔案並且進行編譯與執行 JS 的動作。以下筆者簡單更改 index.ts 並且自動執行結果如圖二與圖三。

貼心小提示

如果第一次執行 npm start 出現錯誤,找不到 build/index.js,可以先用 tsc 編譯一次後再重新執行 npm start畢竟剛開始找不到 build/index.js 檔案,所以 node 也沒辦法執行編譯後的檔案

https://ithelp.ithome.com.tw/upload/images/20190925/20120614mBC7BwDz6S.png
圖二:第一次執行過後的結果

https://ithelp.ithome.com.tw/upload/images/20190925/20120614VhqEF555gg.png
圖三:更改 index.ts 內容,不需要進行編譯即可自動動作

從設計過程中找出問題所在

首先額外建立一個資料夾名為 characters:你可以使用 mkdir characters 或直接在編輯器裡面新增資料夾也可以。

新增 characters/Role.ts 檔案,內容如下:

https://ithelp.ithome.com.tw/upload/images/20190925/20120614f2ipWTAkvp.png

主要就是將職業內容使用列舉型別後,再 export 出去。讀者若不熟悉 import/exportdefault import/export 語法請記得上網查一下喔~)

然後新增 characters/Character.ts 檔案,內容如下:

https://ithelp.ithome.com.tw/upload/images/20190925/201206146u8eZHoYNm.png

Character 類別只有兩個成員變數以及一個成員方法:

  • name 是角色名稱,所以為 string
  • role 則是角色職業,為 Role 列舉型別;由於 Role 被定義在其他檔案,因此必須載入進來(這裡是用 default import 方式載入)

筆者在 index.ts 裡,將兩個模組載入後,利用簡單的程式碼進行測試。(結果如圖四)

https://ithelp.ithome.com.tw/upload/images/20190925/20120614YZShkcQ98c.png

接下來才是進入問題的開始:角色職業有四種(筆者本篇會以其中兩種為例)—— 每種職業都有相似的模式,譬如:

  • 屬性與能力值,如生命值 health、魔力值 mana 等等
  • 會攻擊之外,也會被攻擊,但攻擊方法可能又分很多種,譬如:直接攻擊 MeleeAttack、魔法攻擊 MagicAttack(當然也可以再細分)
  • 各種職類可能也會有特殊技能

這裡筆者就先繼續寫下去,寫到有問題出現時 —— 進行分析後,再來看看如何解決。

首先,我們可以利用繼承 Character 的方式建構出角色們 —— 新增 characters/Swordsman.ts 以及 characters/Warlock.ts,內容如下:

https://ithelp.ithome.com.tw/upload/images/20190925/20120614Kd2I8LEYVT.png

https://ithelp.ithome.com.tw/upload/images/20190925/20120614lDqGoWcb85.png

圖五是對程式碼進行簡單的檢測結果。

https://ithelp.ithome.com.tw/upload/images/20190925/20120614fjb9WVT58k.png
圖五:SwordsmanWarlock 是可以正常使用的類別

到這裡應該沒問題 —— 接下來,筆者先用不好的方式呈現 attack 方法的實踐過程。

大部分使用類別繼承的想法,不外乎是因為父類別跟子類別的關係是很緊密的,因此父類別定義新的成員,子類別也自動擁有父類別定義的該成員。

https://ithelp.ithome.com.tw/upload/images/20190925/201206143dF7vVgteJ.png

因此,父類別若新增 attack 方法,則子類別也會跟著擁有 attack 方法。(圖六為程式碼檢測成果)

https://ithelp.ithome.com.tw/upload/images/20190925/201206149lNI7bWPuJ.png
圖六:子類別繼承了父類別的成員,因此可以使用 attack 方法

若希望每個子類別攻擊的方式不同,通常最直觀的作法就是 —— 直接在子類別內覆蓋父類別的方法。

因此筆者將 Warlockattack 方法進行修改的動作。

https://ithelp.ithome.com.tw/upload/images/20190925/20120614X9rg7X2Vab.png

(其實筆者後來重新看過,英文連接詞 and 前後文法應該要一致... 不過想說算了不改了 XD

理所當然,測試 Warlock 類別的 attack 方法會出現不ㄧ樣的結果喔~(如圖七)

https://ithelp.ithome.com.tw/upload/images/20190925/20120614iayKb8Vbzb.png
圖七:Warlock 攻擊時,變成使用魔法攻擊呢~

這裡就碰到了問題點:儘管利用繼承的方式避開了 switch...case... 這個冗長判斷式的解法,然而,取而代之的只是 —— 宣告更多子類別然後對父類別的方法進行覆蓋的動作

這實在是不行啊!治標不治本,搞不好也有其他職業 —— 假設我們又有新的職業為 Occultist(神秘學者 —— 筆者隨便舉的一個職業),它也會使用魔法攻擊,難道又得從 Warlock 裡面將 attack 方法照本宣科複製到 Occultist 類別嗎?於是這裡就違反了 DRY(Don't Repeat Yourself)的原則。

因此,筆者今天就要搬出今天的主角:策略模式 —— Strategy Pattern

策略模式 Strategy Pattern

筆者上網查詢,通常會出現的一句話代表策略模式的意涵:

Changing algorithm during runtime.

英文到底是要用介系詞 During 還是 On,筆者覺得看完本篇文章再去自己查詢文法正不正確,反正在本系列,學到工具的用法與真諦最重要。

其實簡單來說:

策略模式的意義在於 —— 根據不同情形,在程式執行時可以靈活地轉換演算法(策略)而不需要再另訂新的類別與類別繼承的動作

另外,以下會用個人見解對策略模式進行描述,如果讀者看得懂就表示你可能早就學過亦或者是你是萬年以來的天才 —— 筆者也願意把膝蓋割下來跪在你面前!(好痛

筆者想要試試看從概念上切入再一步步實踐策略模式,因此筆者認為讀者對以下重點剛開始看不懂是不意外的,讀者也可以選擇先跳過以下的重點直接往後面的步驟看,等熟悉策略模式的設計手法後再回頭看看筆者寫的重點也 Ok

本篇唯一重點. 策略模式的意涵

如果要在眾多類別中實踐近似但相異的行為,與其直接實踐(implement)出功能並使用一連串的敘述式進行演算法的切換,不如在父類別裡建立一個行為演算法的參考點(reference point),任何符合該參考點的演算法必須遵照某介面(interface)進行實踐的動作;父類別可以藉由在該參考點切換演算法,不需要經過一連串判斷流程,就可以達到功能相異的結果。

而父類別的參考點切換演算法的過程,又被稱作為切換不同策略的行為,因此得名 —— 策略模式。

可能看完這一段,會很想吐槽筆者:“這個作者為何要那麼麻煩地把一個概念拐彎抹角的描述出來?”

筆者知道以上的意涵很模糊,但那是因為我們還沒討論到以下筆者提出來的問題:

  1. 直接實踐參考點差別到底在哪?
  2. 由父類別定義參考點(reference point)進行策略的切換意思是什麼?
  3. 而符合該參考點的演算法 —— 也就是策略,策略本身是什麼?必須遵照某介面進行實作,也就是說策略必須跟介面進行綁定的動作?

其實筆者刻意留了一些線索進去,我們來仔細推斷。

首先,參考點的概念在第 2 點有被提示到 —— 由父類別去定義一個參考點,也就是說:

參考點(reference point)是父類別的成員之一

二來,第 3 點有說到:“符合該參考點的演算法 —— 也就是策略”;而後又有一句:“策略必須跟介面進行綁定的動作”,也就是說:

策略並不是函式或者是方法,因為要能夠和介面進行綁定;不過也從策略必須能夠綁定介面這個特點,得知 —— 策略是一個類別的宣告

所以再回到含糊的重點那一段,其中:“父類別可以藉由在該參考點切換演算法,不需要經過一連串判斷流程,就可以達到功能相異的結果”,代表父類別使用參考點進行演算法的切換,達到切換不同功能的目的。

以上推論過程不清楚也沒關係,這裡筆者就要開始以上面簡短推論出來的結果對 Character 類別系列進行新增角色攻擊的能力。

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

首先,筆者必須先宣告一連串的策略(Strategies,亦或者演算法)。其中,每一個策略必須綁定某介面以確保實踐出來的功能是固定,但內部的演算可以是不ㄧ樣的。

筆者先建立一個資料夾名為 abilities。根據角色的攻擊能力這一項功能 —— 宣告 Attack 這個介面並且放在 abilities/Attack.ts 這個檔案,內容如下。

https://ithelp.ithome.com.tw/upload/images/20190925/20120614z9k04grs4J.png

再來筆者可以開始定義不同的攻擊策略,以下就以 MeleeAttackMagicAttack 為範例,各自為 abilities/MeleeAttack.tsabilities/MagicAttack.ts

https://ithelp.ithome.com.tw/upload/images/20190925/20120614uLgqwkvwi6.png

https://ithelp.ithome.com.tw/upload/images/20190925/20120614MJ0Izavi0N.png

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

定義好策略後,接下來就是在父類別建立起參考點(Reference Point)—— 該參考點負責的任務就是進行策略的切換

https://ithelp.ithome.com.tw/upload/images/20190925/20120614Cy7Qbn0GHe.png

由以上程式碼得知,參考點其實只是一個類別屬性 attackRef 負責連結到 Attack 型別的物件;此外,筆者也屏棄掉原本在父類別的 attack 成員方法。

這個步驟很簡單就這樣被結束了 XD。

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

在父類別裡,因為我們的介面 Attack 絕對會有 attack 方法,儘管裡面的內容並不知道(可能會是 MeleeAttackMagicAttack)—— 但這根本不重要我們只要知道角色有 Attack 的策略可以呼叫就夠了!因為每個策略綁定了介面,確保都有 attack 方法進行實踐。

因此,原本的 attack 方法可以藉由參考點 attackRef 指定到的若干策略進行呼叫 attack 方法的動作:

https://ithelp.ithome.com.tw/upload/images/20190925/20120614KZm6FdRP8U.png

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

我們就快完成了!

接下來,可以為各種子類別進行策略的選擇喔!譬如:Swordsman 選擇的攻擊策略是 MeleeAttack;相對的,Warlock 選擇的則是 MagicAttack

https://ithelp.ithome.com.tw/upload/images/20190925/20120614Xmg9NyQI7t.png

https://ithelp.ithome.com.tw/upload/images/20190925/20120614PhBNDyv8xG.png

可以看到,範例程式碼裡面的子類別不需要進行覆蓋父類別方法的動作,而是藉由選擇策略(也就是演算法)的方式進行功能的實踐

以下照常進行程式碼的測試(跟之前的測試程式碼一模ㄧ樣,沒有修改,結果如圖八)。

https://ithelp.ithome.com.tw/upload/images/20190925/20120614PPQDgiTfQa.png
圖八:藉由策略模式,只要簡單地定義不同的策略演算法,子類別就只需要指定其中一種策略就可以被實踐出功能

如果將類別與介面的關係畫成圖的話,結構會如圖九。

https://ithelp.ithome.com.tw/upload/images/20190925/20120614wp2iXaFmJp.png
圖九:使用策略模式後,類別與介面的關係圖~

筆者就把本日實踐策略模式的過程簡單的敘述出來:

  • 父類別 Character 藉由參考點(Reference Point)連結 Attack 介面下的不同策略
  • 父類別 Character 實踐的 attack 方法會將角色的攻擊能力,由 attackRef 連結到的策略執行
  • 子類別可以自由選擇要使用的策略,並且將該策略指派到父類別早就定義好的參考點

以上讀者可以慢慢吸收~

小結

今天主要把整個策略模式的實踐過程,按照步驟地呈現給讀者看。

本篇章事實上還沒結束,有鑒於篇幅已經超過 10,000 字(網站的 Markdown 編輯器寫的XD,事實上應該快破 4,000 字而已),所以筆者將會以本篇的案例繼續延伸下去,讓讀者體會一下更多策略模式的優點喔~

]]> Maxwell Alexius 2019-10-06 15:39:04
Day 25. 機動藍圖・類別與介面 X 終極的組合 - Ultimate Combo of Class & Interface https://ithelp.ithome.com.tw/articles/10219927?sc=rss.iron https://ithelp.ithome.com.tw/articles/10219927?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

  1. 試描述類別(Class)的型別推論機制與註記機制。
  2. 繼承過後的子類別,試描述其類別推論機制與註記機制。
  3. 子類別跟父類別的推論與註記機制交互錯用時的特殊規則是什麼?

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

昨天原本講到類別的型別推論與註記(Type Inference & Annotation)的機制,今天就來講一下類別跟介面的結合的種種情況。本篇也算是《機動藍圖》系列的大重點呢~

筆者寫到這裡也是覺得神奇 —— 我們才正開始要討論介面與類別的結合,但筆者在後續篇章會寫到簡單利用介面與類別結合一些 OOP 設計模式的應用。

以下正文開始

類別與介面的終極組合

類別實踐介面的規格

首先,筆者好像之前有在某篇章使用過 implements 這個關鍵字,負責將類別綁定介面的規格 —— 今天就是要講這個!

按照一貫的步伐,筆者一定是從最基本的案例淺入深出地講起。

平常我們會直接宣告類別後直接進行開發的動作,但今天筆者會好好按照標準程序(Standard Procedure)—— 先把規格定義出來後,再進行實踐的動作:

  1. 先把介面宣告出來,確認規格(Speculation,時常被簡短為 Spec.)
  2. 將類別對介面進行綁定
  3. 類別必須實踐介面規範的規格

https://ithelp.ithome.com.tw/upload/images/20190924/20120614e60fg8GUes.png

以上的程式碼,先從最簡單的 ICharacter 介面開始 —— 第一個步驟,規格的確認完成。

介面 ICharacter 很陽春,就是:

  • name 代表角色名稱
  • role 代表角色職業
  • attack 是一個函式,目前輸入參數是 target,代表角色可以攻擊的對象,其中 —— target 參數代表的值,只要是任何實踐 ICharacter 介面的物件都可以接受

接下來幾篇的主題就是陽春的 RPG 系統無誤!

第二步驟 —— 類別與介面進行綁定的動作。

如果讀者有看過別人的文章,有些人會把類別與介面的結合形容成 —— 簽訂契約的概念(Signing Contract)。也就是說,類別一但跟介面綁定了,就必須實現介面裡描述的內容,否則會被 TypeScript 認定為違約。(你不會被罰款,但你會遭受到 TypeScript 的指控!)

綁定介面很簡單,就是使用 implements 這個單字:

https://ithelp.ithome.com.tw/upload/images/20190924/20120614MxcbPWO8II.png

筆者刻意在這裡貼出違約訊息。(如圖一)

https://ithelp.ithome.com.tw/upload/images/20190924/20120614cBBNExAhvQ.png
圖一:Character 明顯違反了契約

Character 類別缺少了三個東西:namerole 以及 attack 這三個成員們。

所以第三步驟:實踐介面的規格(也就是契約內容)。以下的程式碼就是符合契約內容簡單的實踐:

https://ithelp.ithome.com.tw/upload/images/20190924/20120614WHcoDU37il.png

貼心小提示

OOP 經驗豐富的讀者一看就知道 —— 冗長的 switch...case... 敘述式的解法中 —— 其中一種就是使用 Strategy Pattern (策略模式)來解掉。筆者後續會寫這部分的設計模式篇章,畢竟這也會跟後面第 30 天的重頭戲 —— Object Composition 的概念有關~

以上筆者來測試看看使用結果。(以下程式碼編譯並使用 node 執行結果如圖二)

https://ithelp.ithome.com.tw/upload/images/20190924/20120614L5WdHgSaOJ.png

https://ithelp.ithome.com.tw/upload/images/20190924/20120614grphqLszb0.png
圖二:根據不同的職業,呼叫 attack 時會有不同的結果

重點 1. 類別對介面進行綁定

若已宣告類別 C 與介面 I,其中 C 想要對 I 進行綁定的動作,必須使用 implements 關鍵字。

一但 C 綁定了 I,則類別 C 必須要實踐出介面 I 裡面的所有規格成員

https://ithelp.ithome.com.tw/upload/images/20190924/20120614OjwjWDWVl3.png

類別繼承與介面綁定最大的不同 Class Inheritance V.S. Interface Implementation

有些讀者肯定會對類別的繼承介面的綁定感到迷糊 —— 這兩種到底差別差在哪?

其中,最大的不同就是:一個子類別一次只能繼承一個父類別;然而,一個類別可以跟多個介面進行綁定

這也是介面的運用會比類別繼承還要更有彈性的主因。在軟體設計裡,時常討論到 —— 兩個系統的耦合程度(Coupling)中,使用類別繼承的耦合程度一定會比介面的綁定還來得高

在父類別新增一個功能跟嵌入一個介面比起來,後者的難度會比較低。父類別要是新增一項功能,則必須確保所有的子類別能夠正常運作,否則會面臨到所有的子類別為了遷就父類別新增的功能必須進行覆寫的動作;另外,如果想要將父類別裡面的某些功能抽出來給其他程式碼或類別使用實在是不容易的事情。

嵌入介面是比較保險版本的新增功能方式,而且介面是可以被不同類別重複利用,不會像類別死死地把成員細節絕對綁定。除非類別跟父類別間的關係程度真的是很緊密,可以使用繼承,否則通常會使用介面來組出功能。

然而,OOP 設計模式裡,當然不侷限於使用介面的方式降低耦合程度,善用類別物件組織起來(Object Composition)而不使用類別繼承也可以達到降低相依的耦合度,這些都算是軟體江湖上流傳的招式 —— 筆者將在第 30 天揭曉。(不過講到策略模式時,就會讓讀者體會到不需經由類別繼承就可以達到耦合度的降低!聽起來很好吃!

假設 Character 除了實踐基本資料的介面 ICharacter 外,也還會有更多屬性,因此筆者再生出新的 IStats 介面。程式碼如下:

https://ithelp.ithome.com.tw/upload/images/20190924/20120614715D6FDv4h.png

因此,如果想要同時讓 CharacterIStats 進行綁定的話,非常簡單 —— 就直接在 implements 後面再加上去就好 —— 如果至少有兩個介面以上,不同的介面就用逗號分隔

https://ithelp.ithome.com.tw/upload/images/20190924/20120614W5kNoWxOmZ.png

TypeScript 會照常幫我們追蹤類別綁定介面時違約的部分。(如圖三)

https://ithelp.ithome.com.tw/upload/images/20191005/2012061447mg07ushG.png
圖三:很明顯,剛綁定 IStats 上去一定會出現錯誤呢 —— healthmanastrength 以及 defense 這幾個值都沒被實踐進去。

以下的程式碼進行簡單的實踐。(其實就是很懶惰的把值給丟上去 XD)

https://ithelp.ithome.com.tw/upload/images/20190924/20120614adLR07MviA.png

此外,繼承跟介面的綁定可以同時進行 —— 你可以在宣告類別時,使用 extends 進行繼承外,同時也對介面進行綁定喔!這部分筆者認為讀者可以去試試看,因此就放在重點ㄧ併整理起來吧。

重點 2. 類別的繼承與介面的綁定 Class Inheritance & Interface Implementation

類別繼承與介面綁定的最大差異是:

  • 類別一次只能繼承一個父類別
  • 類別可以同時實踐多個介面

若已宣告過某類別 C 以及介面 I1I2、...In。其中,想要再宣告一個繼承父類別 C 的子類別 D,並且與介面 I1I2、...In 進行綁定的動作,程式寫法如下:

https://ithelp.ithome.com.tw/upload/images/20190924/20120614hPZbp7XNcn.png

其中,D 類別的宣告因為繼承自 C,因此 D 擁有 C 的所有 publicprotected 模式下的成員。另外,由於 D 類別也有對介面 I1I2、...In 進行綁定,因此必須實踐所有 I1I2、...In 融合過後的結果之規格 —— 可以參見介面融合篇章

另外,類別繼承通常不容易將功能拆出來再利用,因此耦合程度較高;然而,因為介面的實踐是可以拆卸又裝到不同的類別上去,因此介面與類別的耦合程度較低以外,可再利用度較高。

類別綁定介面後的型別推論與註記機制

前一篇已經講過單純的類別建構出來的物件之型別推論與註記機制,今天就順便把類別與介面綁定的案例討論完畢!

我們ㄧ樣使用剛剛的 Character 範例進行驗證的動作,其中 Character 同時有 ICharacterIStats 這兩種介面的實踐。首先從最簡單的程式碼開始:

https://ithelp.ithome.com.tw/upload/images/20190924/20120614MMWoO88CUX.png

其中,character 的推論結果如圖四。

https://ithelp.ithome.com.tw/upload/images/20190924/20120614fP5S7mX0Pg.png
圖四:相信讀者感到不意外,推論結果就是 Character,如果熟悉前一篇討論的類別的型別推論機制就會覺得正常

然而,這裡真正要問的問題是 —— 變數若被註記為介面時,可以把實踐該介面的類別建立的物件指派進去嗎

筆者試了一下底下的程式碼。(檢測結果如圖五)

https://ithelp.ithome.com.tw/upload/images/20190924/20120614032pbKbSyn.png

https://ithelp.ithome.com.tw/upload/images/20190924/20120614euM5yWSvpL.png
圖五:檢測的結果是可以的,不過因為是被註記的變數,因此被推論為註記之介面 ICharacter

以下比較有註記跟沒註記的差別。(以下程式碼檢測結果如圖六;錯誤訊息如圖七)

https://ithelp.ithome.com.tw/upload/images/20190924/20120614oCeQg4l1AO.png

https://ithelp.ithome.com.tw/upload/images/20190924/20120614Nv69rJHn5d.png
圖六:很明顯地,被特別註記為介面 ICharacter 的變數不能夠呼叫 health 屬性的理由則是因為 health 不存在 ICharacter 介面

https://ithelp.ithome.com.tw/upload/images/20190924/20120614oA9CNiYm9G.png
圖七:ICharacter 介面裡並不存在 health 屬性

這裡我們得到很重要的結論:儘管類別可能擁有多種不同的介面,若變數被註記到類別有實踐過的介面,該類別建構的物件可以被指派到該變數去。

重點 3. 類別綁定介面的推論與註記機制

任何類別 C —— 儘管有綁定介面 I1I2、... In建構出來的物件之型別推論結果一律都是指向該類別 C

若變數被積極註記為 I1I2、... In 中的任一介面 —— 該變數依然可以被指派類別 C 建構出來的物件。主要原因是 —— 被註記為介面型別的變數,只要該物件至少符合介面的實作,就算通過。

變數被推論為類別 C 或者是被積極註記為介面型別 I1I2、... In 的差別在於:

  • 如果變數被推論亦或者註記為 C,則變數除了可以呼叫類別裡自定義的 public 成員外,也可以呼叫介面 I1I2、... In 融合過後的規格之屬性與方法。
  • 如果變數被註記為 I1I2、... In 介面裡其中一個介面 Im,儘管變數可以被指派有實踐介面 Im 類別建構出來的物件,卻只能呼叫 Im 介面裡面的規格之屬性與方法

通常會需要積極註記為介面而非讓 TypeScript 自動推論為類別的情形其實沒有想像中的少 —— 重點是這個特性:如果將變數積極註記為介面 I 時,任何類別如果有實踐 I,則該類別產出的物件就算是 I 介面可以接受的範疇。

譬如除了 Character 類別外,筆者還可以再宣告 Monster 這個類別為範例。(順便在 Role 的列舉型別內再塞一個 Monster,當然這不算是好的寫法,不過這裡的程式碼只是在展示重點 3 延伸出來的應用 XD)

https://ithelp.ithome.com.tw/upload/images/20190924/20120614UAcOAXpDqR.png

以上的程式碼,筆者完整地給大家看到:CharacterMonster 同時有實踐 ICharacter 介面。

筆者將焦點放在兩個類別裡實踐出來的 attack 成員方法:

https://ithelp.ithome.com.tw/upload/images/20190924/20120614DLcDMbWe11.png

讀者會發現,attack 方法的參數 —— target 並不是 Character 或者是 Monster 類別,而是被註記為 ICharacter 介面;這代表任何實踐過 ICharacter 介面的類別所建構的物件都可以被代入到 attack 方法作為 target 參數的值。(以下程式碼編譯並使用 node 執行結果如圖八)

https://ithelp.ithome.com.tw/upload/images/20190924/20120614EoiWa5R9kz.png

https://ithelp.ithome.com.tw/upload/images/20190924/20120614yY8fMKYJLt.png
圖八:對象只要是實踐過 ICharacter 的類別 —— 該類別創建出來的物件都可以被套入 attack 方法裡

重點 4. 積極註記介面型別的好處

任何被註記為介面 I 的變數 A 或函式的參數 P,只要有類別實踐過介面 I,該類別建構出來的物件可以被代入到變數 A 或函式裡的參數 P

繼承後的子類別同時綁定介面後的型別推論與註記機制

這一小節的名稱雖然又臭又長,筆者還是得說明一下同時繼承以及實踐介面的類別的型別推論與註記機制。其實只要熟悉前一篇提到的重點結合今天提出的重點,基本上不需要再為了本節的案例進行深入討論。

筆者乾脆再從剛剛的 Character —— 將它作為父類別,宣告其他類別對其繼承,這裡以 BountyHunter 作為子類別。

https://ithelp.ithome.com.tw/upload/images/20190924/20120614h8n1cj0KIL.png

其中,BountyHunter 除了繼承父類別 Character 以外,還有額外的類別成員:

  • hostages 為成員變數,型別為 ICharacter[] 陣列型別,代表賞金獵人獵取到的人質(Hostage)
  • capture 為成員方法,參數分別為 targetthreshold,分別代表賞金獵人獵取到的目標物件以及機率
  • sellHostages 也是成員方法,沒有任何輸入,負責賣掉人質賺取 $$。

在實際測試前,運用今天學到的東西 —— 筆者把本篇章重點 3 開頭第一句話原封不動貼下來

任何類別 C —— 儘管有綁定介面 I1I2、... In建構出來的物件之型別推論結果一律都是指向該類別 C

BountyHunter 甚至沒有實踐任何介面,因此 new BountyHunter(...) 被建造出來後之型別推論結果絕對是 BountyHunter,這一點請讀者自行驗證。

另外,運用本篇學到的重點 4 :

任何被註記為介面 I 的變數 A 或函式的參數 P,只要有類別實踐過介面 I,該類別建構出來的物件可以被代入到變數 A 或函式裡的參數 P

可以推斷:BountyHuntercapture 成員方法 —— 第一個參數 target 絕對可以代入 CharacterMonster 類別建造的物件。因為 CharacterMonster 都有實踐 ICharacter 這個介面,而 target 參數對應的型別就是 ICharacter 介面。

所以以下的程式碼 TypeScript 不會亂叫,編譯過後並且執行的結果如圖九。

https://ithelp.ithome.com.tw/upload/images/20190924/20120614ljVra82Fpj.png

https://ithelp.ithome.com.tw/upload/images/20190924/20120614gsPfq2jyNf.png
圖九:結果這個賞金獵人連怪物都沒抓到

BountyHunter 沒有實踐介面 ICharacter,但它的父類別有,那 BountyHunter 型別的物件能不能夠代表 ICharacter 的值呢?

要測試這個其實不用再額外定義變數,直接用早已藉由 Character 建構的物件對 BountyHunter 建構的物件呼叫 attack 方法。不過筆者這邊直接貼出 TypeScript 判定結果(如圖十),理所當然是可以被接受的喔!

https://ithelp.ithome.com.tw/upload/images/20190924/20120614uYhfZLva25.png
圖十:角色 Character 可以回擊 BountyHunter 呢!

那麼就算不是父類別但也有實踐 ICharacterMonster 類別呢?(如圖十一)

https://ithelp.ithome.com.tw/upload/images/20190924/20120614v98QsNPo4y.png
圖十一:怪物 Monster 也可以回擊 BountyHunter 呢!

所以筆者得出結論:

重點 5. 類別繼承有實踐介面的父類別

子類別繼承父類別,除了擁有父類別 publicprotected 模式的成員外,也同時繼承父類別實踐之介面的性質

讀者試試看

https://ithelp.ithome.com.tw/upload/images/20190924/20120614eXMBfjhnSA.png

這邊筆者認為不需要再討論的主要原因是 —— 可以藉由前一篇以及今天學到的重點推出這邊的程式碼的行為,因此才會放到讀者試試看這個單元。(不過以上的程式碼驗證,相信讀者應該也會推斷出來,非常簡單)

小結

今天又是莫名超長篇,不過把介面跟類別的結合寫完之後,筆者頓時神清氣爽。

筆者原本沒有想要把策略模式放到系列文的,但既然自己都挖洞了。那就心甘情願跳下去吧

讀者能夠從中學到東西的話,筆者就已經感到值得~

]]> Maxwell Alexius 2019-10-05 15:44:22
Day 24. 機動藍圖・類別推論 X 註記類別 - Class Type Inference & Annotation https://ithelp.ithome.com.tw/articles/10219657?sc=rss.iron https://ithelp.ithome.com.tw/articles/10219657?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

  1. 大致上已經了解類別的基本用法與性質了嗎?
  2. TypeScript 針對物件方面的型別推論與註記機制為何?

如果還沒理解完畢的話,可以先翻看以下的文章喔:

筆者事實上是因為寫類別這個主題過程太投入,差點忘記還要講類別的推論與註記過程。XD

所以我們趕緊就正文開始吧!

類別在 TypeScript 的推論與註記機制

類別的型別推論 Type Inference in Class

基本上,讀者看到今天的文章,應該已經對型別的推論與註記概念很了解。如果是跳到後面看到本篇文章時,建議可以先看筆者一開始列的文章列表喔,其中《前線維護》篇章主要是讓讀者能夠理解 Type Inference 與 Annotation 的定義與機制 —— 因為這可是 TypeScript 的主打 Feature 呢!

以下筆者就派出本日的範例類別 Horse

https://ithelp.ithome.com.tw/upload/images/20190923/20120614pwG8zSEoRj.png

(筆者知道這個類別範例很白癡,將就一下XD)

首先,以上的類別 Horse 有四個基本成員變數(Member Variables),分別代表:

  • name:馬的名稱,字串型態,而且可以供外部使用(public 模式)
  • color:馬的顏色,設定為列舉型態,顏色也可以被外面竄改
  • type:馬的種類,為字串型態;儘管為 public 模式但卻被標記 readonly —— 唯讀模式
  • noise:馬的叫聲,但是是 private 模式,而且有預設值(讀者會學這種 'MeeeeeeeEeeééeéeée~' 的叫聲嗎?)

以上的說明,筆者真的沒有在罵粗話 XD

再來也有幾個成員方法(Member Methods):

  • makeNoisepublic 模式,負責讓馬叫(叫啊!
  • info 也是 public 模式,負責印出馬的基本資料
  • infoText 則是 private 模式,負責將馬的基本資料用 Template String 湊合起來

理解之後,今天的目標是要理解類別的型別推論與註記機制。

因此我們先變出一隻馬,但是完整建構馬之前:筆者必須截下這張圖。(建構物件時 VSCode 提醒視窗如圖一)

https://ithelp.ithome.com.tw/upload/images/20190923/20120614IXdfTnnWR9.png
圖一:還沒建構物件前,TypeScript 提醒我們 Horse建構子函式的參數被推論的結果

很明顯看到:

Horse(name: string, color: Color, type: string, noise?: string): Horse

這是屬於函式型別推論結果,光是這一行就已經告訴我們很多很有用的訊息呢!

  1. 函式名稱為 Horse —— 也就是說宣告類別(Class)就相當於宣告一個函式 —— 原生 JS 在模擬類別的情形時,就是用 Function 來模擬的。
  2. 函式 Horse回傳型別是 Horse,代表類別本身是一種型別化名(Type Alias);也就是說,宣告一個類別等於建造新的型別,因此也印證筆者之前在類別繼承篇章偶然提到的概念:“類別與介面跟普通的型別一樣,都是型別化名的一種”。
  3. 圖一中,仔細看視窗被底線與粗體凸顯的訊息:name: string,TypeScript 提示我們 —— 第一個參數要填入參數型態為 string,代表 name 這個參數

根據剛剛筆者講的第 3 點,假設我們已經填好第一個參數,那麼下一個畫面出現的是(如圖二):

https://ithelp.ithome.com.tw/upload/images/20190923/20120614PIQ0s1zOoY.png
圖二:第一個參數填完,自動提示你第二個參數為 color: Color

以上是筆者想要提醒讀者 —— 要好好善用 TypeScript 提供給你的工具,增進開發效率的小細節不要輕易放過。

另外,有時候注意到工具提供的訊息 —— 可以猜出一些 TypeScript 隱藏的機制,很方便呢!等等筆者繼續驗證類別的註記部分,基本上又會再把 Day 03. 提到的完整性理論再次搬出來喔。

首先先產出一隻小馬:

https://ithelp.ithome.com.tw/upload/images/20190923/20120614CEoqAJxaJg.png

ㄧ樣來看看類別建造過後的物件被推論的結果是什麼。(推論結果如圖三)

https://ithelp.ithome.com.tw/upload/images/20190923/2012061444HjjNo1W1.png
圖三:aRandomHorsie 的推論結果是 Horse 型別

恩,筆者來下一個很根本(但很廢話)的重點

重點 1. 類別型別

宣告新的類別 —— 本身就是在創造新的型別化名;也就是說,我們可以使用類別名稱作為變數的型別註記(Type Annotation)。

根據很久之前描述的廣義物件完整性原則

  • 我們不能破壞物件的完整性 —— 新增屬性會被 TypeScript 喊卡!
  • 我們不能破壞物件的完整性 —— 對物件原本有的屬性指派錯誤型別的值會被 TypeScript 喊卡!
  • 我們不能破壞物件的完整性 —— 覆寫整個值就必須覆寫完整且正確的格式,亦即要覆蓋掉屬於 Horse 型別的變數的值,必須用 Horse 類別建構出來的物件進行完美覆蓋動作

以下的程式碼就是對以上的案例的驗證行為。(驗證結果如圖四;錯誤訊息如圖五~圖七)

https://ithelp.ithome.com.tw/upload/images/20190923/20120614CeMz0wwY3J.png

https://ithelp.ithome.com.tw/upload/images/20190923/20120614CEAhrJzHm7.png
圖四:各種層面的出錯,TypeScript 會主動河蟹

https://ithelp.ithome.com.tw/upload/images/20190923/20120614DZB8a74Hyf.png
圖五:難道紅色就錯了嗎?

https://ithelp.ithome.com.tw/upload/images/20190923/20120614RfB9mkjSga.png
圖六:難道豢養的馬也錯了嗎?

https://ithelp.ithome.com.tw/upload/images/20190923/20120614Ob1K6waikk.png
圖七:難道馬不見了也是我的錯,開什麼玩笑?

以上就是驗證過後的類別推論行為的機制,也符合完整性原則。

重點 2. 類別的型別推論機制 Type Reference in Class

若變數被指派的值為類別 C 建構出來的物件,則 TypeScript 會自動推論該變數之型別為 C

被推論出型別為 C 的變數符合廣義物件完整性原則

  1. 該變數不能夠新增屬性
  2. 該變數在原有屬性下不能指派錯誤的型別的值
  3. 完整覆寫該變數,指派的值必須是類別 C 建構出來的物件

類別的型別註記 Type Annotation of Class

恩,用 of 作為副標的介系詞應該會比 in 適合 <-- 這是 P 話不要理會筆者)

型別註記其實沒什麼好講的,就是以下幾種註記方式:

https://ithelp.ithome.com.tw/upload/images/20190923/20120614ynD7pIZhxV.png

這邊就留給讀者自己試試看囉。

我們還沒討論完全部的推論與註記行為!

哦哦哦哦~~接下來才是很整人的部分 —— 我們要討論的 Case 很多,不光只有討論單純類別的型別推論與註記行為,這樣交代實在是太不負責任啦~~~XD。(笑 P 笑

筆者要討論的狀況有這些:

  1. 普通類別之型別推論與註記行為(剛討論完)
  2. 繼承過後的類別之型別推論與註記行為
  3. 類別實踐介面時之型別推論與註記行為
  4. 類別實踐介面時,類別繼承之型別推論與註記行為

類別跟介面想當然會有 4 種組合。不過呢,類別也可以結合普通型別 —— 實踐 type 宣告的型別化名裡的靜態資料格式,硬要討論總共有 8 種組合(但絕對不會是八大行業)。

所以事實上,你也可以這麼做,這是之前沒提到的:

type SomeType = {
  message: string;
}

class SomeClass implements SomeType {
  /* ... 略 */
}

然而,筆者不鼓勵類別與型別 type 結合的原因還是一句話:“型別代表的意義是靜態資料格式,類別是針對物件的設計而有動態的行為,所以兩個概念相較之下 —— 根本是兩回事啊!”

通常類別會跟介面進行結合 —— 因為都是代表功能的規格與實踐,理所當然要討論類別跟介面合作時的推論與註記現象。既然筆者不鼓勵類別與型別合作,理所當然就會跳過這方面的說明~

繼承過後的類別之型別推論與註記 Type Inference & Annotation of Inherited Class

接下來筆者用簡單繼承過 Horse 的子類別,檢視子類別的型別推論機制。

另外,筆者會將 Horse 類別裡的 infoText 成員方法改成 protected 模式,為的就是讓子類別可以自創自己的 infoText 方法但是不被外部竄改。

程式碼如下:

https://ithelp.ithome.com.tw/upload/images/20190923/20120614S9KbHoRd2h.png

Unicorn 這個子類別的結構應該對讀者來說簡單吧!(筆者就不貼 Unicorn 的圖 XDDDDDD,應該會讓人感到可愛又崩潰

  • Unicorn 繼承 Horse 時,除了 name 必須讓使用者自訂外,其他的父類別要求的參數都補上去了(super 可以看成父類別建構子函式
  • Unicorn 覆寫了父類別的 infoText 的成員方法
  • 因為獨角獸會吐彩虹色嘔吐物,所以你可以叫 Unicorn 類別建立的物件去呼叫 puke 這個成員方法喔(這是什麼設定?

我們來建一個 Unicorn 物件來看看推論出來的型別。(變數 aRandomUnicorn 推論結果如圖八)

https://ithelp.ithome.com.tw/upload/images/20190923/20120614dSJMOVYHVY.png

https://ithelp.ithome.com.tw/upload/images/20190923/201206140EoFIotpC8.png
圖八:其實也不意外,一個 Unicorn 物件,不是 Unicorn 那會是啥呢 XD?

不過,你覺得這個 Case 成立嗎?

https://ithelp.ithome.com.tw/upload/images/20190923/20120614VDk8Ov5dyU.png

事實上是可以的,結果如圖九所示。

https://ithelp.ithome.com.tw/upload/images/20190923/20120614tHoqIXFfmi.png
圖九:UnicornHorse 的子類別,且 Unicorn 類別建構的物件可以被指派到被註記為父類別型態的物件

因此可以認定被註記為父類別的變數可以指派子類別的物件 —— 在原生 JS 裡的詮釋下 —— 它們隸屬於同一個原型鍊(Prototype Chain)下的產物。

但是,有註記與沒註記父類別仍然有差,而且差得可能比讀者想像中多!

筆者特別為 Unicorn 新增 puke 方法不是沒緣由的,我們來看以下程式碼被 TypeScript 檢測的狀況。(姐測結果如圖十;錯誤訊息如圖十一)

https://ithelp.ithome.com.tw/upload/images/20190923/20120614BKP8IBEFiJ.png

https://ithelp.ithome.com.tw/upload/images/20190923/20120614O2KnBgdwCW.png
圖十:結果被註記為 Horse 的變數,“踏破鐵鞋無覓處、不能恣意亂嘔吐” —— 簡直天地不容啊!

https://ithelp.ithome.com.tw/upload/images/20190923/20120614Tv6t28H8kg.png
圖十一:因為 TypeScript 參考的是 Horse 類別而不是 Unicorn 類別的成員,因此才會被 TS 警告

從這裡得知一個很重要的點:儘管被父類別註記的變數可以接收子類別建構的物件,但是子類別新增了父類別沒有的成員,該成員若被呼叫時 —— 會被 TypeScript 警告

但筆直還沒討論完,因為還有一個看似不起眼但會讓讀者感到怪的特性 —— 子類別有機會可以代表父類別型別

“WTx!作者你是指以下這樣的情形嗎?”

https://ithelp.ithome.com.tw/upload/images/20190923/20120614NTLdf7XtyB.png

首先,以上的程式碼 —— 一定會錯!但是錯誤訊息筆者用幾個字形容:“很有意思~”。

請看以下的結果!(圖十二為錯誤訊息)

https://ithelp.ithome.com.tw/upload/images/20190923/20120614SWnX6VpQnP.png
圖十二:筆者剛開始以為 UnicornHorse 的子類別,因此不能代表 Horse,不過這錯誤訊息到底在暗示什麼?!

這錯誤訊息透漏出一些很細節的概念:

Property 'puke' is missing in type 'Horse' but required in type 'Unicorn'.

恩!很有趣的一個問題 —— 如果父類別再額外定義 puke 方法就可以代表 Unicorn

如果是在父類別建構出來的物件 —— 手動對該物件新增屬性或方法就會破壞掉物件的完整性,一定會被 TypeScript 開罰單!

另外,筆者根據上面的錯誤訊息,提出的回應是:

如果子類別 Cinherited 繼承父類別時,並沒有額外定義出更多成員,亦或者是只有覆蓋父類別的成員但沒有做其他事情,那麼是不是代表父類別創建出來的物件型別等效於該子類別 Cinherited

於是筆者快速建構一個子類別 Stallion 並直接對 Horse 繼承,但沒有做任何其他的事情:

https://ithelp.ithome.com.tw/upload/images/20190923/20120614tZnEYE5oCY.png

我們來測測看以下的程式碼。(結果如圖十三)

https://ithelp.ithome.com.tw/upload/images/20190923/20120614OIqcSIav89.png

https://ithelp.ithome.com.tw/upload/images/20190923/20120614wvSSRhhFfr.png
圖十三:哇塞!沒有錯誤,儘管積極註記為 Stallion,但是指派它的父類別 Horse 竟然也是通過的!

因此筆者把剛剛展示過的東西寫成一個重點供讀者做筆記用:

重點 3. 類別繼承之型別推論與註記

假設宣告某類別 C,另外再宣告 C_Inherit 為繼承 C 的子類別,則:

  1. 子類別 C_Inheirt型別推論機制跟普通類別的型別機制一模一樣(查看本篇重點 2)
  2. 若變數 A 被註記之型別為父類別 CA 除了可被指派 C 類別建構出來的物件外,子類別 C_Inherit 建構出來的物件也可以被指派到 A
  3. 若變數 B 被註記之型別為子類別 C_Inherit —— 在 C_Inherit 繼承父類別 C 的過程中,並未額外定義 C 本身沒有的成員的條件下,父類別 C 所建構出來的東西可以被指派到變數 B

型別等效假說 Typed Equivalence Hypothesis

貼心小提示

這個標題是筆者自立的,並不是參考任何 Document 或者是外來網站,讀者只會在本系列看到這個假說。

另外,物件完整性理論也是筆者自己在本系列訂立的,為的是解說方便,將一大坨概念抽象化的結果。

所以你幾乎不會在外面的 TypeScript 系列看到這些名詞。

從以上的範例,筆者又要大膽地再立一個假說:類別的型別判定原則與判定類別的成員格式沒兩樣 —— 所以兩個不同類別建造出來的物件,其物件型別的判定與類別名稱無關,只要格式相同就算通過

類別繼承過後的推論機制,不是以原型鍊(Prototype Chain)或者是類別名稱來辨識,而是以類別創造出來的物件格式判定是否為同一型別

(哎呀... 有點繞口令,讀者看不懂的話~可以看以下的範例)

想要真的讓以上那句話成立,我們要講一個更極端的案例 —— 某兩個類別的宣告僅僅只是名稱不同但是成員結構等等都相同,那麼型別是否會間接等效

https://ithelp.ithome.com.tw/upload/images/20190924/201206143mfhch7dH0.png

以上的 C1C2 類別都只有 public 模式的成員。我們來測試看看下面會不會通過。(測試結果如圖十四)

https://ithelp.ithome.com.tw/upload/images/20190924/20120614zqshuP0OgE.png

https://ithelp.ithome.com.tw/upload/images/20190924/20120614KGHBebj6Mo.png
圖十四:筆者簡直要吐血啦~還真的通過!

所以意思是說:只要結構相同就會通過嗎!?

當然~(哦哦哦~所以是ㄧ樣囉?)~~~~~~ 答案不是這樣,請看以下的範例(錯誤訊息如圖十五):

https://ithelp.ithome.com.tw/upload/images/20190924/201206148nCtdhit47.png

https://ithelp.ithome.com.tw/upload/images/20190924/20120614PdTxRnWOzY.png
圖十五:結果 private 模式因為可以被自訂任意的行為,因此被 TypeScript 判定型別格式不等

因為 private 模式下的成員會使得類別的使用形式被改變,因此不能夠等效於其他格式相同的類別呢!看來剛剛的假說是有條件的~

此外,如果讀者測試改成 protected 模式,也會出現類似的錯誤訊息喔!

重點 4. 類別型別等效理論

若宣告兩個類別 C1C2 —— 其中 C1C2成員皆為 public 模式,並且所有的成員名稱對應型別皆相同,TypeScript 判定 C1 型別等效於 C2 型別。

讀者試試看

其實,根據類別等效理論的結果可以再反推 —— 連型別跟介面,只要格式一模ㄧ樣,都可以被等效。以下寫幾個範例讓讀者研究一下,看看這些程式碼能不能動作吧!

https://ithelp.ithome.com.tw/upload/images/20190924/20120614AOuoJu4GvT.png

小結

今天已經完整講完整個類別的推論與註記機制~

若類別跟介面結合的話,會產出什麼樣的型別推論機制呢?我們就留到下一篇來看看囉!

]]> Maxwell Alexius 2019-10-04 15:38:10
Day 23. 機動藍圖・私有建構子 X 單身狗模式 - Private Constructor & Singleton Pattern https://ithelp.ithome.com.tw/articles/10218770?sc=rss.iron https://ithelp.ithome.com.tw/articles/10218770?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

  1. 還記得存取修飾模式(Access Modifiers)有哪些嗎?
  2. 你有想過 private 除了類別成員與類別的靜態屬性與方法外,還有哪些地方可以使用呢?(提示:今天的文章標題)

如果還沒理解完畢的話,可以先翻看類別存取修飾篇章靜態成員篇章喔!

今天的標題筆者亂打 XD,但 Singleton Pettern 單例模式 —— 也就是今天要講到的 Design Pattern 的應用之一,確實在中文翻譯上很難翻得好聽,因為 Singleton 本身就帶有單身漢的意思。通常會翻譯成單例、單子或獨體模式,讀者如果上網查不外乎會看到這些翻譯。

不過在講到今天的主角前,ㄧ樣先從過往學到的東西出發~

以下,正文開始

私有建構子的應用 Private Constructor

私有存取模式+建構子函式 = 私有建構子

標題很直觀,就是將類別裡的建構子加上 private 這個修飾模式就可以了。(今天重點下得倒挺快的,之前為了鋪陳還要講一大堆東西)

重點 1. 私有建構子 Private Constructor

私有建構子 —— 顧名思義,就是將類別的建構子設定成 private 模式。若某類別 C,其建構子設定成 private 模式,如下:

https://ithelp.ithome.com.tw/upload/images/20190923/20120614DmtE7ejqDc.png

其中,因為 C 的建構子被設定成 private 模式了,因此我們不能夠從該類別 C 進行建構物件的動作:new C(/* 參數 */) 在類別的外部不能被使用

讀者云:“這是叫我們怎麼使用類別啊?”

是的,一但將類別建構子私有化不能在外面建構物件!(沒聽過黨產嗎?—— “反對的舉手” ... “沒有!” ... “沒有!” ... “沒有!” ... “好!一致通過!”

(以下是簡單的私有建構子的範例;TypeScript 檢測結果如圖一)

https://ithelp.ithome.com.tw/upload/images/20190923/20120614Q3y0xBwWrt.png

https://ithelp.ithome.com.tw/upload/images/20190923/20120614SB9vYQo4rR.png
圖一:就連建構子都被封裝到類別裡面去了(這句話筆者覺得特性跟克萊因瓶 (Klein Bottle)很像 XD)

沒看過私有建構子的讀者肯定覺得莫名其妙,不過筆者繼續推展下去。

單例模式 Singleton Pattern

筆者一樣開始引導讀者的思路:

通常會把類別的建構開口給封住的原因會是什麼呢

於是有幾個想法可能會冒出來:

  1. 類別成員或類別的靜態屬性方法間接建立物件嗎?
  2. 愚人節整人用 XD?
  3. 恩 ... 不是用來建構物件用?
  4. 根據類別靜態成員篇章,是要單純模仿類似 Math 物件,寫一些由類別本身提供的功能嗎?
  5. 拿來生產更多的 Bug XD?
  6. 這是黨產,你不能碰!(夠了沒?)

當然,第 4 點的想法,用類別本身的靜態屬性寫出一系列的功能,仿造 JS 裡的 Math 物件是可以的喔!不過就看個人需求要不要這麼做~

今天要講的主題是單例模式 —— 以上列出的幾個想法,隱隱約約產出了幾個重點關鍵字:『 靜態屬性與方法 』以及『 不是拿來建構物件用 』

筆者就直接從中點出單例模式的特點:若某類別採取單例模式,則該類別產出的物件會是全域裡面的唯一個體

以下是單例模式的示範程式碼:

https://ithelp.ithome.com.tw/upload/images/20190923/201206148LpG7HgLKc.png

筆者簡單敘述一下到底這是在做什麼:

  • SingletonPerson 為使用單例模式的類別。單例模式的實踐方法 —— 必須具備私有建構子,防止外部的人私自建構更多該類別的物件
  • SingletonPerson 的建構子裡面有三個成員變數:nameage 以及 hasPet;儘管是 public 模式,但是不能夠被覆寫,因為也被標註為 readonly
  • SingletonPerson 有一個私有靜態屬性名為 Instance,存放的是單例模式下:唯一一個被建構的物件;當程式碼讀進去的時候,立即被建構起來。
  • SingletonPerson 有一個公用靜態方法名為 getInstance,其實就是把唯一的建構出來物件叫出來

(另外讀者可以注意到的一點是,Instance 對應型別與 getInstance 方法輸出的對應型別都是 SingletonPerson,有關於類別也是型別的一種 —— 這點筆者還沒清楚說明過,後續會再補足)

我們可以用 SingletonPerson.getInstance() 的方式去呼叫 SingletonPerson 唯一建構出來的物件。(以下程式碼編譯並使用 node 執行過後結果如圖二)

https://ithelp.ithome.com.tw/upload/images/20190923/201206149erAExX8lf.png

https://ithelp.ithome.com.tw/upload/images/20190923/20120614xxYHzZUVyh.png
圖二:我們可以從 getInstance 類別靜態方法取得單例模式下建構出來的唯一物件呢

重點 2. 基本的單例模式實踐 Singleton Pattern

若某類別 SingletonC 實踐單例模式,則必須符合:

  1. SingletonC 的建構子函式為 private 模式
  2. SingletonC 必須要有私有靜態屬性專門存放單例模式下的唯一物件(該物件又被稱為 Singleton,或單子),習慣上該靜態屬性的名稱為 Instance
  3. SingletonC 必須要有公用靜態方法負責把單子回傳出來,是唯一一個取得單子的途徑,習慣上該靜態方法的名稱為 getInstance

https://ithelp.ithome.com.tw/upload/images/20190923/201206149tTbJOsicQ.png

單例模式的目的與意義

基本上,從剛剛的範例應該可以推測出單例模式可以解決到的問題,最明顯的莫過於:

確保該物件(單子)在任何地方的單一性,並且針對該物件提供統一的成員方法

比如說,我們可能會讓專案的設定檔(Configuration File)成為全域裡 —— 唯一的一種物件。

注意多執行緒的環境 Multithreaded Environment

另外,在任何語言 —— 尤其是多執行緒(Multithreaded)的環境下,有可能會出現同時有多個執行緒呼叫到 new SingletonInstance,因此產生了兩個以上的單子 —— 這是不合理的行為,所以如果讀者轉換到其他語言,想要實踐單例模式,必須注意有沒有發生的可能性(大部分都會,現在幾乎都是多執行緒的環境了)。

從 NodeJS 來看,儘管 Node 本身是多執行緒(還要顧慮一些 I/O 等事情),但執行 JavaScript 的程式碼過程本身是單執行緒,而這也是因為當初 Google 開發 V8 引擎時的限制,不是 NodeJS 本身的問題喔。

單例類別的繼承 Singleton Class Inheritance

單例模式事實上是可以被繼承的 —— 讀者可能想說:“建構子都被鎖住了,繼承後的子類別頂多也需要父類別的建構子在 protected 模式下才能夠覆寫啊?”

StackOverflow 裡確實有人提議 —— 將 constructor 設定為 protected 模式,子類別就可以覆寫父類別的建構子函式。

不過筆者查閱了以下這本物件導向的設計模式原著翻譯本(如圖三)。

https://ithelp.ithome.com.tw/upload/images/20191003/201206141DBgjdPp03.jpg
圖三:當時 OOP 圈熱門的 Gang of Four —— 四人幫整理出的 23 種不同設計模式匯集在這本書裡

裡面寫道對於單例類別的繼承的用意是:

(前面有兩點筆者略過)

  1. 操作和內部結構仍有內部空間。Singleton 類別可被繼承,我們可用其子類別的物件個體輕鬆地設定應用程式組態,也可以在執行期動態選擇要用哪一種 Singleton 子類別的物件個體。

  2. 允許不定個數的物件個數。同理,...(筆者後面略過)

這裡作者們(因為是四人幫)已經闡明:

繼承的用意在於擴充單子個體的功能,並沒有叫你覆寫父類別的建構子

所以那些網路上跟讀者講說:“你可以使用 protected 模式的建構子,繼承過後進行覆寫父類別的建構子的動作” —— 被筆者俗稱網路偏方,嚴重模糊焦點甚至講錯觀念叫做網路謠言(我們知道的假消息、假新聞等等) —— 就好像聽說手機的電磁波會讓人致癌,如果是這樣,那全世界的人都 GG 了,因為衛星發出的電磁波散佈在各地,穿梭人體來去自如!

筆者的目的是要讓讀者知道:有些資訊必須去求證,就算是吹毛求疵也好,不能單純只有 Gxxgle 來 Gxxgle 去(聽說 Gxxgle 是被註冊過的商標?)—— 結果被灌到錯誤的資訊,而網路戰的由來,其中一種就是去 Exploit 一個群體的資訊落差然後再進行內部分化。(內容超展開

儘管我們在開發的過程中遇到很多 Bug 必須上網查詢解決,但是最重要的是 —— 任何可以求證資訊的機會不要放過

另外,時常還有人認為(筆者就是其中之一,看完原著的書反省了三天三夜,丟臉到膝蓋都不見了):

(以下是錯誤觀念)
你對單例模式的類別繼承過後,因為有了複數個子類別等同於違反了單一物件個體建構的原則,也就是 Singleton 本身的意涵 —— 要維持單一物件的狀態

原著的意思是:

“單例類別被繼承的目的:除了可以擴充個體的功能外,多個子類別不同的單例個體可以抽換運用

這是一種更彈性的作法。請再重看第三點被筆者特別匡起來的部分就可以知道:

  1. 操作和內部結構仍有內部空間。Singleton 類別可被繼承,我們可用其『 子類別的物件個體輕鬆地設定應用程式組態,也可以在執行期動態選擇要用哪一種 Singleton 子類別的物件個體 』。

讀者可以另外上網查有關於某些開發者認為單例類別不能繼承這一個觀點 —— 那些都是錯的、亦或者是根本連原著都沒看過。其中有一篇 Medium 文章(筆者不貼,讀者自己去找,而且不只是一些 Medium 文章,部落格等等可以找),它就這樣大膽闡述:

Notice how the constructor is made private to eliminate the ability to make an instance of the class outside the Singleton class. If you look carefully you will notice that the class is made final as well, this to indicate that the class cannot be inherited (private constructor also inhibit inheritance).

以上簡單翻譯是:『 注意到因為單例類別的建構子已經被私有化,代表它是不能被繼承的

請讀者以後看到類似的話,果斷對那篇文章說不。(但你不需要防狼噴霧,或者是可以選擇請它進 OOP 設計模式勞改營

重點 3. 單例模式的意義與注意事項

確保某物件(單子)在任何地方的單一性,並且針對該物件提供統一的成員方法。

多執行緒下的環境,必須確保單例模式下的類別,不會因為兩個執行緒以上同時讀到類別裡 —— 建構物件的表達式而違反單例模式的初衷。

另外,對單例模式的類別進行繼承的動作並不違反單例模式的初衷。在單例模式下運用繼承的目的有二:

  1. 子類別可以擴充該個體的功能(可能擴充靜態方法等)
  2. 多個子類別的單例物件可以在程式中隨時抽換

最後,建議不要將單例模式的建構子函式設定成 protected 模式,因為這並不是設計模式原著主張的東西,而是網路偏方

以下附上書籍寫的內容,確保筆者沒有從其他消息來源造假,但筆者鼓勵讀者買書來看 XD。(如圖四)

https://ithelp.ithome.com.tw/upload/images/20191003/201206148ohAfQd8jX.jpg
圖四:《物件導向設計模式 —— 可再利用物件導向軟體之要素》—— Page. 146 節選內容

懶漢模式 Lazy Initialization in Singleton Pattern

(這真的就是維基翻譯出來的名稱,不要討伐筆者 XD)

由於單例模式是在剛初始化類別的時候就順便將單子給建構好。有時候會遇到單子建構過程中會耗費龐大的資源;另外,如果剛開始也不急著去建構單子物件亦或者是單子物件的需求性不高 —— 搞不好在整個程式的運轉當中也不需要單子物件,所以建構好的單子也會被視為浪費資源的可能性的話,那麼我們還有單例模式的變體 —— 懶漢模式

其實懶漢模式的概念很簡單 —— 第一次呼叫到 SingletonClass.getInstance 這個靜態方法時,到時候再建造就好了,於是就出現了以下的程式碼。

https://ithelp.ithome.com.tw/upload/images/20190923/20120614dvu3rZYpNu.png

你可以看到我們的 Instance 的型態為 null 型別的 union的複合型別 —— 代表剛宣告單例模式的類別時,先不要把單子建構出來,而是用 null 值來代替。

等開發者第一次呼叫到 getInstance 靜態方法才正式把單子給建構好。

這也是一種 Lazy Initialization 的應用概念啊!

重點 4. 單例模式變體 —— 使用延遲初始化技巧

如果碰到單子不急著被建構出來的情形,可以採取呼叫 getInstance 方法才建構單子的模式。

https://ithelp.ithome.com.tw/upload/images/20190923/20120614B9MTtMvT7Y.png

小結

今天主要講解跟類別有關的設計模式的應用,單例模式其實很好理解,就是確保物件的單一性。

另外就是:闢謠!闢謠!闢謠!—— 這點非常重要!

下一篇又會回歸要講解類別的型別推論與註記機制(Type Annotation & Inference),這一部分會跟《前線維護》系列調性很像~但還是非常重要,筆者很雞婆的還是得說:“因為這是 TypeScript 主打的 Feature 啊!”

]]> Maxwell Alexius 2019-10-03 15:29:05
Day 22. 機動藍圖・特殊成員 X 存取方法 - TypeScript Class Accessors https://ithelp.ithome.com.tw/articles/10218638?sc=rss.iron https://ithelp.ithome.com.tw/articles/10218638?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

  1. 類別的靜態成員(Static Members)是什麼?與普通成員差異在哪?
  2. 什麼情況下會採用靜態成員的設計呢?

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

有些沒有 OOP 經驗的讀者可能覺得 —— 類別要講的東西怎麼特別多

是很多沒錯 XD —— 會用倒是覺得挺好用的~ 尤其結合 TypeScript 介面可以寫出還蠻牛逼的程式碼。

因此本篇就~正文開始吧!

取值方法與存值方法 Accessors

特殊的類別成員

筆者照樣用簡單的方式舉例,就以昨天講到的 CircleGeometry 類別進行延伸。以下是它的程式碼:

https://ithelp.ithome.com.tw/upload/images/20190921/20120614jKQP2nFngA.png

今天來搞一些神奇的功能 —— 假設每次計算圓形的面積時,與其呼叫物件的 area 方法,我們能不能夠改成類似呼叫物件屬性的方式計算出面積呢?

一種可行方式是這樣:

https://ithelp.ithome.com.tw/upload/images/20190921/20120614Cqr8K2aem7.png

於是可以這樣使用 CircularGeometryV2。(編譯並且用 node 執行結果如圖一)

https://ithelp.ithome.com.tw/upload/images/20190921/20120614hp0fLcgpBF.png

https://ithelp.ithome.com.tw/upload/images/20190921/20120614oYZeyyFJtw.png
圖一:正常地印出半徑為 2 的圓之面積的結果

不過這樣有兩個缺點:

  1. 我們可以直接用 randomCircle.area = 某數字 來強行覆寫掉面積的值,使得計算結果被破壞掉
  2. 在建構子裡面進行運算事實上是不建議的,因為建構子的目的是初始化物件的成員變數們

當然可以仿造過往的手法,把 area 換成 private 模式,並且宣告 getArea 的公用方法讓使用者可以呼叫並取得 area 的值。但這就跟本篇一開始想要實踐出的結果相違背啊!

於是筆者就把今天的主角請出場~存取方法(Accessors)。

使用存取方法 Accessors

存取方法分成兩種:取值存值,也是時常聽到的 Getter Methods 或 Setter Methods

貼心小提醒

儘管在 JS 裡將 Accessors 存取方法分別稱為 Getter/Setter Methods(取值/存值);然而某部分語言會將 Accessors 代表取值方法,而 Mutators 則是代表存值方法 —— 因此不是用 Getter/Setter 名稱之分,而是以 Accessor/Mutator 這樣的名稱來分。

根據今天想要達成的事情:想要藉由呼叫物件的 area 屬性取得圓的面積的值 —— 這裡的關鍵功能是『 取值 』,因此筆者先來介紹 Getter Method 怎麼使用。

以下是正確的實踐方式:

https://ithelp.ithome.com.tw/upload/images/20190921/20120614ak3LLhXxKb.png

讀者可以發現,取值方法的實踐比想像中的簡單,就是get 關鍵字搭配想要取的名稱

再次測試使用 CircleGeometryV2 的結果如圖二。

https://ithelp.ithome.com.tw/upload/images/20190921/201206143La4G9Hp4W.png
圖二:使用 get area 的特殊取值方法結果正常

若我們強行對 area 進行覆寫的動作,會被 TypeScript 送出罰單。(檢測結果如圖三;錯誤訊息如圖四)

https://ithelp.ithome.com.tw/upload/images/20190921/20120614Dg0EKv95qy.png
圖三:TypeScript 會警告你,屬性 area 是不能被覆寫的

https://ithelp.ithome.com.tw/upload/images/20190921/20120614dAEiuXL1FX.png
圖四:其中,TypeScript 吿訴我們的訊息很有趣 —— area 是唯讀的!(Read-only)

這裡出現了一個很有趣的結果:area 是唯讀的屬性,也就是說 —— 如果單純只有定義取值方法(Getter Method),它本身就是唯讀的狀態

另外,使用存取方法(Accessors)令人感到方便的地方是 —— 只要物件的狀態被改變,存取方法們計算過後的值也會被自動更新!

主要是因為,一但那些藉由存取方法定義過後的物件屬性 —— 呼叫該屬性時,會根據存取方法裡面寫的程式進行計算

所以測試以下的程式碼,radius 的屬性的值被改變後,area 屬性不需要主動去寫程式碼更新狀態,它就會根據取值方法(Getter Method)裡的內容計算出來。(以下程式碼編譯與 node 執行結果如圖五)

https://ithelp.ithome.com.tw/upload/images/20190921/20120614T69qbSRtSv.png

https://ithelp.ithome.com.tw/upload/images/20190921/20120614LXO6XdLuuL.png
圖五:儘管 radius 被強行改變,我們的 area 也跟著被計算出來

讀者試試看

試著把 circumference 專門計算圓形周長的方法改成用 Getter Method 的方式實踐出來。

另外,我們還可以使用存值方法(Setter Methods)去更改物件屬性被指派值的狀況。這是什麼意思?

假設想將剛剛的 CircleGeometryV2area 屬性 —— 原本是唯讀狀態 —— 改成每一次被指派值時,其半徑值 radius 也會跟著被調整成正確的值。因此,筆者這裡就直接示範使用 Setter Method 來達成剛剛所敘述的功能。

https://ithelp.ithome.com.tw/upload/images/20190921/20120614P5o6ponkgk.png

從以上的程式碼可以看出 —— 相對於取值方法 Getter Method,定義一個存值方法 Setter Method 則是用 set 關鍵字!

其中,要注意的一點是:因為存值方法專門在模擬屬性被指派值的情況因此會需要一個參數去代表被指派的值,所以存值方法的型別會多一個參數 (value: number): void

以下筆者來驗證看看 CircleGeometryV2 的存取 area 這個屬性的行為會不會連同物件的半徑 radius 被更改。(編譯並且使用 node 執行之結果如圖六)

https://ithelp.ithome.com.tw/upload/images/20190921/20120614vljClcGrMm.png

https://ithelp.ithome.com.tw/upload/images/20190921/20120614lrXVHTuCb5.png
圖六:對 area 指派值,radius 也自動被更改了!

存取方法的型別推論與註記 Accessors Type Inference & Annotation

另外,筆者必須提到跟型別系統有關的重點(畢竟這可是 TypeScript 的主打 Feature 啊!)。

由於在 area 對應的取值方法(Setter Method)裡,其參數對應的是數字 number 型別 —— 也就是說,如果指派錯誤的型別到 area 去,TypeScript 也會自動幫我們監控喔!(偵測結果如圖七;訊息如圖八)

https://ithelp.ithome.com.tw/upload/images/20190921/20120614nDgOEC6N2P.png
圖七:如果指派不為 number 型別的值到 area 屬性,由於 area 的存值方法的參數特別被註記為 number 型別,因此會出現錯誤

https://ithelp.ithome.com.tw/upload/images/20190921/20120614Y2Y4KjpeBW.png
圖八:很明顯地,TypeScript 告訴我們不能將字串丟進去

相對於存值方法,取值方法也是有型別推論與註記的機制。如果對於函式型別篇章夠熟悉的話,TypeScript 會自動推論函式的輸出型態

所以以下的程式碼,areaOfCircle 應該會被自動推論為數字 number 型別。(推論結果如圖九)

https://ithelp.ithome.com.tw/upload/images/20190921/20120614qE1cTPd6Or.png

https://ithelp.ithome.com.tw/upload/images/20190921/20120614ycE6xuPf1p.png
圖九:取得 area 的值時,也會被自動地被 TypeScript 推論出型別來喔!

存取方法的限制

取值方法(Getter Method)不能有任何參數:因為是模擬呼叫屬性的方式進行物件的取值,當然不會有參數的出現喔!(錯誤訊息如圖十)

https://ithelp.ithome.com.tw/upload/images/20190921/20120614MDBxMpkGp0.png

https://ithelp.ithome.com.tw/upload/images/20190921/20120614e2pqvANsJv.png
圖十:TypeScript 扳著臉跟你講,get Accessor 不能有任何參數

另外,取值方法的用意是模擬呼叫物件的屬性,因此 —— 取值方法沒有回傳任何值是錯誤的行為!(檢測結果如圖十一)

https://ithelp.ithome.com.tw/upload/images/20190921/20120614RVnzlCmnIZ.png

https://ithelp.ithome.com.tw/upload/images/20190921/201206141YLPKfIBrZ.png
圖十一:TypeScript 會自動告訴你,你忘記回傳值了!

相對地,存值方法(Setter Method)只能有一個參數:因為是模擬指派任何值到屬性的方式進行物件的存值,而指派任何值只會被當成一個參數。(以下程式碼錯誤訊息如圖十二)

https://ithelp.ithome.com.tw/upload/images/20190921/20120614z5sasLCfC6.png

https://ithelp.ithome.com.tw/upload/images/20190921/20120614UNb65Kq61E.png
圖十二:TypeScript 也會提醒你不能有更多參數在 set Accessor 裡面喔

重點 1. 類別的存取方法 Accessors

分成兩種:取值方法(Getter Method)與存值方法(Setter Method)。

  1. 取值方法專門在模擬呼叫物件的屬性時的行為;存值方法則是在模擬指派值到物件屬性的行為:由於兩者皆是用方法的方式來呈現屬性的呼叫與指派行為,因此才會被稱為存取方法。(而不是存取屬性)
  2. 若只有單純實踐某物件屬性的取值方法(Getter Method)而沒有相對應的存值方法,該屬性可以模擬唯讀(Read-only)的狀態
  3. 取值方法的實踐不能有任何參數。若某屬性是利用取值方法來模擬的話,呼叫該物件的屬性,型別推論的結果會等同於取值方法回傳的值之型別。又因為是在模擬物件取值的過程,因此不回傳值的行為也是錯誤的
  4. 存值方法只能有一個參數,而該參數代表的值是指派的值。根據函式型別篇章提出的重點,我們必須對存值方法內部的參數進行積極註記的動作。若某屬性的指派行為是用存值方法模擬,則該屬性被指派錯誤的值也會根據存值方法的參數被註記到的型別進行比對。
  5. 若想要在類別 C 宣告某存取方法模擬物件呼叫或指派值到屬性 P,其中 P 必須被指派的值之型別為 Tassign,則程式碼的格式為:

https://ithelp.ithome.com.tw/upload/images/20190921/20120614dOeiJ4DOVl.png

題外話:readonly 也是可以在類別內使用的

剛剛提到可以單純實踐取值方法(Getter Method)就可以模擬唯讀(Read-only)屬性的行為。

不過,在介面與型別裡可以用的 readonly 關鍵字在類別裡的成員變數裡也可以用

假設我們的 CircleGeometryV2 裡 —— 圓周率 PI 原本是 private 模式,但是想要讓它既可以開放讀取但又可以防止被更改的話,可以直接加註為 readonly 並且宣告為 public 模式喔!

https://ithelp.ithome.com.tw/upload/images/20190921/20120614Idb9zCpids.png

我們可以在外面取得 PI 的值但是確保它不會被更改。(檢測結果如圖十三;錯誤訊息如圖十四)

https://ithelp.ithome.com.tw/upload/images/20190921/20120614yFua7jXGf0.png
圖十三:如果 PI 被覆寫的話,就會被 TypeScript 提醒!

https://ithelp.ithome.com.tw/upload/images/20190921/20120614iXWVNf940h.png
圖十四:TypeScript 提醒你,物件的 PI 屬性是 read-only 狀態喔

再者,不只是普通的成員變數,連**類別的靜態屬性也可以被標註為唯讀模式**呢~

https://ithelp.ithome.com.tw/upload/images/20190921/20120614xZHo2Cxxot.png

所以如果強行覆寫 CircleGeometryV2 就會被警告呢。(檢測結果如圖十五;錯誤訊息如圖十六)

https://ithelp.ithome.com.tw/upload/images/20190921/20120614mcG3S2gRp8.png
圖十五:有標註 readonly 就可以防止被外部覆寫

https://ithelp.ithome.com.tw/upload/images/20190921/20120614csrYlq9NGP.png
圖十六:CircleGeometryV2.staticPI 是唯讀(read-only)狀態,錯誤訊息也寫得好好的

重點 2. 類別裡使用 readonly

可以在類別的成員變數(Member Variables)與靜態屬性(Static Properties)標註 readonly,代表該成員變數或靜態屬性是唯讀狀態。

小結

今天我們又把類別的一項功能講完了 —— 也就是存取方法(Accessor)。

下一篇要講到一個很有趣的設計模式應用 —— 單例模式(Singleton Pattern),明天見~~~

]]> Maxwell Alexius 2019-10-02 14:39:51
Day 21. 機動藍圖・靜態成員 X 即刻操作 - Static Properties & Methods https://ithelp.ithome.com.tw/articles/10217952?sc=rss.iron https://ithelp.ithome.com.tw/articles/10217952?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

  1. 如何使用類別的繼承(Inheritance)?
  2. 為何我們設計類別的成員時,會儘量以 private 模式為基準?什麼時候該開放成員給外部使用呢?
  3. privateprotected 模式間的最大差別在哪呢?
  4. 通常子類別建構物件時,要如何連結父類別初始化物件的邏輯呢?
  5. 使用 super 有沒有特別需要注意的事項呢?

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

今天講的東西不會像昨天討論類別繼承一樣 —— 篇幅太大。(筆者也被折騰了一番

正文開始

類別的靜態屬性與靜態方法 Static Properties & Methods

靜態跟動態意義差別在哪?

首先,有些人可能對靜態(Static)與動態(Dynamic)這兩個詞在類別的概念會搞混。不過大部分在講 OOP 時我們不會刻意提到動態屬性與方法,那是因為我們早就在使用了。

其實最簡單的想法是這樣:

類別新增物件時的成員變數與成員方法 —— 皆可被當成動態屬性與方法

所以那些類別成員們(Class Members)都是被形容成動態的概念,筆者是這麼看待的。

那為何還要分動靜態?靜態又是什麼概念?

假設想要寫一個類別是專門計算幾何圖形中 —— 跟圓形有關的類別,以下簡單的把程式碼寫出來進行示範。

https://ithelp.ithome.com.tw/upload/images/20190921/20120614wBXy6bBz4H.png

以上的程式碼的說明如下。

CircleGeometry 的成員變數事實上有兩個(不要看到建構子函式裡面只有一個參數就認定只有一個成員變數喔!):

  • PI 被設為 private 模式代表圓周率 3.14
  • radius 代表圓的半徑,並且需要經過建構子進行初始化的動作

CircleGeometry 的成員方法也有兩個:

  • area 代表計算圓的面積,其型別為 (): number
  • circumference 代表計算圓的周長,其型別也是 (): number

這邊筆者認為讀者可以自行試試看如何使用這個類別建立物件並且呼叫方法,所以以下的程式碼暫且就不測了。

https://ithelp.ithome.com.tw/upload/images/20190921/201206149j5dOF4erJ.png

好的!上述的 CircleGeometry 的成員們為何會被認定並非靜態的主要原因是:在建構物件的時候,我們的成員變數的值會不定

譬如可以建立半徑為 2 的圓 —— new CircleGeometry(2);也可以建構半徑為 2.71828 的圓 —— new CircleGeometry(2.71828) 以及更多無限種案例。每一次建立半徑不同的圓,物件的成員方法 areacircumference 也會計算出不同的結果。

儘管類別藍圖提供的成員們都是一樣的格式,但是每一次建構出的新物件 —— 該物件的值以及呼叫方法出現的結果可能也會變得不同,因此讓人感覺到物件的狀態是動態的

如果想要了解相對於物件的狀態是動態的意義,必須把焦點轉換為:把類別本身看待成一個物件 —— 類別本身的屬性與方法,這些東西就被稱作是靜態屬性與方法

類別本身的屬性與方法

筆者換個角度來解釋靜態屬性與方法。

讀者應該看過 JavaScript 裡面的 Math 這個物件吧 ~ 但它跟普通類別的使用方式不同 —— 它不需要用 new 去建構物件然後呼叫方法,而是直接提供 Math 本身的屬性與方法讓開發者使用

https://ithelp.ithome.com.tw/upload/images/20190921/201206141nhKr1KNLH.png

我們可以稱 Math.PI 中的 PIMath 這個類別的靜態屬性(Static Property);相對地,那些 Math.randomMath.sinMath.pow 與更多從 Math 延伸出來的方法都統稱為 Math 這個類別的靜態方法(Static Methods)。

重點 1. 類別的靜態屬性與方法

不需要經由建構物件的過程,而是直接從類別本身提供的屬性與方法,皆稱之為靜態屬性與方法,又被稱為靜態成員(Static Members)。

因此,靜態成員具有一個很重要的特點:不管物件被建立多少次,靜態成員只會有一個版本 —— 這也符合靜態的概念:固定、單一版本、不變的原則等等。

通常會使用靜態成員的狀況:

  1. 靜態成員不會隨著物件建構的不同而隨之改變
  2. 靜態成員可以作為類別本身提供的工具,不需要經過建構物件的程序;換句話說:類別提供之靜態成員本身就是可被操作的介面

宣告靜態成員 static

想要仿造 Math 類別 —— 我們可以使用 static 關鍵字將 CircleGeometry 裡的成員轉換成靜態成員們。

https://ithelp.ithome.com.tw/upload/images/20190921/20120614cWc7QdFQzV.png

要注意的是:由於 PI 從成員變成了靜態成員,因此不能使用 this.PI,而是 StaticCircleGeometry.PI 來取用。

那是因為:靜態成員是綁在類別身上,並非類別建立起來的物件 —— 因為不需要經過 new 建立物件的過程就可以從類別身上取用!

再來,因為捨棄了原本建構新物件時,必須提供 radius 作為物件的成員變數這個管道 —— 沒了物件本身建構後的 radius 屬性,所以必須改寫 areacircumference 這兩個方法:將型別從原本的 (): number 變成 (radius: number): number,並且從 public 成員方法變成靜態方法。

以下比對 CircleGeometryStaticCircleGeometry 之間的使用差別。(編譯過後並且使用 node 執行結果如圖一)

https://ithelp.ithome.com.tw/upload/images/20190921/20120614Nb2dej3OPo.png

https://ithelp.ithome.com.tw/upload/images/20190921/20120614aGAXeQ1zIh.png
圖一:結果都ㄧ樣,但實踐的過程不ㄧ樣罷了

重點 2. 宣告與使用靜態屬性與方法 Static Properties & Methods

若想要在某類別 C 宣告的靜態屬性 Pstatic 與方法 Mstatic,程式碼格式如下:

https://ithelp.ithome.com.tw/upload/images/20190921/20120614hXG2XlkkJa.png

若想使用類別 C 的靜態屬性 Pstatic 與方法 Mstatic,則可以直接從 C 呼叫:C.PstaticC.Mstatic(/* 參數... */)

static 與存取修飾子 Access Modifiers

事實上,靜態成員跟普通類別成員有類似的地方 —— 都是可以設定存取模式

假設想要防止使用者竄改 StaticCircleGeometry 裡面的靜態屬性 PI 之值,並且提供另一個管道跟讓使用者得知它的值為多少,我們可以這麼做:

https://ithelp.ithome.com.tw/upload/images/20190921/20120614r2G4IZEg0P.png

讀者可以發現 private static 的概念很簡單:private 模式可以封裝我們的屬性與方法,加在 static 旁邊就只是告訴開發者不能亂動這個靜態成員。因此類別裡面是可以使用靜態屬性 —— 因此 areacircumference 這兩個方法裡面就算去呼叫 StaticCircleGeometry 是不會出現錯誤的,但是如果你在外面呼叫就會被 TypeScript 警告喔!(被 TypeScript 查驗結果如圖二;錯誤訊息如圖三)

https://ithelp.ithome.com.tw/upload/images/20190921/20120614R5y0IP1hPY.png
圖二:強行取得 private 模式下的靜態屬性或方法,也是會被記警告的!

https://ithelp.ithome.com.tw/upload/images/20190921/201206148oBT9tEoHB.png
圖三:TypeScript 很明確就跟你講了,屬性 PIprivate 模式,只能在類別內部取用呢!

靜態成員這個功能可是非常實用的!

重點 3. 靜態成員與存取修飾子 Static Members & Access Modifiers

類別的靜態成員可以被設定不同的存取模式 —— 包含 publicprivate 以及 protected 模式。其運作的方式跟普通成員變數與方法的流程一模一樣;差別就在於 —— 普通成員是綁定在建構過後的物件上,而靜態成員則是跟類別本身綁定。

上一篇:火車票務系統案例

還記得上一篇的範例嗎?筆者短暫把必要的程式碼部分截取下來:

https://ithelp.ithome.com.tw/upload/images/20190921/201206143NbJySXvkr.png

事實上,我們必須訂立站點與站點間的資訊 stationsDetail 與站點的列表 stops。不管車票 TrainTicket 被建立多少次,但站點資訊是不太可能隨之變動的

因此可以將這兩個成員設定成靜態成員。

https://ithelp.ithome.com.tw/upload/images/20190921/201206148VgQw7dQNw.png

不過把成員從普通的狀態變成類別的靜態成員過程中,必須要把所有跟靜態成員相關的程式碼,原本是用 this 去呼叫就得改成用類別 TrainTicket 去呼叫喔

因此裡面的 isStopExist 函式取用到 stops 這個屬性就必須被改成靜態屬性的呼叫方式:

https://ithelp.ithome.com.tw/upload/images/20190921/20120614rRTMXooHpZ.png

deriveDuration 則是因為有取用到 stationsDetail 屬性,因此也必須要被轉換:

https://ithelp.ithome.com.tw/upload/images/20190921/20120614WYtRMyPlwJ.png

讀者試試看

筆者剛剛展示過:將 TrainTicket 裡的 stopsstationsDetail 更改為靜態屬性。讀者可以自行編譯並且驗證程式碼的運行。

另外,除了可將 stopsstationsDetail 改成靜態屬性外,讀者也可以確認看看,將這兩種屬性從 private 轉成 public 並且測看看能不能在外面呼叫。而且如果這些屬性可以在外面呼叫的話,讀者甚至可以自訂車票的站點表來測測看不同的結果 —— 體會一下靜態成員的語法運作過程喔。

當然,創意一點的方式是:能不能夠寫簡單的靜態方法,進行站點更改或新增站點的工作?這些就可以留給讀者發想~

小結

今天的主題應該比昨天簡單很多,畢竟繼承的概念本來就多到炸筆者也沒想到會寫到篇幅太長

下一篇也很簡單,筆者要介紹存取方法 Access Methods~

不過筆者照樣要提醒,剛入門 OOP 的讀者們,會建議來回複習類別的基礎,因為後面還有一大堆還沒講完。目前已經提到的有:

  • Class 的基礎 / Member Variables & Methods
  • Access Modifiers
  • Class Inheritance & super
  • Static Properties & Methods

記得,下一篇要講到的 Access Methods 與之前講完的 Access Modifiers,儘管都有 Access 這個字,可是意義天差地遠啊~可不要搞混了。

]]> Maxwell Alexius 2019-10-01 14:34:06
Day 20. 機動藍圖・類別繼承 X 延用設計 - TypeScript Class Inheritance https://ithelp.ithome.com.tw/articles/10217674?sc=rss.iron https://ithelp.ithome.com.tw/articles/10217674?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

  1. 描述類別存取修飾子(Access Modifiers)的功能與意義。
  2. 為何類別要實踐某介面時,介面裡的所有規格在類別裡會直接綁定為 public 模式呢?

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

上一篇的筆者的例子裡提到:類別如果想要根據某個介面的設計進行實作的話,可以使用 implements 這個關鍵字 —— 使類別進行與介面的規格進行綁定的行為。詳細的類別與介面之間的協作過程會在 Day 25. 以後ㄧ併介紹,並且也會教讀者實踐簡單的設計模式,讓讀者認識 OOP 可以寫出多實用的程式碼!

回過頭來,本篇要講到前兩篇不停出現的東西 —— 繼承(Class Inheritance)的概念 —— 內容有點多,初學的讀者斟酌分次服用也可以,學習路上不勉強一定得跟上步伐,最終目的就是會理解然後會應用就好了。

繼承的語法跟介面的延伸(Extension)很像 —— 都是使用 extends 關鍵字,不過請讀者注意這兩個功能與意義是完全不同的:一個是對於類別的延伸(也就是繼承);另一個則是對於介面的延伸,所以才會除了有 Interface Extension 的說法外,也有介面的繼承 Interface Inheritance 的說法,但筆者傾向前者說法,因為 TypeScript 針對介面的延伸是用 extends 這個關鍵字。

而類別的繼承與類別實踐(Implements)介面的用法,兩者也是有相似的地方,但也有各自需要注意的使用情境。

不過本篇就先探討繼承的概念以及使用情境,否則一下子又在介面與類別之間切換來切換去 —— 筆者也被搞得很亂 XD。

貼心小提示

讀者可以到本系列的 GitHub Repo. 索取範例程式碼

廢話不多說,正文開始

類別的繼承 Class Inheritance

交通票務範例

這一次筆者以設計陽春的交通票務系統為例子,不過從本篇開始變得稍微複雜一些!

筆者立馬把本日主角類別 TicketSystem 變出來,程式碼如下。

https://ithelp.ithome.com.tw/upload/images/20190920/20120614D4TmGJtjJq.png

簡單地描述一下類別 TicketSystem 裡面的內容:

  • 宣告 TransportTicketType列舉型別(Enumerated Type),代表的是交通票券的種類 —— 因為資料的獨特性(Uniqueness)與主觀認知的資料相關性(Similarity)皆符合,因此判斷採用列舉
  • 宣告 TimeFormat元組型別(Tuple),代表的是時間的格式,依順序分別代表小時、分鐘與秒鐘
  • 類別 TicketSystem 內含的成員變數 Member Variables:
    • startingPointdeparture 分別代表啟程點與終點,皆為 string 型別與 private 模式
    • departureTime 代表啟程時間,為 Date 型別以及 private 模式
  • 類別 TicketSystem 內含的成員方法 Member Methods:
    • deriveArrivalTime 負責計算抵達時間,因此回傳的結果是 Date —— 但由於是 private 模式,所以只能在類別內部使用
    • deriveDuration 負責計算通車過程所需要經過的時間,為 TimeFormat 型別以及 private 模式;其中,因為交通方式有三種,因此我們選擇這裡先寫死回傳的時間,固定為 1 小時,也就是 [1, 0, 0]
    • getTicketInfo 則是將票券的資料都列印出來,為 public 模式,因此可以在任何地方使用

貼心小提示

有進階 OOP 概念的讀者,可能會選擇使用抽象類別將 deriveDuration 或視情形,對其他成員轉換成抽象成員。

看不懂這個小提示的讀者們可以選擇跳過,不會影響本篇文章的內容。

抽象類別會在 Day 28. 介紹。

讀者應該發現,幾乎所有的成員變數弄成 private 模式,其中一個原因是:OOP 模式很直觀,每次定義出新物件就有它的屬性(Properties)以及可做出的行為 —— 呼叫方法(Methods)。然而,屬性很容易被竄改,這就是所謂的物件的變異(Mutation)

但 OOP 令人詬病的地方也是因為太自由,開發者隨隨便便一行就可以更改物件內部屬性或者是給它型別錯誤的值,呼叫該物件的方法出錯的機率會提高;光是要去找到 Bug 可能得逐行檢查。如果是數個物件這樣呼叫來呼叫去,這樣子要找到 Bug 所花費的時間一定更多,因此不太建議讓開發者擅自竄改物件的屬性 —— 也就是讓類別的成員變數設定為 public 模式。

為了防止開發者亂動物件的屬性 —— 設計類別時,盡量將成員的變數們設定為 private 模式,這就是將功能包裝(Encapsulation)的意義

通常我們會提供成員方法供開發者外部使用,此時就會用 public 模式。例如:如果想要讓開發者知道該物件的內容,與其讓開發者可以接觸成員變數們,可以選擇寫個公用方法(Public Method)讓開發者去呼叫,裡面的方法可能就包含該物件的內容,而這裡的 TicketSystem 中的成員方法 getTicketInfo 就是一個例子。

筆者可以開始使用這個票務系統的類別建立簡單的火車票券,程式碼如下。

https://ithelp.ithome.com.tw/upload/images/20190920/201206144Kn8ld49j1.png

(這邊有一點需要注意:Date 物件裡,月份部分的值是從 0 開始計算(代表一月),也就是說範例裡的 new Date(2019, 8, 1) 代表的是 2019 年的 9 月 1 日)

因此我們經過 TypeScript 編譯器編譯過後並且由 node 執行之結果(如圖一)。

https://ithelp.ithome.com.tw/upload/images/20190920/201206142cVgdbncW8.png

以上的範例建立了一張簡單的火車票!但這時筆者必須提出幾個疑點:

  1. 每一次建立票券時,必須將票券的種類標示出來(好長一串的 enum
  2. 火車、捷運以及航空計算站點的方式不同,難道要直接在 deriveDurationif...else... 根據票根交通種類進行站點與站點間的行車時間運算嗎?
  3. 應該會有一個表亦或是根據站點路線圖 —— 也就是根據站與站之間的間隔,自動換算出啟程站與終點站的間隔時間,不可能手動打進程式碼(這樣的票務系統未免太白癡了,用紙本搞不好比較快?)

本篇將示範:使用類別的繼承(Inheritance),創造出好用一點的火車票的票務系統。(當然也可以選擇設計其他交通種類的票券,但筆者擇其一作為示範)

火車票券類別的前置作業

本節目標是設計出一個 TrainTicket 類別,其中的部分功能來自於 TicketSystem 類別

首先,把 TrainTicket 類別宣告出來,並且制訂站點間的路線對應與間隔時間的表,請看以下程式碼。

https://ithelp.ithome.com.tw/upload/images/20190920/20120614ch9yNAD8P4.png

筆者一樣先定義站點的靜態格式 TrainStation —— 使用的是型別 type 而不是介面 interface 喔!

其中 TrainStation 有三個屬性:

  • name 代表站點名稱,為 string 型態
  • nextStop 代表下一站名稱,為 string 型態
  • duration 代表本站點跟下一站之間的間隔時間(筆者這邊亂填,舉個例子而已XD),為 TimeFormat 型態

另外,在 TrainTicket 類別裡面,筆者宣告一個簡單的車站資訊對應表 stationsDetail,並標記為 private 模式,為 TrainStation[] 型別—— 代表的是一系列的車站站點的資訊。

貼心小提示

此範例舉的是臺灣北向行的車票,實際上要設計一個票務系統,你可能還會遇到南向行亦或者是來回票這些狀況。但為了減少本例子的複雜度,筆者就簡化為北向行的車票作為範例。另外,實際上規劃路線時,更正確的資料結構應該是採用圖(Graph)會比較適合喔!

另外,stops 代表所有的火車站站點,這裡也是設為 private 模式,型別為 string[]

我們還需要一個方法專門判斷火車票券的站點名稱沒有錯誤,因此筆者在 TrainTicket 內定義新的函式:isStopExist,此函式為 (stop: string): boolean 型態 —— 專門檢查站點是不是存在。如果不存在就回傳 false

https://ithelp.ithome.com.tw/upload/images/20190920/20120614j3E5UjtfIz.png

下一個項目對讀者來說比較複雜一些。

原本 TicketSystem 類別有 deriveDuration:專門計算票券之起點站與抵達站之間所需耗費的交通時間的方法。

我們也可以在 TrainTicket 裡實踐類似的機制(取代父類別的 deriveDuration 方法)—— 專門為火車站點這個應用做特別設計,因此筆者將實踐結果直接貼出來如下:

https://ithelp.ithome.com.tw/upload/images/20190920/20120614vjdc1FLx3x.png

筆者簡單描述設定為 private 模式的 deriveDurationTrainTicket 類別裡面會怎麼跑,以下舉兩種狀況:

  • 從台南 Tainan 到新竹 Hsinchu 會經過 Tainan -> Taichung -> Hsinchu,根據定義過的 stationsDetail,交通時間結果必須累加兩種 TimeFormat[3, 20, 0][2, 30, 30],累加結果為 [5, 50, 30]
  • 從高雄 Kaohsiung 到台北 Taipei 會經過 Kaohsiung -> Tainan -> Taichung -> Hsinchu -> Taipei,根據定義過的 stationsDetail,交通時間必須累加四種 TimeFormat[1, 45, 30][3, 20, 0][2, 30, 30] 以及 [1, 30, 30],加總結果為 [7, 120, 90] —— 然而,必須遵守分鐘跟秒鐘是六十進制的特性,轉換成合理的時間格式,因此結果為 [9, 6, 30]

知道運作流程後,這裡筆者得先指出 deriveDuration 方法裡:

https://ithelp.ithome.com.tw/upload/images/20190920/20120614uOdPVUFVvk.png

我們有使用到 destination 以及 startingPoint 這兩個值,其為 TicketSystem 類別早就有宣告過的成員變數。

如果想要讓 TrainTicket 可以使用 TickeySystem 的功能,因此必須進行類別繼承的動作

使用類別繼承 Inheritance

重點 1. 類別的繼承 Class Inheritance

若宣告類別 C ,其程式碼實踐結果如下:

https://ithelp.ithome.com.tw/upload/images/20190919/20120614ef95r4hoeN.png

另外宣告類別 D,然後 DC 進行類別的繼承(Inheritance),因此必須用 extends 語法,其寫法如下:

https://ithelp.ithome.com.tw/upload/images/20190930/201206148EduEvp7NO.png

我們稱 D 類別繼承了 C 類別

其中,CD父類別(Parent Class/Superclass);相對地,DC子類別(Child Class/Subclass)。

D 類別具有以下的特性:

  1. D 類別可以使用 C 類別非 private 模式的成員變數與方法們(D 除了 PprivateMprivate 外,其他成員都可以使用)
  2. D 類別建造出來的物件(使用 new),該物件的型別除了屬於 D 類別以外 —— 由於繼承的關係,該物件的型別也同時屬於類別 C
  3. 相對地,C 類別所建造出來的物件型別為 C 類別,但不屬於 D 類別

貼心小提示

類別的型別推論與註記的機制將會在 Day 24. 介紹。然而,筆者倒是可以先劇透一下:

類別(Class)與 TypeScript 介面(Interface)本身也是型別化名(Type Alias)的一種~”

另外也要注意一點,介面的擴充(Extension)以及類別的繼承(Inheritance)用的都是 extends 關鍵字,但這兩個東西是兩回事,不能相提並論,只能說機制很像,但對象不同罷了。

以下開始使用類別的繼承。

首先將 TrainTicketTicketSystem 進行繼承的動作。

https://ithelp.ithome.com.tw/upload/images/20190920/20120614sZXk6eMl6v.png

筆者就先貼上到目前為止的程式碼的狀況。

https://ithelp.ithome.com.tw/upload/images/20190920/20120614kXqyylHKwX.png

以上的程式碼一定會被 TypeScript 發出警訊。筆者是故意要讓錯誤出來的,為的是要講解類別繼承的一些重要規則 —— 從檢視錯誤訊息的過程可以學到很多東西

以下幾節就是介紹運用類別繼承時,通常會遇到的問題。

protected 存取模式的應用

首先,第一個錯誤出現就在進行繼承的那一剎那 —— TrainTicket 類別馬上被 TypeScript 警告。(錯誤狀態如圖二所示;而錯誤訊息如圖三)

https://ithelp.ithome.com.tw/upload/images/20190920/20120614uHPs1hHKnW.png
圖二:進行繼承的那一剎那,我們就被 TypeScript 警告

https://ithelp.ithome.com.tw/upload/images/20190920/20120614bkvv5dO9Bc.png
圖三:TypeScript 告訴我們,類別 TrainTicket 繼承時出現的錯誤原因是 —— deriveDuration 這個模式是 private 模式

讀者看到圖三的訊息,TypeScript 告訴我們:“deriveDuration 在父類別被設定成 private 模式,因此不能被使用。” 這一點在本篇重點ㄧ就有提到:

D 繼承 C 類別的情形下)D 具有以下的特性:

  1. D 類別可以使用 C 類別非 private 模式的成員變數與方法們(D 除了 PprivateMprivate 外,其他成員都可以使用)
  2. (...略)

繼承過後的子類別不能使用父類別的 private 成員

於是這裡就產生一個問題了:

我們要如何能夠同時封裝好類別成員們,不被外部使用,但又能夠給繼承的子類別們定義(或是覆寫父類別)的成員呢

這裡的答案就是使用 protected 存取模式

重點 2. protected 存取模式

當類別成員被標示為 protected 模式下,該成員儘管不能被外部取用,但可以在當前類別以及子類別的範圍內使用

protected 存取模式跟 private 的差別就只有一個:能不能在繼承的子類別裡使用該成員 —— 標示為 protected 存取模式即代表可以。

因此筆者將 TicketSystem 以及 TrainTicket 裡的 deriveDuration 成員方法改成 protected 模式。

https://ithelp.ithome.com.tw/upload/images/20190920/20120614YhkU3YIaPt.png

可以看到剛剛標示在 TrainTicket 底下的錯誤警告消失了。(如圖四)

https://ithelp.ithome.com.tw/upload/images/20190920/20120614knbC2DNGl1.png

父類別建構子函式 Parent Class Constructor Function

不過我們還沒完全解決完畢!筆者提出另一個問題:

若從 TrainTicket 類別(它是本案例中的子類別)建立出物件,由於 TrainTicket 擁有其父類別 TicketSystem 的成員變數與方法們。

但如果藉由子類別去創建物件時,如何將成員變數們進行初始化的動作

畢竟 TrainTicket 的成員變數是被繼承過來的,然而那些初始化的邏輯都是被寫在父類別裡面,也就是 TicketSystem

因此這裡必須要有一個管道專門從子類別去連結父類別的建構子函式進行物件成員變數初始化的動作

而這個管道就是 —— 名為 super 的關鍵字。

重點 3. 類別繼承中的 super

由於子類別繼承了父類別的成員,通常也會需要進行初始化物件的動作。其中,與父類別進行溝通的管道就是 super

在子類別裡,super 可以等效於父類別的建構子函式

還記得一開始使用的 TicketSystem 類別,光是要初始化一張交通票券,會這樣寫:

https://ithelp.ithome.com.tw/upload/images/20190920/20120614bAaapOA1ai.png

其中,new TicketSystem(...) 這段是負責從 TicketSystem 建立票券的物件。而 TicketSystem 本身就是類別建構子函式

那麼我們如何使用 super 來連結到父類別並填入物件初始化資料呢?

根據重點 3. 的描述:我們可以在子類別裡的建構子函式內,使用 super並且將 super 看成父類別的建構子函式

https://ithelp.ithome.com.tw/upload/images/20190920/20120614HQCqX9b4i2.png

運用 super 來呼叫父類別的建構子函式時,必須按照父類別建構子函式的型別,填入正確順序的參數

資深開發者可能會問:“有時父類別會被放置在其他的檔案,想要在龐大的專案找父類別的建構子函式的內容豈不是很痛苦?更何況這裡的參數有四個,也是很容易被搞錯的。”

我們可以依靠簡單的技巧取得父類別建構子的型別資訊 —— 請將鼠標指向 super 這個關鍵字就會出現如圖四的畫面。

https://ithelp.ithome.com.tw/upload/images/20190920/20120614V9kzpEwIHz.png
圖四:super 關鍵字被鼠標指的時後,它會提供父類別建構子的資訊

你可以看到 super 關鍵字彈出的視窗很明確地說 —— constructor TicketSystem後面接的是初始化物件所需要的參數,型別對照也寫得非常清楚。

因此這裡筆者提醒:重點是要會利用工具帶來的便利性省去翻程式碼的麻煩

其他的小錯誤

最後還有一個小 Bug 還沒有解決 —— 在 deriveDuration 函式裡有使用到父類別的 private 模式下的成員變數:startingPoint 以及 destination

如果我們查看 TrainTicket 裡面的函式,TypeScript 自動地幫我們標註錯誤。(錯誤警告如圖五;訊息如圖六)

https://ithelp.ithome.com.tw/upload/images/20190921/20120614F9LtgFc9Y4.png
圖五:TypeScript 自動幫我們標註 startingPointdestination 部分有潛在錯誤

https://ithelp.ithome.com.tw/upload/images/20190921/20120614asYG0SLLmK.png
圖六:TS 很貼心地提醒我們,父類別 TicketSystemdestinationstartingPoint 成員皆為 private 模式,因此不能在子類別使用

看到這裡的讀者應該也會覺得要處理掉這個錯誤很簡單,就是把父類別的建構子函式裡的 startingPointdestination 這兩個成員從 private 模式切換到 protected 模式,一切就 OK 了!

https://ithelp.ithome.com.tw/upload/images/20190921/201206149b1OJEpMGm.png

走到這裡,我們順利完成了 TrainTicket 類別的實踐 —— 筆者簡單新增一個火車票測試看看結果。(以下程式碼經過編譯並用 node 執行結果如圖八)

https://ithelp.ithome.com.tw/upload/images/20190921/20120614WUqh1XVZNF.png

https://ithelp.ithome.com.tw/upload/images/20190921/201206140QcOVxm1l3.png
圖八:成功地實踐出簡單的交通票券系統中的火車票部分

以上的程式碼,新增一張火車票 —— 只要標明啟程站與終點站(startingPointdestination)以及發車時間(departureTime),是不是符合平常買車票的邏輯呢?

我們也不需要再次實踐就可以使用父類別早就幫我們定義過的 TicketSystem 方法,因為被 TrainTicket 繼承下來了。

另外,父類別也早就幫我們把 arrivalTime 也都算好了,所以也不需要再重寫一次那些邏輯。

使用 super 的注意事項

最後的最後,筆者還是得講一下使用 super 語法的注意事項:

重點 4. 子類別的建構子注意事項

  1. 子類別的建構子函式裡,進行初始化物件時 —— 也就是**super 被呼叫之前**,由於物件還未建立完畢,不能有 this 相關的操作行為
  2. 假設宣告某類別 C,而另外一個類別 D 繼承 C。另外,C 類別的建構子函式裡擁有若干參數 ...args 並且子類別也沒有實作建構子函式時,則預設的子類別建構子函式的行為為:

https://ithelp.ithome.com.tw/upload/images/20190920/20120614Xk4lz0FcAa.png

第一點可能還可以理解,物件都沒建構出來怎麼可能去 Reference 到物件的實體(也就是 this)呢?(圖九)

https://ithelp.ithome.com.tw/upload/images/20190920/20120614uIhTV1w1kG.png
圖九:錯誤訊息很明確就跟你說,在 super 被呼叫前,不能使用 this,因為物件還沒被導出來(用的是 Derive 這個單字)

然而,第二點要表達的東西可能會對眾多人感到陌生,這裡舉簡單的例子,單純確認 super 的行為。

https://ithelp.ithome.com.tw/upload/images/20190920/20120614wUWzCIOjTB.png

以上的程式碼就是仿造 TrainTicket 繼承 TicketSystem 類別的方式:子類別 TestChildClass1 的建構子函式呼叫了 super —— 父類別 TestParentClass 的建構子函式,並且按照順序填入參數,初始化類別內繼承過來的成員變數。

回過頭來,我們來試試看 TestChildClass1 這個案例。(編譯以下程式碼並用 node 執行結果如圖十)

https://ithelp.ithome.com.tw/upload/images/20190920/20120614zPRx3lyCQU.png

https://ithelp.ithome.com.tw/upload/images/20190920/20120614NBBsHrcabc.png
圖十:執行結果正常

那我們來換另一種情形 —— 繼承了父類別卻沒有實踐子類別的建構子函式。(也就是我們要講的重點 4. 的第二點)

https://ithelp.ithome.com.tw/upload/images/20190920/20120614EKawRT7e1e.png

對,就這麼短 XD,不過我們還是ㄧ樣來測看看如何從子類別建構出物件,在這裡筆者刻意不填入任何參數在子類別的函式建構子內。

https://ithelp.ithome.com.tw/upload/images/20190920/20120614vLpxgqM7sD.png

這樣子的程式碼一定會出現錯誤訊息,因此筆者就直接把它貼出來。(如圖三)

https://ithelp.ithome.com.tw/upload/images/20190920/20120614mE83twCJPg.png
圖三:錯誤訊息明確跟你講少了哪些參數

就算你不對子類別自訂建構子函式,子類別會直接延用父類別建構子函式的規格,要求使用者必須代入建立物件時所需具備的參數

就算 TestChildClass2 沒有 constructor,它照樣要求你必須要依序代入 (p1: number, p2: string, p3: boolean) 這些值,所以 TestChildClass2 的建構子函式跟 TestChildClass1 的效果也沒什麼兩樣,讀者可以試試看進行驗證。

基本上讀者反覆看覺得很複雜,沒關係 —— 記下驗證過程的結論就好!畢竟那些重點只是以公式的方式表現出來罷了。

小結

簡而言之就是廢話心得一籮筐。

本篇文章其實完全超出筆者預料之外的難寫 —— 因為是隨時想出應用實例、隨時寫出解法,但也萬萬沒想到單純講繼承的相關功能會花掉筆者整整兩天的時間思考(就為了一個很笨的交通票務系統)。

要能夠將自身的知識轉換成文字真的頗困難的,筆者也是感到有點受挫。不過如果一直都在講語法,當然會覺得無聊,所以才會想說寫一段簡單應用。

]]> Maxwell Alexius 2019-09-30 14:18:02
Day 19. 機動藍圖・存取修飾 X 藍圖規劃 - TypeScript Class Access Modifiers https://ithelp.ithome.com.tw/articles/10217365?sc=rss.iron https://ithelp.ithome.com.tw/articles/10217365?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

  1. TypeScript 類別(Class)的意義是什麼?
  2. TypeScript 類別跟介面(Interface)的最大差別在哪裡?
  3. 什麼是成員變數(Member Variables)、成員方法(Member Methods)以及建構子函式(Constructor Function)呢?

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

今天要講一個非常重要的東西,並且這個功能 —— 存取修飾子(Access Modifiers)是 ES6 Class 原本沒有的東西,而目前還在進行 stage-3 Proposal 階段的也就僅僅是 Private Method(未來的某個時間點會被推出)。

本身沒有 OOP 背景的讀者可能會覺得:“什麼是存取修飾子?一開篇怎麼還沒進到正文就看不懂了?”

沒關係,筆者馬上進入正文 XD

正文開始

TypeScript 類別之存取修飾子 Access Modifiers

提款機範例

大家應該都會對提款機這一個東西很熟悉吧~ 不外乎就是裡面充滿 $$$ 但是必須握有提款卡才能提領裡面的錢錢。(讀者云:“廢話!”

筆者舉一個很陽春的提款機介面大概長什麼樣子~

https://ithelp.ithome.com.tw/upload/images/20190919/20120614VrE4LAqA2Q.png

以上的程式碼,型別 TUserAccount —— 代表可以使用提款機的帳戶的靜態格式;介面 ICashMachine —— 代表該提款機的介面。

TUserAccount 除了有最簡單的帳戶與密碼欄位外,還必須紀錄金額,因此多了一個 money: number 欄位。

ICashMachine 分別有基本的存提款功能(depositwithdraw)以及使用者帳戶系統(userscurrentUsersignInsignOut)。不過筆者認為,因為 ICashMachine 介面包含這兩種不同的功能,因此也可以抽象化拆成兩個部分再組起來:

https://ithelp.ithome.com.tw/upload/images/20190919/20120614JTbTMt2R1f.png

這樣是不是就很清楚:提款機明確分成帳戶系統與交易系統。另外,帳戶系統 AccountSystem 介面可能也可以應用在其他的系統裡面。

回歸正題,接下來筆者實際用類別去實踐 ICashMachine 這個介面定義出來的規格!

這裡筆者會先用到還沒有講到的東西 —— 類別如果要根據某介面的規格實踐出來的話,可以使用 implements 關鍵字

https://ithelp.ithome.com.tw/upload/images/20190919/20120614aE3yxReX4I.png

貼心小提示

筆者這邊短暫解釋:類別使用 implements 連結某介面(或型別)與我們在對某變數積極註記某一個型別/介面的概念很像。因此可以想成類別 implements 某介面 —— 就等同於我們在對類別進行積極註記(Annotation)的動作

Day 24. 與 Day 25. 會討論類別結合介面的推論(Inference)與註記(Annotation)機制喔!

筆者對以上的程式碼進行說明:

  • 本類別裡,每一次創建新的提款機的物件,users 欄位(或者可以稱之為 —— 成員變數 users)固定只會有三個帳戶(讀者也可以試試看用 constructor 建構子來初始化 users
  • currentUser 一開始是 undefined 的狀態
  • signIn 裡面的方法型別跟 AccountSystemsignIn 對應的型別一模ㄧ樣 —— 填入 accountpassword 後,經由簡單的 for 迴圈尋找使用者並鎖定起來
  • depositwithdraw 會先檢查有沒有使用者;如果有的話就會正常動作,沒辦法就會拋出例外

筆者以下來測試看看。(使用 TS 編譯器編譯過後與執行結果如圖一)

https://ithelp.ithome.com.tw/upload/images/20190919/20120614OfA12j1pSG.png

https://ithelp.ithome.com.tw/upload/images/20190919/20120614tbpsh7VMvp.png
圖一:完整的登入 -> 提款 -> 登出流程,執行結果很正常

儘管我們可以確定這個類別實踐出來的結果正確了,不過 ...

讀者敢用這款陽春的提款機系統嗎? XD

首先,CashMachine 類別產出來的提款機物件 -- 裡面所有的屬性與方法都可以被外部存取(Access)。其實存取這個詞筆者覺得講起來很怪,乾脆說:“裡面的屬性與方法隨時隨地都可以被檢視與呼叫!”

也就是說,光是使用 machine.usersmachine.currentUser 就可以調閱出所有的使用者資料,裡面除了基本的帳戶資訊外 —— 密碼以及個資等等都會被洩漏出去啊!

這不是我們希望看到的狀況:

於是上帝在創造類別後的第二天,創造出類別成員的存取修飾子(Member Access Modifiers)

至少不是大洪水

存取修飾子的使用與意義 Access Modifiers

筆者這裡就先下重點:

重點 1. 存取修飾子 Access Modifiers

  1. 存取修飾子總共分為三種模式:publicprivate 以及 protected
  2. 存取修飾子可以調整成員變數(Member Variables)與方法(Member Methods)在類別裡面與類別外部的使用限制
  3. 類別在宣告時,若成員變數或方法沒有被註記上存取修飾子,預設就是 public 模式
  4. 若宣告某類別 C,則裡面的成員變數 P 或成員方法 M 被註記為:
    • public 模式時:PM 可以任意在類別內外以及繼承 C 的子類別使用
    • private 模式時:PM 僅僅只能在當前類別 C 內部使用
    • protected 模式時: PM 除了當前類別 C 內部使用外,繼承 C 的子類別也可以使用
  5. 若宣告某類別 C,其中該類別有明確實踐(implements)某介面 I,則類別 C 必須實踐所有介面 I 所提供的格式 —— 而介面 I 的規格轉換成為類別 C 時 —— 成員變數與方法皆必須為 public 模式

貼心小提示

有關於類別繼承與 protected 模式將會在 Day 20. 篇章揭曉。

今天先講本篇重點 —— 存取修飾子的用途與意義

其實上面的重點已經將存取修飾子的意義講出來了:限制成員變數或方法被呼叫的權限

剛剛的 CashMachine 類別的提款機範例裡,所有的成員變數跟成員方法因為沒有出現存取修飾子的蹤跡,因此判定 —— 所有成員變數與方法都是 public 模式

也就是說,以下定義 CashMachine 類別的程式碼(每個成員變數與方法前面都加 public)與原先沒有標注 public —— 兩者行為完全沒兩樣:

https://ithelp.ithome.com.tw/upload/images/20190919/20120614QzyBo1cRUB.png

如果想要限制使用者不能檢視成員變數的狀況,可以將剛剛的 userscurrentUser 標註 private 修飾子。(程式碼如下,被 TypeScript 檢測結果如圖二;錯誤訊息如圖三)

https://ithelp.ithome.com.tw/upload/images/20190919/20120614RyUdvtjXxe.png

https://ithelp.ithome.com.tw/upload/images/20190919/20120614Euie3TPZLf.png
圖二:將 userscurrentUser 改成 private 模式,依然被 TS 警告

https://ithelp.ithome.com.tw/upload/images/20190919/20120614cM9wP7QMNX.png
圖三:CashMachineusersprivate 模式但 ICashMachine 則是 public

儘管我們對 userscurrentUser 改了模式,但依然出現警告,通常這是初次使用 TypeScript 介面與類別會遇到的問題。因此,筆者必須另外提及一個重點:

重點 2. 介面相對於類別的意義

TypeScript 介面(Interface)定義的是功能的完整規格外,若類別對介面進行綁定的動作,裡面的規格細目代表類別成員 public 模式的成員

回過頭來,請讀者思考一件事情:

為何介面定義的規格必須是絕對綁定為 public 模式呢

其實概念很簡單,譬如說開車的時候,你操作的是方向盤、手排檔等等東西 —— 這些都是你在操縱汽車的介面

然而,你會去手動操作這些東西嗎? —— 比如說手動播速度計:此時時速為 100km/hr,亦或者翻開油箱查看現在的油量?(超猛的!)

當然是不會的!因為這些都是汽車的內部零件與細部功能互相連動,你只能操控表面的東西與看到速度計或其他資訊

這些你看得到的東西通通都屬於汽車的介面(跟 public 概念很像),你看不到的東西不是介面的範疇(跟 private 很像)。

因此可以這麼說:

interface 如果有定義包含 private 屬性的東西事實上是不合理的!(參見 StackOverflow

回過頭來,我們的提款機中的 userscurrentUser 因為被標記為 private 模式,因此 AccountSystem 介面只能有 signInsignOut 這兩個方法 —— 至於是依靠什麼方式登入登出,可以進行自由實踐。

https://ithelp.ithome.com.tw/upload/images/20190919/20120614StDcLh5S3n.png

修改完之後,回頭再看類別 CashMachine 的錯誤訊息,基本上會消失,筆者這邊就不放結果圖了。

以下是經過修改過後的完整程式碼(圖會有些長)。

https://ithelp.ithome.com.tw/upload/images/20190919/201206148Dhk6HV03k.png

注意:我們已經更換 userscurrentUserprivate 模式,並且把那兩個屬性規格從 AccountSystem 剔除掉了。

筆者測試看看以下使用 const machine = new CashMachine() 的狀況。(圖四為在 CashMachine 內部使用 userscurrentUser 的狀況;圖五則是在類別外部使用的狀況;圖六則是在類別外部使用時出現的錯誤訊息)

https://ithelp.ithome.com.tw/upload/images/20190919/201206140YCLnzPQGS.png
圖四:在類別的內部實踐出 signIn 這個方法 —— 裡面有呼叫到 userscurrentUser 都不會出現問題。

https://ithelp.ithome.com.tw/upload/images/20190919/20120614DhVT3H5N7K.png
圖五:在類別的外部,使用 machine 這個屬於類別 CashMachine 產出的物件,呼叫 currentUser 屬性就被 TypeScript 警告了。

https://ithelp.ithome.com.tw/upload/images/20190919/20120614s36vtgfnfa.png
圖六:TypeScript 很明確地跟你講,currentUserprivate 模式並且只能在 CashMachine 內部使用。

這裡應該可以看得出來 —— 藉由 private 模式將類別內部實踐出的功能隱藏起來,防止外部的開發者做出不良行為。

初始化成員變數的多種方法

另外,前一節的問題是:每一次初始化新的提款機物件,該物件都會被限制在三種帳戶的狀況。

其中,前一篇 文章介紹類別的基本宣告與用法就已經提過 —— 除了可以將成員變數定義在類別裡面、成員方法外面,我們還可以將成員變數的在建構子函式(Constructor Function)裡進行初始化動作。因此我們可以這樣做:

https://ithelp.ithome.com.tw/upload/images/20190919/20120614gaEToSDqY4.png

但還有一種更簡潔的寫法:

https://ithelp.ithome.com.tw/upload/images/20190919/20120614f6VACEER5X.png

這種寫法除了將 users 宣告成成員變數外,也直接把 users 設定為 private 模式 —— 也因此我們不需要再建構子函式內寫這一行:this.users = users

https://ithelp.ithome.com.tw/upload/images/20190919/20120614JkjEyzy8Rl.png
圖七:節選自某段 Angular 官方教學的程式碼,其中可以看出,該類別 HeroService 在建構子函式內直接宣告其成員變數 messageServiceprivate 模式,對應的是某 MessageService 介面(或型別)

重點 3. 建構子函式裡的參數直接宣告成員變數

若某類別 C 的宣告裡,P1P2、...、Pn 為其成員變數 —— 每個成員變數對應型別(或介面)分別為 T1T2、...、Tn

其中,P1Pn 的存取模式可為任意修飾子(publicprivate 以及 protected),則我們可以直接在 C 的建構子函式的參數內進行成員變數的宣告:

https://ithelp.ithome.com.tw/upload/images/20190919/20120614D5YTN5lbSP.png

不過必須注意的是:就算在建構子的參數裡,想要宣告 public 模式的成員變數,你必須明確註記 public 這個修飾子出來,不然 TypeScript 只能當該參數為普通參數而不是該類別的成員變數

再者,建構子裡的參數 —— 宣告的順序有差:使用 new C(/* 參數 */) 建構新物件時,填進參數的順序跟你在宣告成員變數在建構子函式的參數順序一模一樣喔!

小結

今天已經介紹完了基本的存取修飾子,明天會進到稍微困難一點的部份 —— 類別的繼承(Class Inheritance)。

畢竟這兩篇講解類別的基礎時,都提到了繼承這個字眼;再者,如果已經學會了存取修飾的概念,要進到類別的繼承其實算簡單。

]]> Maxwell Alexius 2019-09-29 15:39:20
Day 18. 機動藍圖・類別宣告 X 藍圖設計 - TypeScript Class https://ithelp.ithome.com.tw/articles/10217203?sc=rss.iron https://ithelp.ithome.com.tw/articles/10217203?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

複合型別 unionintersection 的功能與意義代表為何?

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

今天總算要進到類別的主題了~ 不過有 OOP 經驗的人會覺得這部分應該算(超級)簡單。然而,講到一些進階主題,譬如策略模式或抽象類別可能就稍微難一些囉~

對於只熟悉 JavaScript 的開發者們(包含筆者本身喔!),對於 OOP 並不是完整的了解。此外,ES6 版本的類別語法 -- 儘管對於 JavaScript 朝向 OOP 方式開發已經算是很大的進展;不過對於筆者來說,ES6 Class 的機制並不是很完整,因為缺少了在 OOP 應用領域裡面很實用的功能 —— 而 TypeScript 則是把大部分類別應該出現的功能基本上實踐出來了

筆者除了嫌 ES6 Class 語法不完備外,對於未來出現的 Private Member(或者是俗稱的 Private Method,不過完整一點會稱為 Private Access Modifier)的語法也是深感覺得:『 醜 』一個字足以形容。(這是個人看法!)

(到正文開始前,以下部分讀者看不懂也沒關係,因為之後會講到 private 這東西到底在做什麼)

筆者在這裡很主觀,但忍不住還是給不知道的人看:

https://ithelp.ithome.com.tw/upload/images/20190918/20120614UPW0JYLqYG.png

恩... 與其加一個 # 難道沒有想乾脆一點就換成 private 關鍵字嗎?

可能筆者的見識還不廣,也不知道當初提 Proposal 的程序為何?到底審核過程又是如何?要把一個新的 Feature 實作在 JS 引擎上的難度? —— 諸如此類的問題。(就當作是筆者在什麼東西也不懂吧!)

搞不好也有人認為加一個 # 很方便,但是對於學習一門語言而言,個人覺得只會增加複雜度,因為通常學一個語言版本是 A,另一個語言版本是 B —— 若 AB 語法機制相似的話,學完其中一個,另一個就會非常好上手,就算有細微差別也都可以再去花時間鑽研。(就像當初筆者使用 Ruby 以及一點點 C++ 打好類別相關的基礎,上手 TypeScript 自然就很快,相信很多開發者都有類似的經驗)

特別訂立出新的語法,必須要注重的是 -- 訂好這些語法規則之背後用意與哲學到底是什麼

如果單純只是研發更多 Syntax Sugar 的話而沒實質意義或質量性 —— 那筆者也只能說,就是該語言的一種噱頭而已,就看個人要不要使用囉。(糖果畢竟只是糖果,會不會容易蛀牙看個人造化

說到最後,Class 的完整概念在正式的 ECMAScript 裡是還不完備的,因此本系列文會從 Class 最基本的東西然後慢慢進行展開的動作。所以呢...

正文開始

TypeScript 類別的宣告與基本使用

環境建置

這裡我們要建第三個環境在本系列的 typescript-tutorial 的資料夾裡,畢竟前一個環境都是在講 interface 部分也是有點亂了。如果讀者有跟著本系列的範例的話,應該都知道筆者將這些程式碼存在哪裡。因此筆者就趕快將新的環境建好。

不過想要查看筆者所有的範例程式碼,可以到 GitHub —— Maxwell-Alexius/Iron-Man-Competition 這裡去查看喔~

這一次筆者ㄧ樣會在 typescript-tutorial 資料夾建造新的資料夾,名為 03-interface-class

$ cd PATH_TO/typescript-tutorial
$ mkdir 03-interface-class
$ cd ./03-interface-class

一如往常,讀者應該知道要下什麼東西來初始化 TypeScript 編譯器設定檔:

$ tsc --init

最後在該資料夾裡新增 index.ts 我們就準備好囉!(打開 VSCode 應該會出現如圖一的狀況)

$ touch index.ts

https://ithelp.ithome.com.tw/upload/images/20190918/20120614tSvOejEVtL.png
圖一:環境建立完成

最後還是得注意,把 tsconfig.json 裡面的 strictNullChecks 選項改成 true。筆者後續要開始示範小專案會再跟讀者說明這些編譯器設定到底在做什麼~

https://ithelp.ithome.com.tw/upload/images/20190918/201206143DiqOabHRh.png
圖二:將 tsconfig.json 裡的 strictNullChecks 設為 true

類別的概念:『 物件的藍圖 』

筆者先從最~最~最~最~最基本的東西開始,也就是(狹義的)物件本身。

貼心小提示

如果讀者是從這個篇章看起,本系列會用到一些特殊名詞,這是筆者自訂的,目的是要好解說本系列文章:

  • 狹義物件:泛指普通 JSON 物件
  • 廣義物件:包含狹義物件、函式、陣列、類別以及類別所建立出來的物件

通常一個 TypeScript 介面的宣告,就是要被作為:物件的規格。(讀者反嗆:“不然要做什麼?”

https://ithelp.ithome.com.tw/upload/images/20190918/20120614ILNGZWUfFZ.png

如果要讓物件可以做出什麼行為(Action),最簡單的做法就是在物件裡面定義方法(Method)。以剛剛的範例進行延伸的話,我們可以新增函式型別為介面裡其中一項規定的屬性 —— 要求開發者必須對該物件實踐出必要的方法。

https://ithelp.ithome.com.tw/upload/images/20190918/20120614dlvF3oC2jG.png

讀者看到這裡,想也知道 —— 又違反了 DRY(Don't Repeat Yourself)原則了啊!

貼心小提示

筆者寫到類別相關的文章會不停 DRY 來 DRY 去好色情的感覺),因此讀者要有心理準備。(這是哪門子的提示?

另外,我們也發現一件事情:printInfo 是該物件的方法,而每一個物件實作 printInfo 的程序都一模ㄧ樣,於是有了一個想法出現 —— 有沒有工具是跟介面的概念很相似,但是又可以預先把物件的屬性(Properties)與行為(Behaviours)都表現出來呢

於是上帝讓 類別 就這麼誕生了!(“讚美主!阿・們~!” <—— 關主 P 事,滾去寫你的程式!

類別(Class)的基本用法

筆者就馬上介紹如何宣告一個類別:

重點 1. 類別的宣告

若我們想要定義類別 C,其中類別包含屬性 P 與方法 M。其中,P 對應之型別為 Tp,而方法 M 對應之函式型別為 (paramName: Tparam): Toutput,則最基本的宣告方式為:

class C {
  P: Tp;
  
  M(paramName: Tparam): Toutput {
    // 方法內的內容
  }
}

備註:本宣告方式為最基本的宣告方式,並非所有類別都必須這樣定義的!

筆者綜觀本系列 —— 深深覺得感動,我們的主角從原本的型別 T 到介面 I,現在又多了類別 C 了。

貼心小提示

到現在還分不清函式(Function)和方法(Method)的人,可以參考這一篇

讀者可以看到,類別(Class)跟介面(Interface)的最大差別在於:

介面只能定義格式;而類別除了宣告屬性外,也可以描述出物件的行為

筆者示範:如果將 PersonInfo 介面(上一個範例)轉換成類別的宣告方式,大概會長什麼樣子。(TypeScript 判定以下程式碼的結果如圖三)

如果沒接觸過類別的讀者,剛開始進行學習類別時,會感到吃力的話是正常的。(筆者當初學的時候感到超級吃力,花了好幾個月的時間才知道類別到底怎麼寫。因為那時候學校課程上的是 C++,而 C++ 的類別難度對筆者而言,跟 TypeScript 相比簡直又是另一個層次啊;不過 TypeScript 的類別不會到特別難寫)

https://ithelp.ithome.com.tw/upload/images/20190918/20120614AeGt9JV6ex.png

https://ithelp.ithome.com.tw/upload/images/20190918/201206141C1kGg7aXz.png
圖三:直接按照重點 1. 的方式將 PersonInfo 介面轉換成 CPersonInfo 類別,結果還是錯!

其實上面這段程式碼照樣會出錯!剛入門 TypeScript 的讀者可能在這一步感到疑惑,明明都已經把東西都標註好了,是還想怎樣啊?

那是因為跟 TypeScript 的型別推論機制(Type Inference)有關。但我們先不管這些錯誤!若筆者選擇直接講解解決錯誤的方法,又會對在 JS 圈打滾但不完全熟悉 OOP 概念的人太快。

先從基礎來!

先來個正式的名詞定義 —— 而這些名詞不管讀者跑到哪個 OOP 語言,不外乎都會看到這些名詞,想轉跑道或跳槽到其他 OOP 語言必備的名詞系列。

重點 2. 成員變數與方法 Member Variables & Member Methods

若某類別 C 裡,包含一系列屬性 P1P2、...、Pn 以及方法 M1M2、...、Mn。其中,類別 C 的宣告方式如下:

https://ithelp.ithome.com.tw/upload/images/20190918/20120614N1jlgn7kex.png

我們將 P1P2、...、Pn 這一系列的屬性稱之為類別 C成員變數們(Member Variables)。

而我們也會將 M1M2、...、Mn 這一系列的方法稱之為類別 C成員方法們(Member Methods)。

貼心小提示

有些讀者可能會細問,為何不將 P1P2、...、Pn 取名為成員屬性,不都是物件的屬性嗎?為何都改成變數?

筆者這邊的回答是:在類別裡面,我們可以任意操作物件的屬性跟方法之間的關係屬性也可能會隨著物件方法的使用而被改變 —— 這個就是所謂的物件的變異(Mutation),而物件屬性變來變去的狀態跟變數的本質沒兩樣

筆者可以放心地用這些專有名詞了~

首先回到剛剛的錯誤。(筆者重新把錯誤貼在下面,如圖四)

https://ithelp.ithome.com.tw/upload/images/20190918/201206141C1kGg7aXz.png
圖四:直接按照重點 1. 的方式將 PersonInfo 介面轉換成 CPersonInfo 類別,結果還是錯!

我們的成員變數 —— 也就是 nameage 以及 hasPet 三個欄位都被 TS 標記有問題。那是因為它們三個都還沒被初始化,因此不符合各自的對應型別:stringnumberboolean

TypeScript 對類別可是很敏感的,所以其中一種解法是直接對那些欄位指派值。(以下程式碼被 TS 判定結果如圖五)

https://ithelp.ithome.com.tw/upload/images/20190918/20120614INxHICoYFS.png

https://ithelp.ithome.com.tw/upload/images/20190918/20120614zo1hOePvUH.png
圖五:類別 CPersonInfo 裡的那些成員變數們的警告通通消失了

好,那既然錯誤都被解光光,我們就可以使用它啦~(請勿遐想,TypeScript 關心您

重點 3. 從類別建立物件的基本方式

若已宣告完類別 C,可以藉由 new 關鍵字從 C 建立出物件 O

let O = new C(/* 可能也會包含參數,會在建構子部分提及 */);

以下程式碼根據 CPersonInfo 建立出物件:

https://ithelp.ithome.com.tw/upload/images/20190918/20120614uVxX202Dig.png

筆者編譯過後之執行結果如圖六。

https://ithelp.ithome.com.tw/upload/images/20190918/20120614o6CJYkxTtb.png
圖六:物件被建立起來了

從圖六可以看到,我們的物件被建立起來了!而且將它印出來的結果,確實有成員變數們的蹤跡。然而,成員方法能不能使用?(以下程式碼編譯與執行結果如圖七)

https://ithelp.ithome.com.tw/upload/images/20190918/20120614CO0AhDL6ES.png

https://ithelp.ithome.com.tw/upload/images/20190918/20120614cg1czLsAFx.png
圖七:類別建立出的物件,也可以使用類別定義過的方法呢!

筆者這邊還要額外提到某樣功能 —— 儘管這可能對大部分人來說習以為常的事情。通常只要是程式碼編輯器 IDE ,對於類別建立出的物件的屬性與方法都會在讀者正在打字的過程中被顯示出來,這應該算是所謂的 Auto-Complete 的功能,TypeScript 也是不例外的。(如圖八)

https://ithelp.ithome.com.tw/upload/images/20190918/20120614k9XbvwZTg8.png
圖八:基本上,幾乎所有的 IDE 應該都會對具有類別概念的語言,使用其建立的物件時會出現的 Auto-Complete 功能。

如果仔細看圖八,它也提供使用該物件之屬性或方法的型別推論結果(Type Inference),在上面的例子即是:(property) CPersonInfo.age: number

類別建構子 Constructor Function

如果 CPersonInfo 類別將 name 的值固定為 'Maxwell'age 固定為 20 以及 hasPet 設定為 false,這樣實在是不好用。

因此會需要一個辦法 —— 在使用 new 建構新物件時,能夠傳入一些參數設定物件的內容,例如:

https://ithelp.ithome.com.tw/upload/images/20190918/20120614AP9SSRUUTN.png

不過讀者有沒有發現,CPersonInfo 在被呼叫的時候不是單純呼叫名稱,而是被看待成函式般地呼叫new CPersonInfo()

這個函式我們稱之為類別建構函式,又或者是類別建構子(Constructor Function)。

既然英文叫做 Constructor,剛好在 ES6 Class 定義的類別建構函式名稱也叫做 constructor,因此我們可以把剛剛的 CPersonInfo 類別的宣告裡,新增一個名為 constructor 的函式並且加入一些初始化值的邏輯。

(注意細節:是叫做類別建構函式,並不是叫類別建構方法)

https://ithelp.ithome.com.tw/upload/images/20190918/20120614Olr5ANMvct.png

筆者簡單解說以上的程式碼。根據函式型別推論篇章提及到的重點,筆者把它貼過來:

《函式型別 X 積極註記 - Function Types》 之 重點 1. Implicit Any

大部分的情況下,只要定義任何函式,TypeScript 通常會無條件推論函式內的參數(Parameters)為 any 型別,這種現象我們稱之為 Implicit Any。

從以上重點得知 —— 由於建構子被視為是函式的一種,所以建構子裡的參數一定要被註記,不然一律被 TypeScript 視為 any,引發 Implicit any 問題。

另外,讀者發現以上的程式碼裡,我們不需要再把預設值填進成員變數的後方。那是因為在建構子函式的時候,TypeScript 早就判定:這些成員變數的值,一定會在物件剛被建構的時候被開發者提供

當然也可以使用 ES6 的預設參數(Default Parameters)將預設的值改填到建構子函式的參數裡。

https://ithelp.ithome.com.tw/upload/images/20190918/2012061444uAXjIKTm.png

筆者寫一段簡單的程式碼測試類別建構物件的效果:

https://ithelp.ithome.com.tw/upload/images/20190918/20120614VATBqcOLu8.png

將以上的程式碼進行編譯過後再去執行之結果如圖九。

https://ithelp.ithome.com.tw/upload/images/20190918/20120614kfKT8m5fYR.png
圖九:類別建構子真的還蠻方便的~

當然,類別的函式參數的檢測,TypeScript 依然會幫助我們把關。(檢測結果如圖十)

https://ithelp.ithome.com.tw/upload/images/20190918/20120614WADOASl9aL.png

https://ithelp.ithome.com.tw/upload/images/20190918/20120614CTBLFy4APB.png
圖十:不管是填錯型別、呼叫不存在屬性或方法都會被 TS 警告!

儘管我們已經看到建構子的用處,筆者這邊整理的重點非常重要 —— 以下這些細節會不停在討論與類別(Class)相關的篇章中重複提及。

重點 4. 類別的建構子函式 Constructor Function

類別建構子函式有以下特點:

  1. 專門進行物件初始化的動作:若宣告某類別 C,則每一次建立屬於 C 類別的物件時,最先跑的程序即是建構子函式裡的內容。而 C 本身就是那個建構子函式。
  2. 類別的宣告不一定要存在建構子函式,而建構子函式的預設值是空函式。(即 function() {})前提是,該類別沒有繼承自其他的類別(Inheritance)。
  3. 若要在宣告類別時定義建構子函式的程式內容,必須使用關鍵字 constructor 作為建構子名稱。可直接把建構子當成函式撰寫;若建構子含有參數部分,必須積極註記
  4. 建構子函式通常只用來進行初始化成員變數,強烈建議禁止塞其他的邏輯進去
  5. 若必須要在物件建構之初,執行其他的 Business Logic,則建議將這些程序進行抽象化(Abstraction)並定義為當前類別之下的方法後,在建構子函式進行呼叫的動作。(通常抽象化過後的方法,會被標記為私有 private

初入類別(Class)的讀者,看到有些重點提到繼承跟 private 這兩個詞,可能會覺得模糊。筆者會在後續部分提到建構子的注意事項!讀者目前只要知道建構子的目的就是要初始化成員變數,其餘邏輯強烈建議不要放在裡面就好,不用擔心像是第二點講到的:

類別的宣告不一定要存在建構子函式,而建構子函式的預設值是空函式。(即 function() {})前提是,該類別沒有繼承自其他的類別(Inheritance)

筆者還沒有講到繼承(Inheritance)的概念,所以讀者目前先聽過這個名詞就好了

讀者可能還有一個問題:

為何只能放初始化成員變數的邏輯而不能放其他的 Business Logic 呢?所有東西擺在一起,這樣不是很方便嗎?

建構子的目的就是只有進行初始化動作

讀者如果記得在 Day 13. 短暫提到的 SRP(Single-Responsibility Principle)的話,它明顯主張:一個類別(延伸出去即是函式、介面等等)一次只專注做一件事情 —— 將這個概念類比到建構子函式,就是告訴你:“這裡是專門初始化物件的成員變數,不是叫你順便把其他邏輯塞在一起”。

小結

今天應該算是超入門類別(Class)的語法與意義教學!不過請不熟悉類別的讀者謹記這些名詞的代表意思:

  • 成員變數 Member Variables
  • 成員方法 Member Methods
  • 建構子函式 Constructor Function

後續篇章會一直用到這些名詞,而且我們還有一大堆類別相關的功能還沒講呢!

]]> Maxwell Alexius 2019-09-28 13:54:40
Day 17. 機動藍圖・複合型別 X 型別複合 - TypeScript Union & Intersection https://ithelp.ithome.com.tw/articles/10216794?sc=rss.iron https://ithelp.ithome.com.tw/articles/10216794?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

閱讀本篇文章前,今天什麼都不用想!

直接進入正文,快看下面!

筆者就直接讓油門繼續摧下去~正文開始

筆者 O.S.:今天又是數學時間,要學好程式可真不簡單,但學好數學可以應用在程式上,何嘗不可?

複合型別 Union & Intersection

數學意義的集合 V.S. 複合型別

基本上,筆者發現有些在學 TypeScript unionintersection 的過程,有人會以為,或者是有這樣的誤解

TypeScript unionintersection 跟數學上對於集合的聯集與交集的定義是一樣的

很抱歉,以上那句話 —— 大・錯・特・錯 —— 所以被筆者狠狠地劃上了很大的刪節線!

筆者有點想把取這個很讓人誤解的名詞的人給推入火坑

以下就是踢爆這個誤區的推論時間!

數學定義上的聯集(Union)與交集(Intersection)的概念,通常會有簡單的 Venn Diagram 來展示,我們有集合 A 與 B 呈現如圖一:

https://ithelp.ithome.com.tw/upload/images/20190918/2012061418y8AfaIew.png
圖一:集合 A 的範圍為左方橘色圈圈部分;集合 B 的範圍為右方藍色圈圈部分

A 聯集 B(又稱 A 與 B 的 Union)為 A 與 B 包含在一起的範圍 —— 其中,聯集過後元素不可重複,這是集合本身的特性。

A 交集 B(又稱 A 與 B 的 Intersection)為 A 與 B 重疊的範圍。

假設 A 集合包含以下元素:{ 1, 2, 3, 4 }
假設 B 集合包含以下元素:{ 2, 4, 6, 8 }

其中 A 聯集 B 的集合結果為:{ 1, 2, 3, 4, 6, 8 }
而 A 交集 B 的集合結果為:{ 2, 4 }

那以上的定義跟 TypeScript 的複合型別有什麼差別?

首先筆者先從最簡單的方向開始,也就是我們常看到的聯集 union

https://ithelp.ithome.com.tw/upload/images/20190918/20120614nVvxMLNKPs.png

如果按照數學的概念推理的話,A 為 numberstring 的聯集,也就是說 UnionSet1 可以是 numberstring。不過筆者再舉下個例子:

https://ithelp.ithome.com.tw/upload/images/20190918/20120614chPx85eLPo.png

讀者可能覺得:“作者是想表達什麼?這麼簡單的事情:UnionSet2 可以是 UserInfo1 或是 UserInfo2 啊,因為 UserInfo1UserInfo2 都是屬於 UnionSet2 的範疇。”

好,請注意這句話:

UnionSet2 可以是 UserInfo1 或是 UserInfo2

那根據數學推理,照理來說應該只會有這三種組合:

https://ithelp.ithome.com.tw/upload/images/20190918/20120614Tdc7FkUfRA.png

以上就留給讀者進行驗證,TypeScript 檢測結果不會出錯。

好的,那麼以下這些結果,根據強制性的數學推理:就算某變數被註記為 UnionSet2,而該變數裡的值已經完全滿足 UserInfo1UserInfo2 其中一種型別,若另一個型別若完全不滿足的話,理應來說 TypeScript 要發出錯誤警訊。(以下程式碼檢測結果如圖二)

https://ithelp.ithome.com.tw/upload/images/20190918/201206149oRfkYwem0.png

https://ithelp.ithome.com.tw/upload/images/20190918/20120614udBPFSdniM.png
圖二:理所當然,第一個案例一定錯,因為既不滿足 UserInfo1 也不滿足 UserInfo2;然而後續的例子,只要至少其中一個型別被判定滿足,不管其他型別有沒有完整補齊,TypeScript 認為無所謂

筆者舉一個更諷刺的例子:空集合。(Empty Set)

https://ithelp.ithome.com.tw/upload/images/20190918/20120614X7TtcUIC6j.png

讀者一看就知道這一定會出錯(讀者可以自行驗證),因為什麼都不滿足

然而,筆者必須請讀者回憶一下:“集合論裡,空集合不也是屬於任何聯集過後的集合中的元素嗎?” —— 這個機制就很像原生 JS 裡還是必須要有代表空值的 undefined 或代表這個概念的值的 null。(當然啦,總是會有開發者閒 nullundefined 這兩種東西比較起來還是很蠢,不過這裡筆者沒有什麼太大的異義)

藉由以上的試驗,TypeScript 的 union 型別完全不符合數學理論所預期的規則。綜觀 TypeScript 已經出現很久了,使用複合型別的過程中,開發者們在認為理所當然的應用情境下違反了以前學過的最基礎的數學原理,甚至也沒意識到這樣的嚴重性,造成錯誤的觀念亂散播出去。(一開始就對數學家的專業不尊重,遑論尊不尊重軟體開發者的專業

再來想一下另一種案例,數學的交集跟 TypeScript 的 intersection 有沒有差別。

然而筆者不得不說,這裡數學定義的交集又跟 TS 的 intersection 完全背道而馳!請讀者想想看,以下的案例:

https://ithelp.ithome.com.tw/upload/images/20190918/20120614KR0yEXhxh4.png

根據 UserInfo1UserInfo2 各自的型別格式,有沒有認真想過它們有何交集點

nameage 以及 hasPetownsMotorcycle 的組合 —— 兩組屬性的集合中,完全沒有交集!

偏偏在 TypeScript 裡 —— 交集 intersection 的用法是:將兩個可以為型別或介面的組合裡的格式進行結合的概念

也就是說,如果我們註記 IntersectionSet 在某變數 B 上,該變數 B 必須實踐出 nameagehasPetownsMotorcycle 這四種屬性!否則會出錯呢。(以下程式碼檢驗結果如圖三)

https://ithelp.ithome.com.tw/upload/images/20190918/201206148NqJCQFtGA.png

https://ithelp.ithome.com.tw/upload/images/20190927/20120614MBjE0oH2H5.png
圖三:屬性缺一不可啊!

在這裡,筆者必須向讀者澄清,這怎麼能說是交集呢?

筆者認為,我們應該換個想法 —— TypeScript 的 unionintersection 的原理沒有跟數學的集合論符合,但倒是符合另一個模型!讀者想得到嗎?

布林代數的邏輯(Boolean Logic)!

學了那麼久的 AndOr 邏輯,應該很明顯的:| 是我們常看到的 OR 的概念;& 則是我們常看到的 AND 的概念

想想看,剛剛使用 union 時的情境:

UserInfo1UserInfo2 進行 union -- 把它轉換成:將 UserInfo1UserInfo2 OR 起來之後是不是邏輯通順很多?

  • 你可以選擇只要符合 UserInfo1 要求的型別或介面格式
  • (OR)你可以選擇只要符合 UserInfo2 要求的型別或介面格式
  • 但你也可以全部都符合
  • **不過就是至少一個條件一定要滿足,否則出錯!**因此空集合概念在這裡也不覆存在,會被認定是錯的,因為 OR 邏輯本來就是建立在其中一方符合的條件下才能滿足的。

對比使用 intersection 時:

UserInfo1UserInfo2 進行 intersection -- 把它轉換成:將 UserInfo1UserInfo2 AND 起來之後是不是邏輯通順很多?

  • 你必須符合 UserInfo1 (AND)UserInfo2 的型別或介面格式
  • 只要少了一個屬性就會出錯,完全符合 AND 邏輯的真諦

當初取 unionintersection 這兩個名稱的研發者,不是數學概念不好,就是亂用取錯名,簡直是誤導群眾、誤導開發者。

重點 1. 複合型別的基礎法則 Fundamental Law of TS Intersection & Union

複合型別(intersectionunion)在 TypeScript 的運作邏輯完全不等於數學裡集合論的定義。相對地,複合型別的概念反而是跟布林邏輯的概念符合

重點 1. 都已經用『 法則 』這兩個字形容了,應該可以提醒讀者這個概念的重要性了吧。

重點 2. 複合型別的語法與規則

假設某型別化名 AB,其中 AB 各自可以為型別或者是介面,亦或者都是型別或介面,其中 TUnionABunion;而 TIntersectionABintersection,則:

type TUnion        = A | B;
type TIntersection = A & B;

任意變數 C,其中 C 被註記為 TUnion 型別,則 C 的值必須至少符合 AB 其中一項型別的完整靜態格式(或實踐出其中一個介面裡的所有功能,如果 AB 有存在介面宣告的話)。

任意變數 D,其中 D 被註記為 TIntersection 型別,則 D 的值必須完全符合 AB所有的型別靜態格式(或實踐出其中一個介面裡的所有功能,如果 AB 有存在介面宣告的話)。

接下來就要看一些使用 unionintersection 會發生的莫名有趣現象。

讀者可能以為 unionintersection 就只是剛剛講的就結束了~

沒有喔,還有很多可以講 XD,因此筆者才會放在很後面。

原始型別的複合 Primitive Types Union & Intersection

我們剛剛有展示過,原始型別的複合 -- 對 AB 型別使用 union,其中 A 不等於 BAB 皆屬於原始型別

我們都很熟悉 AB 進行 union,然而我們可曾想過將這 AB 型別作 intersection 的結果?

試想一下:number & string 到底是什麼?其實讀者如果有把本系列文章讀熟一定會有答案呢!

筆者突然浮現出的 Brainstorming 過程:

“恩 ...... 要能夠同時為 number 以及 string ... 難道不是空集合嗎?”

“可是空集合在 TypeScript 作者好像沒講到啊 ...”

“世界上真的有既是數字和字串的東西嗎?還是說將數字加兩個引號就可以了?就像這樣:'42'

筆者回答:當然不存在,但是隱身於所有型別當中的共通點 —— 以下這句話節選自 Day 10. Never 型別

never is a subtype of and assignable to every type.

哦~其實這也挺合理的:“既然是不存在的型別交集,最後的結論理應是:例外狀況不可能的狀況出現,因此推得兩個原始型別的交集結果是 never 型別”。(印證的結果如圖四)

https://ithelp.ithome.com.tw/upload/images/20190918/20120614bJyoGGAp0Q.png

https://ithelp.ithome.com.tw/upload/images/20190918/20120614kTopT3guv3.png
圖四:原始型別交集結果就是 never

讀者是不是覺得 Never 型別存在的意義 比想像中重要?筆者認為其他的教學資源基本上只是幾百字不到淺淺帶過 never 型別的語法與用途 —— 但從來沒有把 never 的真諦傳出去,實是可惜,不然這些特殊型別的行為也挺有趣的。

重點 3. 原始型別的交集

TATB 皆為原始型別,TATB 不相同,則兩型別被 intersection 的結果為 never 型別:

type MustBeNever = TA & TB;

因此,MustBeNevernever 型別

讀者試試看

如果將廣義物件型別跟原始型別進行 intersection

  1. 結果判定是 never 嗎?
  2. 如果不是的話,那效果上跟 never 差不多嗎?

不過讀者仔細想想,這不就跟 Never 型別那一篇 ,後面的 intersection 提到的概念很像嗎?

《 Day 10. 特殊型別 X 永無止盡 - Never Type 》之 重點 2. never 型別為所有型別的 Subtype

任何型別 T(包含 never 本身)和 never 進行 union,則型別 T 會吸收掉 never 型別:

type WontBeNever = T | never;
// => WontBeNever: T

任何型別 U(包含 never 本身)和 never 進行 intersection,則型別 U 會被 never 型別強行覆蓋:

type MustBeNever = U & never;
// => MustBeNever: never

其實本篇的重點 3. 不過就是 Never 型別篇章 重點 2. 的擴充呢!

型別檢測 Type Guard

其實筆者真的覺得中文好難翻,甚至也懶得去查 —— 寫作到這裡偶然查到原來 Generics 的翻譯為泛用型別並不是通用型別。(筆者掩面感到丟臉)

因此,可能會將文章裡的使用詞再進行修改。不過 Type Guard 筆者也很難找到翻譯,只知道可以被形容成型別限縮的概念

後來覺得『 型別檢測 』這名詞好像也不錯,乾脆就採用這個名詞 —— 作用依然還是跟型別的限縮有關!

讀者如果看過本系列,這個技巧應該在 Any v.s. Unknown 型別篇章 遇過一次,那時候是在討論 —— 如果變數被註記或推論為 unknown 型別時,該變數基本上什麼事情都不能做,這裡原封不動貼上當時的重點:

《Day 11. 特殊型別 X 無法無天 - Any & Unknown Type 》之 重點 2. unknown 型別下的變數指派限制

  1. any 型別相似的地方在於,若變數被 unknown 型別註記,則該變數可以被任意型別的值指派
  2. 若被註記為 unknown 型別的變數,除了以下情形以外,否則不得將其值指派到任意型別(除了 unknownany)的變數裡:
  • 顯性註記之型別 T 等同於被指派到的變數之型別 T
  • 根據程式的控制流程分析,其 unknown 型別的推論被*限縮到特定的型別 U*致使可以被指派到其他符合型別 U 條件的變數

其中第二點的最後一句話:

“根據程式的控制流程分析,其 unknown 型別的推論被限縮到特定的型別 U 致使可以被指派到其他符合型別 U 條件的變數”

關鍵字是當時筆者說的兩個點(不是人體上的兩個點,筆者的有些朋友會這樣亂想,但這是公眾場合XD):“控制流程分析”與“推論被限縮”。

這很明顯在說明 Type Guard 的重點 —— 藉由簡單的判斷敘述來限縮型別的技巧,因此當時候才會有這個範例:

https://ithelp.ithome.com.tw/upload/images/20190915/20120614rIZqeAiSgg.png

不過筆者這裡不在多做以上程式碼範例的說明,認為需要再複習 unknown 型別的概念請參考 Day 11.

那筆者為何到現在才講 Type Guard?

通常碰到 union 過後的型別,多數狀況下我們必須主動使用 Type Guard 讓 TypeScript 編譯器不會哀哀叫。其實之前在 介面的函式超載篇章 裡的例子舉得不錯,我們重新來看:

https://ithelp.ithome.com.tw/upload/images/20190917/20120614iJy9EDu3qF.png

當時 AddOperation 的介面長這樣:

https://ithelp.ithome.com.tw/upload/images/20190917/201206143asAVcJzgK.png

其中我們遇到的狀況是,要實踐 AddOperation 難免會遇到 union 的狀況,該函式的參數 p1p2 各自可為 stringnumber,因此裡面才會需要 if...else... 判斷式進行參數型別的判斷。

我們今天來看看另一種狀況。

https://ithelp.ithome.com.tw/upload/images/20190918/201206144rqk9P0dD5.png

以上這個 ISummation 介面是屬於純粹函式格式的介面,也引用了函式超載的概念。

這裡想要達到的效果是 -- 假設某函式 F 已經實踐了 ISummation 所訂立的功能規格,則:

https://ithelp.ithome.com.tw/upload/images/20190918/201206141MwJQbqDjy.png

其中,若讀者不熟悉 ...args 這種在函式參數裡面的行為(這個叫做匯聚操作子 Rest-Operator),請多參考社群們熱心教學的 ES6 系列文章 ~

那麼以下筆者就不客氣地丟出實踐結果。

https://ithelp.ithome.com.tw/upload/images/20190918/20120614tezJ0FiLZu.png

筆者這一次是第四次編譯本並且測試結果。讀者如果曉得流程,應該也會直接果斷下 tsc 然後再去用 node 執行編譯出來的 index.js。(結果如圖五)

https://ithelp.ithome.com.tw/upload/images/20190927/20120614ARdiOGoyTG.png
圖五:驗證結果為正確

貼心小提示

有些讀者認為,檢測陣列可以用 Array.isArray,這一點也是沒問題的!不過記得要在 tsconfig.json 裡進行微調:

{
  "compilerOptions": {
    /* 略... */
    "lib": ["dom", "es2015"],
    /* 略... */
  }
}

有關於編譯器設定將會在《戰線擴張》系列進行介紹,筆者已經確定那時候是 30 天以後囉~

這裡筆者想強調的重點是 —— 通常檢測原始型別都是為用 typeof 這個關鍵字。

`typeof value === ''

而通常廣義物件或類別(Class)建造出來的物件則是會用 instanceof 這個關鍵字:

someObject instanceof ObjectBelongingClass

最常遇到的應該是諸如此類的問題:如何寫限縮型別的判斷式。

重點 4. 限縮型別的技巧 - 型別檢測 Type Guard

  1. 若想要過濾出純原始型別的值的話,使用 typeof 操作子
  2. 若想要過濾出廣義物件型別的值的話,使用 instanceof 判斷操作子,並填上屬於該物件型別所屬的類別
  3. 其他方式,譬如 Array.isArray 可以檢測陣列

小結

今天總算了結複合型別,筆者實在是感到溫馨。(讀者看到篇幅大小感到煩躁

筆者接下來要進行的是 TypeScript Class 也就是類別部分的介紹啦~

筆者這邊再次強調:你不需要懂 ES6 Class,筆者幫你建立這方面的基礎,因為基本上學完 TypeScript Class 就等於你會了 ES6 Class 大部分的內容~

而類別的部分也是為了鋪陳本系列後續的重頭戲必備的知識呀!

]]> Maxwell Alexius 2019-09-27 15:28:40
Day 16. 機動藍圖・介面與型別 X 混用與比較 - TypeScript Interface V.S. Type https://ithelp.ithome.com.tw/articles/10216626?sc=rss.iron https://ithelp.ithome.com.tw/articles/10216626?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

TypeScript 的型別系統與介面之間在語法上的差別與介面或型別的使用有何共通點呢?但意義上又會差在哪?

如果還沒理解完畢的話,可以參考關於 Interface 系列的這幾篇:

亦或者來看這一篇!

本篇是筆者紀錄自己認為 —— 正確使用 typeinterface 的方式。

所以這一篇不屬於官方,但也可能也不屬於世界上任何一篇文章。(可能有人會跟我的想法相似?)

另外,本篇可能會跟 Day 13. 內容有相似的地方,但是是綜合所有介面與型別的功能與意義比較。

以下正文開始

Interface 與 Type 的終極擂台

本篇章強烈要求讀者,能夠對 Day 02. 講到關於型別推論與註記 累積到現在的知識,越熟越好!畢竟是 Type 和 Interface 各層面之間的比較 —— 總集篇的概念。

翻過很多資料後的心得

上網時,覺得有些 Medium 文章的作者對 TypeScript 的 typeinterface 應用層面的敘述,依然感到模糊 —— 並不是語法部分不熟,而是沒有把運用時機講清楚。

來自外國的 TypeScript 開發者,有些文章就直接寫:

“儘管語法宣告不ㄧ樣,應用層面大部分都是一樣,因此你用 type 跟用 interface 都沒差”

他們確實有告訴讀者到底 typeinterface 的語法規則跟功能差異在哪裡,但問題來了 —— 筆者沒有看到他們回答如何在哪個情境下使用 type 或者是 interface

上面那句話確實是對的!typeinterface 型別註記起來過後的行為都一樣。

定義出一個型別或一個介面,只要它被註記在變數上,該變數存的值若忘記型別註記裡要求的屬性,亦或者是屬性對應的型別錯誤 —— 變數被註記的是 interface 還是 type基本上都沒差,TypeScript 照樣會找出潛在問題的發生點,註記(Annotate)過後的行為都差不多。

更不用說:上一篇 筆者介紹的更多介面的功能 —— Indexable Types、readonly 屬性、選用屬性等,也都可以使用在 type 的宣告上。

筆者唯一不苟同的是 —— 文章講完語法上差別之後,就沒有講到意義上的差別。另外,TS 官方在 interfacetype 上面的比較,寫得還是讓筆者看不太懂,因此特地再網上翻了一下 StackOverflow,結果找到這篇很完整的分析它們之間的差別;然而,得出的結論是:官方 Doc 是 Outdated 的狀況

恩...... 哈哈!(這是要叫筆者怎麼推人入坑,筆者想辦法找到當初寫 Doc 的人,先把他們推入火坑再說

不過這也是學習 TypeScript 遇到的雷,但是越多人知道,相信要更新也不是時間的問題!

當然也有人貼 TypeScript Type 跟 Interface 的差別,但光是看這個表:

https://ithelp.ithome.com.tw/upload/images/20190917/20120614kvO9BMU2o5.png

連筆者必須誠實的說:“短時間內沒辦法把所有的狀況跟讀者交代清楚啊!”

因此,以下是筆者暴力地把這兩個纏來纏去的東西理出來的結果。今天會把整個系列提及到 interfacetype 這兩者的相關內容都用比較的方式整理出來。

語法上比較 Syntatic Comparison

這應該對讀者來說很簡單。 XD

type T = U;

interface I {
  P: U;
  ...
}

型別化名就是用 type;介面宣告則是用 interface

應用上的比較 Application Comparison

1. 介面可被擴張 Interface Extension/Inheritance

只有介面 interface 可以被擴展 (使用 extends):

https://ithelp.ithome.com.tw/upload/images/20190917/20120614c4INCjB5n2.png

參照 Day 13. 重點 1.

2. 介面可以被融合 Declaration Merging

只有介面可以被重複宣告,但重複宣告的行為 —— 最後是交集的成果

https://ithelp.ithome.com.tw/upload/images/20190917/20120614lvgMPEtlRl.png

參照 Day 14. 重點 2.

介面不能直接模擬原始型別、Enum 與 Tuple

介面的定義格式除了單純物件格式外,也可以單純定義函式型別的介面(亦或者混合型介面);而根據 Indexable Types,介面可以模仿陣列的行為

但介面無法單純模仿原始型別列舉元組,只能用物件格式進行混用。只有型別系統 type 才能單純化名原始型別、列舉與元組。

而 JSON 物件、函式與陣列的共通點是JS 物件的各種表現形式(又被筆者稱作廣義物件),因此才可以被介面描述出來。

介面與型別的共通點

不管是宣告型別 T 或介面 I,基礎的 TypeScript 型別檢測基本上行為都一模ㄧ樣。若某變數 A 被註記型別 T 或被註記介面 I

  • 變數 A 存的值少了型別 T 或介面 I 規範的屬性:被 TypeScript 警告
  • 變數 A 存的值多出原先沒有在型別 T 或介面 I 規範的屬性:被 TypeScript 警告
  • 變數 A 存的值之某屬性在型別 T 或介面 I 裡有規劃,但該屬性存的值之型別跟 TI 規範的不ㄧ樣:被 TypeScript 警告

由於介面跟型別都可以表示廣義物件格式(包含普通 JSON 物件、函式、陣列、類別與類別建構的物件),因此可以推論,以下功能兩者都可以使用:

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

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

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

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

以下也是可以使用的功能,卻沒被筆者提及過:

  • 介面、型別都可以進行複合(unionintersection),甚至也可以介面與型別一起複合

https://ithelp.ithome.com.tw/upload/images/20190925/20120614O3EaU7KAyB.png

  • 介面的擴展除了可以從其他介面擴展外,也可以從型別進行擴展(但該型別不能為原始型別

https://ithelp.ithome.com.tw/upload/images/20190925/20120614uJqkw1jvRv.png

筆者認為有爭議性的:『 兩者都可以被擴張(Extend)』的這種說法

這一點在 Day 13. 有討論過,但 Medium 文章上面有這種說法的案例層出不窮,讓筆者感到煎熬。

筆者認為 —— 介面可以藉由 extends 進行擴展;然而,儘管型別可以藉由 intersection 進行複合,但個人認為那不叫做型別的擴展,那應該被稱做定義新的靜態型別。所有提到這個詞 —— Type Extension 根本就是誤導靜態型別的意義。

以下就把筆者剛剛提到的介面與型別混用的狀況討論清楚。

假設已宣告某型別 T 與某介面 I,並且運用複合型別宣告新型別化名 U

https://ithelp.ithome.com.tw/upload/images/20190917/20120614qaFwI3aOR0.png

我們稱這個作法的意義為:“定義新的型別化名 U,其型別的靜態格式為型別 T 的內容與介面 I 的屬性與方法”

假設已宣告某型別 T 與某介面 I,並且宣告新介面 J,其中型別 T 不為原始型別,則:

https://ithelp.ithome.com.tw/upload/images/20190917/201206148Hf80oKGiC.png

我們稱這個作法的意義為:“宣告新的介面 J,任何變數註記此介面,必須實踐出型別 T 的靜態屬性與介面 I 的所有功能

不過讀者看到這裡,可能會問:

為何介面與型別混用的狀況是可被接受的

筆者給出的個人見解是:如果混用狀況不能被接受的話,型別化名的宣告會比較限制些

比如,我們也可以幫靜態型別定義一些介面 —— 將靜態的屬性格式進行拆解,抽象化的動作。

https://ithelp.ithome.com.tw/upload/images/20190925/201206147XSt111kwi.png

這樣一來,介面的運用上,除了可以被介面與類別使用外,也可以延伸出靜態資料的格式,也就是型別化名。

這只是筆者想到的其中一個原因,不過個人相信應該還有更深的理由在。當初筆者也是認為介面與型別混用在一起是很怪的行為,可是後來發覺,不混用好像也不對,限制反而又被限制太多。

貼心小提示

介面與類別的使用會在 Day 25. 提到,因為類別可以討論的東西很多;再者,TypeScript 類別比 ES6 Class 又多了更多 Feature 可以使用,因此考慮過後,筆者以沒有使用過 Class 為前提,重新認識 Class 的語法與概念,一步一步地進入主題。

意義上的比較 Significance Difference

相信有看到 Day 13. 的文章的讀者應該知道 Type 與 Interface 意義差別在哪裡。

介面(Interface)代表更有彈性的型別表現形式,跟規格的概念很像,也具備擴充功能(Interface Extension)與融合功能(Interface Merging)。

型別(Type)代表靜態的型別格式,因此不建議將交集(intersection)的行為想成擴充型別的行為,而是 —— 宣告新的靜態型別格式,延用交集前的型別靜態格式或介面的規格。

使用情境上的比較 Usage Difference

乍看之下感覺好像都可以使用,不過筆者非常強調兩者之間的不同。

本篇唯一重點. Type 與 Interface 使用建議

以下為筆者認定的 typeinterface 使用的建議:

  • 遇到不希望被人擴充單純想代表一種獨立的資料格式的概念時,使用型別的宣告 type
  • 如果單純是原始型別或者是要表示為列舉型別、元組型別,一定只能使用 type 進行宣告
  • 型別複合(使用 unionintersection)的過程通常都是使用 type 進行宣告
  • 遇到功能是可以被重複再利用該功能可能會被多方程式碼或第三方套件依賴,使用介面的宣告 interface
  • 物件格式通常建議用 interface,使用起來彈性較大
  • 混合使用型別與介面是可以的,但就是要記得:程式碼到底想要表達的重點是什麼
    • 混用過後不希望再被擴充代表獨立靜態的型別格式就應該要用型別化名的宣告 type,藉由 unionintersection 達成
    • 混用過後的結果是可以被擴充或多方利用,則應該要定義成介面,藉由 extends 去達成

小結

本篇文章雖然字數算是系列中偏少的,但筆者認為 —— 塞再多也沒用,因為都是繞在 Type 跟 Interface 的比較。重點在能不能分辨什麼時機運用 Type 或者是 Interface 是本篇主要想傳達的重點。

下一篇筆者要補足複合型別(unionintersection)的註記與使用意義~ 敬請期待吧~

]]> Maxwell Alexius 2019-09-26 15:11:41
Day 15. 機動藍圖・功能多樣性 X 多樣性介面 - More on TypeScript Interface https://ithelp.ithome.com.tw/articles/10216541?sc=rss.iron https://ithelp.ithome.com.tw/articles/10216541?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

  1. 到目前為止對於 TypeScript Interface 介面的理解到什麼程度呢?
  2. 你認為 TypeScript 和第三方套件/專案/框架協作上會有什麼困難點?(本系列後續文章還會探討更多有關的議題喔!)

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

本日正文前的廢文稍微多一些,請讀者多多諒解~

哇,這已經是第 15 篇了! —— 恩... 本系列文原本打算讓讀者 30 天內速成 TypeScript;不過後來筆者轉換方向,改成以推讀者入坑 TypeScript 為目標(或者不一定用到 TS 但至少從中學到東西),因此才寫作本系列 —— 不過每篇文長也是有些長,不知道有沒有起到效果,寫作過程中也是感到迷茫與不確定的時候。

當初開始寫這篇系列文前,原本沒有想過要入賽。筆者原本是想學 Angular 這個框架,不過後來看到 Angular 是用 TypeScript 作為主流開發語言,心想可能要學習一下 TypeScript

後來不知道什麼原因,覺得 TypeScript 實在是太有趣,於是爬滿整個文件跟其他 TypeScript 領域的神人們的教學影片與文章,結果不知不覺兩週過去,偶然看到鐵人賽的報名資訊

然而,看到鐵人賽的報名時,也已經是末尾(差不多剩下一週可以報名),所以才非常緊迫地訂出草稿,並且開始趕文章。打完草稿後丟出來,完蛋 —— 範圍太大啊!啊!啊!啊!啊!啊!超級悲劇!

積極撰文的同時,前十四天的文章也再逐步改善中。(主要是文法部份,本人國文造詣超爛,口語多到炸,連接詞用得很悲劇)前七到八天的文章,筆者重看覺得很丟臉 —— 發文前一天會強迫自己重新審文,每天下午發文一定在按下送出的按鈕前重新看一遍,能夠刪減冗言贅字就刪、換句話說更通順就換。

如果筆者能夠提早看到鐵人賽時間,並且提早準備,應該還可以刪減內容。不過後來想說算了 —— 寫就寫,鐵人賽頂多就這段期間。XDDDD

貼心小提示

要參賽的讀者請不要衝動,三思而後行;但是也可以學筆者這樣 —— 不入虎穴、焉得虎子,搞不好可以教出老虎五隻?

[2019.09.25. AM 00:03 新增訊息]
本系列從 Day 12. 開始到鐵人賽完整 30 天,確定會是《機動藍圖》系列篇章 —— 後續的《戰線擴張》系列會以延長賽方式進行!

讀者不要認為作者只講到一半就不寫了~(俗稱射後不理)

本系列作者不是落跑作者啊!!!!!(好歹本系列 30 天後完成趴數有達到 6% 以上!哈!哈!哈!哈!哈!)XDDDDDDD

好了,今天應該也會講得很輕鬆~(希望不要再像昨天一樣,講到後面扯到專案開發上的協作問題,想收尾也難收)

正文開始

更多介面的功能

貼心小提示

由於本篇已經是本系列的中間篇章,可能有新的讀者(誤入),因此筆者再進行名詞解釋:

  • 狹義物件指得是單純 JSON 物件({} 的格式)
  • 廣義物件指得是包含狹義物件外,還有陣列、函式、類別以及類別建造的物件等等

模仿部分廣義物件的行為 - Indexable Types

“恩... 介面不都是只有狹義物件(也就是 JSON 物件)的表現形式嗎?難道其他物件的格式(例如:陣列)也可以被模仿?”

筆者這邊是以個人見解來表示:TypeScript 的介面以及型別都有 —— 針對物件的屬性或索引做更神奇的微調,這個功能稱之為 Indexable Types(中文好難翻:可控索引型別?筆者還是不要亂翻譯好了),它的寫法是這樣:

https://ithelp.ithome.com.tw/upload/images/20190917/20120614qdQvSqYp3C.png

我們先來看第一個例子,也就是 Dictionary 這個型別。

[propName: string]: T 的意思是 —— 只要屬性為字串型別,其對應的值之型別必須要為 T。以 Dictionary 的例子來看,這裡的 T 是字串型別 string;也就是說,任何字串型的索引只能接字串型態的值。

以下舉幾個使用 Dictionary 的狀況(以下程式碼被 TypeScript 檢測結果如圖一)。

https://ithelp.ithome.com.tw/upload/images/20190917/20120614OuZeYQJm1H.png

https://ithelp.ithome.com.tw/upload/images/20190917/20120614mkJvdr6E2z.png
圖一:基本上,只要屬性對應的值非字串的話,就會被 TypeScript 警告

由以上結果得知,藉由這種方式可以讓使用者任意新增屬性,但對應的值必須被鎖定在特定的型別(Dictionary 為例,就被鎖在 string 這個型別)。

再來是第二個例子的使用情況 —— StringTypedList 這個介面,但它是使用 [index: number] —— 也就是屬性為數字型態。(檢測結果如圖二)

https://ithelp.ithome.com.tw/upload/images/20190917/20120614GJx9xNvkmf.png

https://ithelp.ithome.com.tw/upload/images/20190917/20120614USmd4klOd9.png
圖二:StringTypedList 檢測結果

這邊要特別注意的點有:

  • 不能直接用陣列形式初始化值(除非是空陣列),由於陣列屬於 JS 物件的一種,而物件的屬性初始化時不允許為 string 以外的型態,因此初始化陣列的索引(index)都會以 '0', '1', '2' ... 這種字串的數字形式初始化,所以才會被 TypeScript 判定結果是錯誤!(特別再把錯誤的部分截在圖三中)
  • 為何空陣列可以被初始化則是因為 TypeScript 認為都沒有屬性,沒有檢測之必要
  • [index: number] 將索引部分鎖定在 number 型別上,目的是為了防止開發者呼叫字串型別的屬性(或是用點的方式呼叫屬性),而是模擬陣列的行為 -- 用數字來檢索該物件裡的內容
  • 當物件必須在 [index: number] 這種狀態下初始化時,必須用 JSON 物件的格式,指定索引的位置(數值)並填入對應的值(當然,值必須符合型別 T,其中 T[index: number]: T 裡面的 T

https://ithelp.ithome.com.tw/upload/images/20190917/2012061441cGSRcpHw.png
圖三:儘管屬性專門接收數字型別,但卻不能直接初始化為陣列

另外,StringTypedList 的介面的實作有點類似雜湊表(Hash Table,又被稱為哈希表,直翻感覺有點怪怪)

重點 1. Indexable Types

若想限制物件索引為特定型別 —— 比如 numberstring,可以使用以下的格式。其中,Indexable Type 的實作可以在型別系統或介面出現

[keyName: TKey]: TValue 

其中,必須遵守下面的規則:

  • TKey 必須為 number 或者是 string 其中一種,不能為其他型別與 numberstring 的複合格式(連 number | string 是不接受的!)
  • TValue 可為任意型別

(由於 ES6 有新的 Symbol 型別出現,目前有在討論新增 Symbol 作為索引可以接受的型別)

讀者試試看

筆者提供一些沒辦法在短短篇幅內討論的問題,因此這裡給有興趣的讀者去想想:

  1. 如果知道 ES6 Symbol 這個功能的讀者,有沒有辦法實踐出 [keyName: Symbol]: TValue 這種形式?
  2. 這樣的寫法在 TypeScript 是可以被接受的嗎?
type UseBothKeyType = {
  [key: number]: T1,
  [key: string]: T2
};
  1. 這樣的寫法會不會出現問題?
type UserInfo = {
  name: string,
  [prop: string]: string;
}
  1. 承上題,那這樣的寫法會不會出現問題?
type UserInfo = {
  name: string,
  birth: Date,
  [prop: string]: string;
}

選用屬性 Optional Properties

在前幾篇的範例有展示過,這裡稍微提及:

https://ithelp.ithome.com.tw/upload/images/20190917/20120614cRGoGh7y7Y.png

基本上,介面裡運用選用屬性跟型別裡運用的結果都差不多,不過這裡就照搬 Day 13. 探討過後的結果:

  • Interface 介面的意義是規格
  • Type 型別的意義則是靜態的物件型別格式

至於選用屬性在介面時的行為 —— 就由讀者自行發掘吧~基本上參照 Day 09. 選用屬性 Optional Properties 的文章,在介面時的效果也差沒多少呢。

唯讀屬性 Readonly Property

唯讀(Read-only)屬性標註非常簡單,就是在屬性前加入 readonly 的關鍵字,該屬性就會變成唯讀模式(Read-Only)。另外,這個功能型別系統也可以使用

https://ithelp.ithome.com.tw/upload/images/20190917/20120614WToQlxQg2w.png

以下簡單地測試一下。(TypeScript 檢測結果如圖四;錯誤訊息如圖五)

https://ithelp.ithome.com.tw/upload/images/20190917/20120614kJiLyvvmNY.png
圖四:硬想寫入含 readoly 的屬性,會被 TypeScript 阻止

https://ithelp.ithome.com.tw/upload/images/20190917/20120614FbRydkNeOh.png
圖五:TypeScript 會提醒你,email 屬性是不可以被覆寫的(property 'email' is read-only property

重點 2. 唯讀屬性 Read-Only Property

若某型別 T 或介面 I 有包含某屬性 P,其中屬性 P 前面有標註 readonly

type T = {
  readonly P: TAny;
};

interface I {
  readonly P: TAny;
}

則任何經由型別 T 創造出來或經由 I 實踐出來的廣義物件,這些物件的屬性 P 只能讀取不能進行覆寫。

介面的混合格式 Hybrid Type Interface

英文挺容易讓人誤解 - Hybrid Type 指的不是型別系統裡的一種 type 的表示方式,而是指介面的宣告方式可以用混合的方式呈現

還記得介面的宣告是哪兩種方式嗎?筆者就聞風不動把 Day 12. 裡的重點搬過來:

《Day 12. 介面宣告 X 使用介面》 之 重點 1. TypeScript 介面(Interface)的定義與種類

TypeScript Interface 的定義方式為,使用關鍵字 interface 而後接介面的詳細定義:

  • 物件格式:即 JSON 格式,是為屬性對型別,不是對值
  • 單一函式格式:沒有任何屬性,就是函式而已,但不一定需要標上函式名稱
  • 混合格式:即將『物件格式』跟『單一函式格式』混合在一起

以下舉計數器 Counter 的介面為範例。

https://ithelp.ithome.com.tw/upload/images/20190917/20120614FPBkdnQaV3.png

根據以上的程式碼,筆者進行簡單的實作:

https://ithelp.ithome.com.tw/upload/images/20190917/201206141MhFvvUIQK.png

將此程式碼進行 TypeScript 的編譯並且使用 node 執行過後如圖六。

https://ithelp.ithome.com.tw/upload/images/20190917/20120614wD1Ot075ew.png
圖六:結果順利地按照順序印出 585 這三個結果

儘管混合式的介面可以做出更多不同的物件形式,然而筆者認為這不太是個好方法。

如果忘記要把整個介面的屬性或方法實作出來的話 —— 譬如將 increment 方法拿掉。(TypeScript 檢測結果如圖七)

https://ithelp.ithome.com.tw/upload/images/20190917/201206144yokobCvdx.png

https://ithelp.ithome.com.tw/upload/images/20190917/20120614PpkJfC5Ntd.png
圖七:結果 TypeScript 沒有理人啊!

編譯過後沒有出錯,但上面的程式碼執行過程一定會出錯。(錯誤結果如圖八)

https://ithelp.ithome.com.tw/upload/images/20190917/201206146M5sHfluEC.png
圖八:counter.increment 根本是未定義的狀態

讀者可能問說,為何這時候 TypeScript 沒辦法檢查這個錯誤呢?

筆者其實也搞不清楚這裡的註記與推論機制到底是什麼。之所以提及這個部分的主要目的是為了展示給讀者:“你可以使用介面的混合型態,不過使用起來可能會遇到這種問題 —— 忘記實踐出混合型態介面裡的屬性與方法”。

筆者目前也想不太到什麼情況下會用介面的混合型態(Hybrid Type Interface),因此沒花太多時間研究這個雷點。

混合型態的用法在官方有說明,但筆者仍然認為:“TypeScript Class 和 Interface,光是學好基本的 OOP 與設計模式就夠實用了!”

所以呢,這裡筆者就不下重點了~讀者自己瞧吧!呵!呵!呵!呵!呵!(有病)

小結

本日篇章主要是把介面的雜項部分補齊。讀者應該會發現,除了混合型態的介面以外,其他的 Feature 都可以在 typeinterface 使用,這會讓學 TypeScript 的初心者們覺得:介面跟型別系統好像都沒差的

下一篇就是要統整型別系統跟介面的比較!因為筆者基本上已經把該介紹的介面相關語法都介紹完了。

另外,讀者有沒有發現一件事情?從本系列開講到目前為止 —— 執行 TypeScript Compiler 的次數:只有 3 次

  • 第一次是開篇的 Hello World 範例
  • 第二次是 Day 07. 解釋 Enumerated Type 具有反射性時,編譯 TS 的結果進行討論,不是測試用喔!
  • 第三次是今天這一篇,用來展示混合型態的介面的簡單的程式碼

使用普通 JS 的狀況,大部分得執行過程式碼,才能根據 Error Stack 結果除 Bug。

相比起來,TypeScript 的好處就是:

“不需要經過編譯,TypeScript 編譯器就會靜態地提醒程式碼哪裡會有潛在的 Bug”

這裡當然不是指 TypeScript 一定比原生 JS 好,反倒是先把原生 JS 基礎熟練過後,學習 TypeScript 才會如魚得水。(筆者回想起學 JS 的經驗,過程也是蠻莫名其妙的 XD,有空再說)

回過頭來,想要讓上述 TypeScript 的優點發威也是需要使用者非常清楚哪些事情是在 TypeScript 可以做的。

只要非常~非常~清楚 TypeScript 的推論與註記機制基本上就已經免除六到八成的 Bug,其餘就是學習更多語法與應用,剩下兩成可能就是一些開發者需要注意的或真的是稀有的案例,連筆者腦袋也想像不到的狀況呢~

筆者認定:後面的文章講再好,《前線維護》系列篇章是入門 TypeScript 基礎中的基礎

]]> Maxwell Alexius 2019-09-25 16:56:21
Day 14. 機動藍圖・函式超載 X 究極融合 - Function Overload & Interface Merging https://ithelp.ithome.com.tw/articles/10216153?sc=rss.iron https://ithelp.ithome.com.tw/articles/10216153?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

  1. 試問介面跟型別系統的差異性在哪?
  2. 為何要儘量對程式碼進行抽象化的動作?

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

什麼是函式超載!?

沒有接觸過 OOP 的讀者們不要害怕~ 讓我們以輕鬆的方式:正文開始

介面的函式超載 Function Overload

看到這個東西,應該很少會把 JavaScript 提供的功能連結到這個概念:函式超載(Function Overload) —— 畢竟 JS 本來就沒有這個功能

因此這裡簡單介紹一下 —— 什麼是函式超載(Function Overload)

通常在靜態型別的語言 —— 如 C++、Java 等語言,必須嚴格對函式(或方法)的輸入與輸出強制進行型別註記。尤其這一類的語言,型別的種類性比 TypeScript 或原生的 JS 所規範的還要細緻 —— 光是 JS 的 number 型態就會被 C++ 細分成 intfloatlongdouble 還有以上型別的特定組合。

這裡筆者開始舉例:想要在 C++ 裡面定義一個計算四邊形面積的函式,除了基本的案例 -- 正方形的例子:

int areaOfRect(int size) {
  return size * size;
}

我們還可能會遇到更多組合變體,譬如計算長方形面積:

int areaOfRect(int edge1, int edge2) {
  return edge1 * edge2;
}

還有型別不同的問題,比如:

float areaOfRect(float size) {
  return size * size;
}

不過呢~我們可以發現共通點是 —— 該函式的名稱都是一樣的:areaOfRect

然而,實作對應的參數數目與型別組合可以不ㄧ樣,這也造成一個現象:只要使用 areaOfRect 的函式,填入之參數與型別有被對應到定義過的其中一種案例areaOfRect 可以被正常執行。

// 符合第一種: int areaOfRect(int size)
areaOfRect(5);

// 符合第二種: int areaOfRect(int edge1, int edge2)
areaOfRect(5, 10);

// 符合第三種: float areaOfRect(float size)
areaOfRect(2.5);

以上擴充函式可以被執行的形式,又被稱之為函式超載(Function Overload)。(這時候背景出現:“Ta! Da!” 以及亂七八糟的彩帶飛來飛去)

讀者云:“作者一定是在開玩笑,這種東西 JS 根本不可能動作,騙我做什麼!以下這種程式碼怎麼可能動作!”

https://ithelp.ithome.com.tw/upload/images/20190917/20120614CMmxKN8JuX.png

對!上述的狀況在 TS 裡根本不會動作,遑論在原生的 JS 執行

但是筆者的含義不在這~ (讀者真要硬生生試一下上面的程式碼也是可行,但就是會出錯~ BJ4)

筆者的意思是說:“TypeScript 的介面裡,函式的型別可以被超渡...”(被鐵臉盆打到)

『 絕・對・不・是・超・渡! 』

是介面裡屬性對應的函式型別可以被超載!(希望筆者不要這系列還沒寫完就被讀者超渡,呸!呸!呸!)

以下的範例程式碼在 TS 的檢測結果如圖一~

https://ithelp.ithome.com.tw/upload/images/20190917/201206143asAVcJzgK.png

https://ithelp.ithome.com.tw/upload/images/20190917/201206145z1SX4njfc.png
圖一:這竟然在 TypeScript 是可以被接受的行為,筆者第一次看到時,受驚了一下

有些人可能覺得:“等等!這情況也不太對啊,這應該是:重複的屬性名稱會蓋掉前一個型別的宣告;也就是說,在這個案例裡,只要有任何物件實踐該介面時,應該只要實踐出 addition(p1: string, p2: string): number 這個功能就好了,因為前一個宣告的函式型別被蓋掉了啊!”

哦~是嗎?(一副 P 孩樣)

我們來檢測看看。(被 TypeScript 檢測結果如圖二,錯誤訊息如圖三)

https://ithelp.ithome.com.tw/upload/images/20190917/20120614MCIfzBDflm.png

https://ithelp.ithome.com.tw/upload/images/20190917/20120614RMLJYHjHTn.png
圖二:剛剛我們猜測的 “覆蓋” 性理論是錯誤的

https://ithelp.ithome.com.tw/upload/images/20190917/20120614e3NUnM6ruX.png
圖三:哇塞,這錯誤訊息有夠長

這錯誤訊息挺長的,我們節選重點部分:

https://ithelp.ithome.com.tw/upload/images/20190917/20120614U7LwODVhiy.png

TypeScript 直接用 overload 這個單字,很明確地說:(string, string) => number{ (number, number): number; (string, string): number } 兩個完全是不一樣的。

我們肯定在想:“恩... 若要實踐出函式超載的話,但也沒辦法直接用以下的方式去進行超載啊!”

https://ithelp.ithome.com.tw/upload/images/20190917/20120614UUuOtqYFpS.png

是的,因為在狹義物件的世界裡(普通 JSON 格式的物件),重複名稱的屬性會蓋掉前一個屬性

於是又是複合型別的 union 出場了。

使用複合型別主要是因為 —— 沒辦法直接對函式超載,但又得同時符合參數可以填入不同形式的狀況,因此也只能採取這樣的路徑。

另外,通常被 union 複合過後的型別,會出現至少兩種以上的型別推論可能性,因此必須進行型別限縮 —— 也就是 Type Guard,又名型別檢測 —— 由判斷敘述式進行型別限縮喔~

https://ithelp.ithome.com.tw/upload/images/20190917/20120614S3WILJZlxH.png

好了,但是這段程式碼還是會錯誤!

“恩!?怎麼會錯?”

我們來看看 TypeScript 的檢測結果(圖四)跟錯誤訊息(圖五)。

https://ithelp.ithome.com.tw/upload/images/20190917/20120614AhaYILQm8r.png
圖四:TypeScript 依然認為會錯

https://ithelp.ithome.com.tw/upload/images/20190917/20120614HmJWTkiI5h.png
圖五:這錯誤訊息真讓人搞不懂在做啥耶

別急!我們看一下錯誤訊息的這一句話:

Type 'number | undefined' is not assignable to type 'number'.
Type 'undefined' is not assignable to type 'number'.

有些讀者應該猜得出來 —— undefined 會出現在錯誤訊息裡的原因。

筆者就來解釋一下:在 if...else... 判斷敘述後,儘管讓參數 p1p2 可以為 numberstring,但是參數的組合也有可能是 p1p2 分別為 p1: number; p2: stringp1: string; p2: number —— 後兩種組合狀況會導致直接跳脫函式,並回傳 undefined,但這不符合函式型別輸出的規定狀況。(除非輸出改成 number | void

這裡就要考驗讀者對於這類型的案例的處理想法了

回想一下,如果出現這種狀況,我們希望有一種東西可以幫助我們進行例外排除,而這裡的例外就是 —— 使用者亂填 p1p2 導致它們同時不為 numberstring 的狀況。

如果讀者還記得 Day 10. 這一個重點:

《Day 10. 前線維護・特殊型別 X 永無止盡》之 重點 1. never 型別的概念
never 型別的概念是程序在函式或方法執行時:

  1. 無法跳脫出該函式或方法
  2. 出現例外結果中斷執行

以及:

never is a subtype of and assignable to every type.

也就是說,任何一種型別本來就存在例外處理的狀況導致程式中斷。所以輸出型別為 number 也可以看成輸出型別為 number | never —— 因為 number 等效於 number | never

根據以前學過的觀念,直接對剛剛的程式碼進行修改,改成這樣:

https://ithelp.ithome.com.tw/upload/images/20190917/20120614iJy9EDu3qF.png

拋出例外這件事情本身在 TypeScript 函式的輸出是合理的!

這裡筆者再強調一次:根據 AddOperation 介面的設計,不管回傳型別為 number 亦或者是 number | never —— 由於 never 跟任何型別進行 union 都會被其他型別吃掉,因此在這裡 number | never 被推論過後又會變回跟 number 沒兩樣。因此,TypeScript 檢測過後結果是正確的!(如圖六)

https://ithelp.ithome.com.tw/upload/images/20190917/20120614s7TOlbuHrC.png
圖六:恩~我們實踐出一個 TypeScript 認定很安全的函式

相信讀者看到這裡應該更能體會到 TypeScript 之所以要建構 never 型別的意義了。如果讀者覺得 never 的概念很模糊的話,剛剛的 AddOperation 就是一個展示,也是 never 型別必存在的關鍵 —— 不管你回傳的型別是什麼,依然可以隨時進行例外處理 —— never 就藏身在各種型別當中。

never 也隱藏在你心裡。

重點 1. 介面的函式(或方法)超載

介面定義的屬性對應的函式可以進行超載的動作

被超載的函式名稱必須相同。(型別格式也可以相同,但沒有意義,就很像多出來冗贅的程式碼)

單純函式形式的介面也可以進行函式超載,差別在沒有名稱標記而已。

若某物件實踐該介面時,必須符合該介面裡 —— 超載過的函式之所有情形

讀者試試看

有興趣的讀者可以去玩玩看一些特別(又讓人感到莫名其妙)的案例,畢竟筆者不可能把全部的案例都講完:

https://ithelp.ithome.com.tw/upload/images/20190917/20120614Z721IEhW8C.png

筆者順便說明:其實連型別化名(Type Alias)運用 type 宣告出來的函式也可以進行超載。然而,為何放在介面講解的原因是希望不要把靜態意義的型別系統採用超出靜態行為的技巧,型別的宣告會變得很不合理又很變態。(有雙關喔~)

讀者好奇的話,倒是可以自行驗證看看。

介面延展的另類形式 - 介面融合 Interface Merging

讀者可能以為介面這種東西只能用 extends 來延展。

不過,這裡要介紹 —— 還有另一種方式可以延展介面,稱之為介面融合

對於跟第三方插件或框架協作時,這一節會是很關鍵的概念 —— 請讀者務必好好把這裡的內容學會,就算你看不懂函式超載,也一定要會下面介面融合的技巧;它不會很難,但會對未來使用 TypeScript 作為專案主要語言時非常有用,想要不被其他的第三方套件的型別系統雷到,介面融合的技巧會是救命解藥。(不過也要等筆者寫到很後面很後面,可能第 30 天以後,寫專案時才會實踐到)

貼心小提示

儘管這裡是說 Interface Merging,但是官方的名稱為 Declaration Merging,而介面就是屬於可以進行 Declaration Merging 的其中一種格式

至於有關於 Declaration Merging 部分,筆者考慮過後,本系列文應該不會講太深,也有可能不會講到。但是,如果想要開發 TypeScript 版本的第三方套件,就必須對這領域要更深一點的著墨

有興趣的話可以點這裡看官方 TypeScript 的 Declaration Merging

介面融合的限制也是有的:

重點 2. 介面融合 Interface Merging

若某介面 I重複定義多次,則該介面到最後的推論結果會是所有重複定義的介面的交集

其中,所有重複定義的過程必須遵守這個特性:I 若被重複定義時,裡面若干屬性跟過去所定義的某屬性相符的話,該屬性的型別必須跟過往定義出的介面裡的屬性之型別吻合

筆者今天就順便結合學到的函式超載技巧,舉一個實用的例子。

通常在網頁的 DOM 裡,要創建 HTML 元素與操作它們 —— 譬如要建立一個超連結元素並插入到其他的元素裡:

const $app = document.getElementById('app');
const $a = document.createElement('a');

$a.setAttribute('href', '<url>');
$a.style.color = 'red';
$app.appendChild($a);

其中 createElement 這個方法為例:每一次建立 <a> 標籤,JS 相對會建構出一個 HTMLAnchorElement 物件。如果建立的是 <p> 標籤,相對就會建構 HTMLParagraphElement 物件;如果是 <input> 標籤,則是 HTMLInputElement 等等 ...。

哇,那讀者試著想一下:createElement 這個介面會長成什麼樣子?

可能會有人想出這樣的版本:

https://ithelp.ithome.com.tw/upload/images/20190917/20120614iV5KG1B4X9.png

但是呢,有些敏銳的讀者會發覺不對勁,因為 name 參數在 createElement 這個函式裡面的限制:太過寬鬆

什麼意思?

比如筆者可以刻意實踐出:輸入 'a' 結果創建出 HTMLParagraphElement 而非 HTMLAnchorElement ,但在該介面裡的定義仍然是通過的,因為是用 union 的關係。(讀者撐一下~複合型別的詳細解析會在 Day 17. 出現~)

因此筆者提出一個改善的版本:運用字串明文型別(String Literal Type)與函式超載的技巧。(其實這是官方 Doc 的範例 XD,因為太有趣所以放在這邊做說明)

https://ithelp.ithome.com.tw/upload/images/20190917/20120614xBkwhMwDhK.png

這感覺正常多了。不過還有一個潛在問題:HTML 的元素實在是太多種類(超過 100 多種),要是所有的函式超載狀況與實踐該介面時的內容細節塞在一起 —— 豈不變成很龐大的檔案

這時候,可以對介面進行拆解的動作。但要注意的是:該介面重複定義狀態下,介面的名稱必須重複

https://ithelp.ithome.com.tw/upload/images/20190917/20120614zMuqeLR0UG.png

以上的程式碼,儘管介面被剖成兩半,但是經過介面融合(Interface Merging),你可以把它們想成一體!

介面融合的應用情境

介面融合之所以很重要的原因 —— 筆者這裡就舉一個應用情境。

如果專案跟專案(或第三方套件)之間必須合作 —— 此時兩個專案產生了依賴性(Dependency),勢必會在型別上有衝突產生之可能,尤其如果有使用 Middleware 這種東西,簡直就是破壞型別的平衡

『 什麼是中間層 Middleware!? 』 有些入門的初心者問。

假設某一個很陽春的框架,專門是在處理後端 Request 與 Response。

另外再假設:從 Client 端送出的 Request 部分會被該框架解析成以下格式。

https://ithelp.ithome.com.tw/upload/images/20190917/20120614vF3GL8q4Db.png

裡面的 StupidRequest 的格式設計裡,沒有針對 url 解析出路徑的查詢字串(也就是我們常聽到的 Query String);另外,StupidRequest 介面裡面也沒有宣告含有 query 這個屬性。

通常要從網址解析 Query String —— 例如某網址:

http://domain.com/index?hello=world&visitor=maxwell

後面這一段:

?hello=world&visitor=maxwell

可以被解析成:

{
  "query": {
    "hello": "world",
    "visitor": "maxwell"
  }
}

回過頭來,正想說到底要怎麼擴充 StupidRequest 的功能,在本案例就是從 StupidRequesturl 額外解析出 query 這個 Query String 的 JSON 格式 —— 通常框架的作法是引入中間層(Middleware)。

中間層的概念很簡單:假設某陽春框架接收 Client 端的 Request 時,開發者原本會對該 Request 做某些事情後再回傳 Response;但是在開發者對 Request 做某些事情前,該 Request 會先經過所謂的中間層,然後再把結果傳到開發者寫的程式碼。因此,原本沒有中間層的流程應該是:

  • Client 端發出 Request
  • Server 端接收 Request
  • 開發者針對不同的 Request 進行動作,比如撰寫 Response、RESTful API 溝通、CRUD、提供 Client 端瀏覽器的靜態資料等等眾多行為
  • Server 端發出 Response
  • Client 接收 Response

但如果引入了中間層,程序則會變成:

  • Client 端發出 Request
  • Server 端接收 Request
  • 經過一系列中間層處理 Request:比如以目前的例子,根據 url 進行解析 Query String 的動作,並對 StupidRequest 型別的物件新增 query 這個屬性
  • 開發者針對不同的 Request 進行動作
  • Server 端發出 Response
  • Client 接收 Response

假設真的引入了中間層 —— 專門將 StupidRequest 裡的 url 進行 Query 字串的解析。原本 StupidRequest 的格式是:

https://ithelp.ithome.com.tw/upload/images/20190917/20120614vF3GL8q4Db.png

被中間層搞到最後新增了一個 query 屬性對應一個 JSON 物件

https://ithelp.ithome.com.tw/upload/images/20190917/20120614RiInTdfGAf.png

讀者一定想說:“我們不可能在第三方套件或框架早已經定義的介面裡進行介面擴充的動作啊!那已經被寫死在該框架裡了,要是再被改,這個框架相依賴的其他套件不就也有壞掉的可能嗎?”

對!雖然筆者後續也會講到 TypeScript namespaces 來協助解決這個問題,但在講解這東西之前,如果想要確保 TypeScript 會確認 query 屬性在 StupidRequest 是存在的話 —— 我們不太可能直接在第三方套件裡直接硬加 query 屬性(搞不好其他相依賴套件也會自己用到?),因此最佳的解法是:

https://ithelp.ithome.com.tw/upload/images/20190917/20120614k1yx87o8Dr.png

藉由介面融合的方式,和第三方套件或框架協作 —— 打造出屬於我們的 TS 專案適合的型別版本!

從上面的例子來看,筆者今天在這裡點出了一個 TypeScript 與第三方套件或框架協作的困難點:

只要有類似於中間層概念的程式,亦或者是經過層層包裝的功能,將會對 TypeScript 協作上產生困難點

最後再給讀者補充,通常第三方套件都會有屬於自己的 namespace 防止污染到全域的狀況。因此為了要讓介面也可以融合,你也必須指定該套件的 namespace。(這些細節就由本系列第三篇章《戰線擴張》再來補足這邊的知識吧!)

小結

開頭原本就說今天要讓筆者輕鬆寫,但寫到後頭根本越寫越累XD,因為東西實在太多。

不過今天也點到了重要的點:理解 TypeScript 的介面不只單純用來簡化程式碼以及抽象化,這裡也銜接到 —— 未來如果要第三方套件協作可能遇到的問題與解決層面的方法。

至於 TypeScript Interface 介面的功能:筆者還沒講完~ 所以我們要繼續看下去啊啊啊~~~

]]> Maxwell Alexius 2019-09-24 16:16:02
Day 13. 機動藍圖・介面的延展 X 功能與意義 - Interface Extension & Significance https://ithelp.ithome.com.tw/articles/10215586?sc=rss.iron https://ithelp.ithome.com.tw/articles/10215586?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

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

  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 到今天學到的東西~

]]> Maxwell Alexius 2019-09-23 16:14:44
Day 12. 機動藍圖・介面宣告 X 使用介面 - TypeScript Interface Intro. https://ithelp.ithome.com.tw/articles/10215584?sc=rss.iron https://ithelp.ithome.com.tw/articles/10215584?sc=rss.iron https:...]]></description>
                                    <content:encoded><![CDATA[<p><img src=

《機動藍圖》篇章概要

本系列第二部分:《機動藍圖》(The Agile Blueprint)篇章涵括的範圍就是 TypeScript 的重頭戲。不外乎,筆者想像中的內容大致上有這些(其實有些是筆者臨時打稿時想到可以講的部分XD),內容順序有可能會跳,不過還是給讀者稍微看過:

  • TypeScript Interface 介面
    • 宣告 Interface Declaration
    • Interface V.S. Type System
    • 複合型別 union & intersection
  • TypeScript Class 類別
    • Class 基礎 OOP (讀者若有 Java、C# 等完整實踐 OOP 語言的背景,這些東西對你們來說小事一碟~,不過對 JS 圈裡的初心者或者剛接觸 OOP 的人會有一些挑戰性,但理解過後還蠻好用的)
      • 建構子 Constructor
      • 類別屬性與方法 Member Variables(Properties) & Methods
      • 存取權限修飾子 Access Modifiers
      • 存取值方法 Accessors
    • Class 進階概念(初心者認為有些難的地方呢)
      • 繼承 Inheritance
      • 靜態屬性與方法 Static
      • 私有建構子與單例模式 Private Constructor & Singleton Pattern
      • 抽象類別 Abstract Class
    • Class + Inheritance v.s. Class + TS Interface
    • TS Class V.S. ES6 Class
  • 破壞 JS 圈(並非其他語言圈,就僅限 JS 圈!)裡對於這句話的重大誤解 (<-- 本系列文章重頭戲之一在這!)

    “Favor object composition over class inheritance.”

(讀者請不要誤會,筆者不是說這句話錯,這句話很對!但是被誤解到錯得很離譜,甚至還出現各種 Antipattern 讓筆者覺得不講也不行。)

原本還打算把 Generics 通用型別塞進本篇章,但回過頭來,除了感覺又會過大外,筆者認為在本章節系列內應該要好好把 TypeScript Interface 與 Class 部分講完,進度太快也會吃不消。

因此我們來進行本篇章第一篇,正文開始

介面的宣告與使用 TypeScript Interface

環境建置

在《前線篇章》系列,typescript-tutorial/01-basic 資料夾裡的 index.ts 內的程式碼有點過多。因此我們在新建另一個環境。開啟終端機進到 typescript-tutorial 裡面,並且建置新的資料夾(命名不限):

$ cd PATH_TO/typescript-tutorial
$ mkdir 02-interface-class

讀者應該熟悉了 TypeScript 的指令,一樣初始化 TS 編譯器設定檔:

$ tsc --init

先打開編譯器設定檔 —— 也就是 tsconfig.json,把 strictNullChecks 這個選項設定為 true(如圖二),筆者忘記在《前線維護》篇章寫到這句話,這是一個說小並不小,說大影響也不太大 —— 但依然會影響到後續文章的寫法。使用 VSCode 開啟 typescript-tutorial 這個資料夾你應該會看到類似這個畫面。(如圖一)

https://ithelp.ithome.com.tw/upload/images/20190916/20120614dKn3HV5xkB.png
圖一:新建 02-interface-class 的資料夾

https://ithelp.ithome.com.tw/upload/images/20190916/20120614UvSHswmcXL.png
圖二:將 tsconfig.json 裡的 strictNullChecks 改成 true

如果都已經建置完畢後,新增 index.ts 檔案在 typescript-tutorial/02-interface-class 的資料夾裡然後就開始囉~

宣告 Interface

讀者還記得型別化名(Type Alias)是什麼嗎? 如果還不記得的話,讀者請參見 Day 08. 的文章囉~不過應該還算簡單,筆者就二話不說把它的重點搬過來:

《前線維護・明文型別 X 格式為王》之 重點 2. 型別化名 Type Alias

若某型別 TT 可為任何的型別(包含原始型別、物件型別、TypeScript 內建型別、明文型別、複合型別、Generics 通用型別等)。其中我們想要讓該型別 T 等效於別名 A,則可以使用 TypeScript 的 type 關鍵字進行化名宣告:

type A = T;

型別化名的主要目的為簡化程式碼以及進行型別的抽象化(Type Abstraction)

型別化名的意義就是把複雜格式(尤其是明文格式)的型別進行程式碼簡化抽象化 —— 抽象化的概念一再地出現,非常重要呢!

而 TypeScript 的介面(Interface)與型別化名的作用有些相似,但可以把它想成更具彈性的型別

不過呢,儘管型別化名是可以用原始型別與各種廣義物件格式表示,譬如:

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

貼心小提示

讀者可能覺得最後一個案例 —— 可以直接把字串的值當成一種型別並且 union 起來 —— 這也是明文型別(Literal Type)的一種形式。

通常看到的明文型別是物件的格式,但實際上單純原始型別的或廣義物件的都可以獨立成一個型別,這部分之前沒講到,不過筆者認為知道有這個功能就好。

然而,介面(Interface)跟型別化名可就不同了。

它只能以兩種形式以及混合的方式(Hybrid)呈現。筆者刻意把混合部分隔離出來,因為最後一種方式只是把前兩種形式進行混合罷了。

重點 1. TypeScript 介面(Interface)的定義與種類

TypeScript Interface 可以藉由關鍵字 interface 宣告出來,介面裡面的詳細定義可為:

  • 物件格式:即 JSON 格式,是為屬性對型別,不是對值
  • 單一函式格式:沒有任何屬性,就是函式而已,但不一定需要標上函式名稱
  • 混合格式:即『物件格式』與『單一函式格式』混合在一起

然而,在物件格式的介面定義下 —— 剛剛筆者在貼心小提示所講的 —— 如果你真的把值(比如字串當型別)寫下去,也是可以的!只是以後你註記某變數時使用了該介面,該變數必須強制把該屬性對應的值原封不動複製上去

而單一函式格式與混合格式會在後續進行補充。

但我們先看看前兩種版本的介面定義的形式,參見以下的範例。

https://ithelp.ithome.com.tw/upload/images/20190916/201206143QfyC3Ki1P.png

可以看出 UserInfo 把各種屬性對應的型別都描述出來了,很像在編列資料格式的型態。(筆者知道這是廢話XD)

然而,UpdateRecord 描述的單純就只有一個函式,它必須傳入的參數 —— 各自符合 number 型別以及 UserInfo 介面的參數進去。而該函式的輸出狀態為不輸出(也就是 void)。從這裡就可以看出,我們可以藉由 UpdateRecord 的介面定義一系列的修改 UserInfo 的函式,比如說更改名稱、更改性別、更改興趣等等。

筆者用一些例子展示給讀者看,註記介面的各種狀況。(以下程式碼檢測結果如圖三,錯誤訊息分別為圖四~圖六)

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

https://ithelp.ithome.com.tw/upload/images/20190916/20120614A2LckUsdqP.png
圖三:跟 type 作的型別化名概念很像,多一鍵、少一鍵與屬性對應型別錯誤都會錯

https://ithelp.ithome.com.tw/upload/images/20190916/201206143qujeQ3zB7.png
圖四:少一鍵 age 就被 TS 警告

https://ithelp.ithome.com.tw/upload/images/20190916/20120614wmS5MpkO4a.png
圖五:job 沒有存在在 Person 這個介面裡,因此多一鍵也會被 TS 警告

https://ithelp.ithome.com.tw/upload/images/20190916/201206142nApgYYYLU.png
圖六:hasPet 屬性必須接收 boolean,型別錯誤會被警告,不過感覺被警告也是挺正常

我們還有另外一種案例要測,那就是將變數或 JSON 物件格式傳入函式作為參數的案例。還記得在 Day 08. 裡我們檢測到狹義物件的明文格式作為參數的狀況嗎?上一次我們得出的最後結論是 —— 只要我們出現廣義物件時,會建議進行註記的動作,不然沒有註記過後的變數,帶入函式的參數,被檢測的限制會變輕

以下就是要測測看 Interface 有沒有類似的狀況。(TypeScript 檢測結果如圖七;錯誤訊息如圖八)

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

https://ithelp.ithome.com.tw/upload/images/20190916/20120614RDZoIcHV9l.png
圖七:結果如果直接將物件的明文形式作為參數代入會被警告;然而,沒有對變數作積極註記,但看起來跟 Person 很像,就算通過

https://ithelp.ithome.com.tw/upload/images/20190916/20120614LKhCDrCh7x.png
圖八:恩,TypeScript 確實認為這個物件的明文格式比 Person 介面所定義的屬性多了一些,因此被警告

如果讀者仔細比對本篇用 interface 的結果與使用 type 的結果,幾乎完全一樣。

重點 2. 為註記之變數作為函式參數的行為

由於可以藉由存取物件的表現形式在某變數裡 —— 其中該變數沒有被積極註記 —— 只要該變數至少有符合介面的格式,依然可以通過函式參數對於變數的值的驗證。

但若想避免此狀況發生,任何變數需要存取物件時,必須進行積極註記型別或介面的動作

小結

讀者可能會非常納悶(連筆者剛開始學也是一樣)。

typeinterface 到底是差在哪裡啊?感覺都沒差啊!”

偷偷說一下,其實用法上感覺沒差多少,但意義上的差別可就超級大了啊

你可以同時使用 typeinterface,不過這樣交錯下來可能會導致剛入門 TypeScript 的人會有好像都可以亂用都沒差的錯覺。

因此筆者會把這個問題的探討慢慢呈現出來,今天先把 interface 這東西給讀者瀏覽過一次所有狀況。儘管本篇就算只有兩個大重點,但後面的探討只會越來越深入,開頭篇章先把基礎暖身做好再說~

]]> Maxwell Alexius 2019-09-22 18:14:56

paypal.me/twcctz50
http://blog.sina.com.tw/window/feed.php?ver=rss&type=entry&blog_id=48997
https://paypal.me/twcctz50?country.x=TW&locale.x=zh_TW
使用 PayPal.Me 連結付款給我: https://paypal.me/twcctz50?country.x=TW&locale.x=zh_TW 
健康是最好的禮物蛋黃油https://www.facebook.com/eggsoil  
在生寶妹之前,寶妹媽,就食用蛋黃油調養車禍後造成的心律不整等後遺症狀將近兩年,配合復健、整復治療和游泳運動等,逐漸恢復正常的心律。
直到懷寶妹後至今,感謝蛋黃油讓寶妹媽能恢復健康,給這意外來的祝福一個健康的生長環境。寶妹雖是家中最小的孩子,但也是最健康、幸福的孩子!惟有她是媽媽有豐富的母乳可供親餵。
至今,恭喜寶妹已經滿兩歲了!每天還是喜歡找媽媽喝餒餒睡覺,出生至今,身體健康,沒有感冒和生病的紀錄。祝福寶妹,能一直健康、快樂的成長、學習,成為眾人的祝福!
代工生產製造需一千斤
需用者請提早預約安排
每天補充蛋黃油可降低罹癌風險?!是的!!
昨晚,十多年老案主的弟弟周大哥說,二哥鼻咽癌治療恢復後的後遺症,又發作了!需配合醫師開的抗生素和補充細胞再生的營養素,又訂了10瓶50ml,補充瓶。
據了解個案案主,罹癌前的工作是從事印刷業。坦白說,有點醫學常識的人多半知道化學油墨對身體的傷害為何?
本科所學為設計的我,也曾在相關產業待過十幾年的時間,等到研究所和醫院合作完成論文後,才知道自己的工作:設計和教職,其實,都隱含著很高的罹癌風險!
我們都曾因此賠上過健康,但慶幸自己和案主們,都是有福之人!
感謝蛋黃油豐富的卵磷脂營養成份,除了維生素C之外,幾乎涵蓋了所有!在103學年間,我曾因超鐘點一週上課時數33小時,忙碌於家庭和工作間,哪時老二剛出生半年,做好月子後,馬上就恢復忙碌的教職工作,不出一個月,就為卵巢炎、併發盆腔炎,抗生素吃了半年,還是不見恢復的病痛所苦!
期間,幾乎每兩週跑醫院兩三趟,署基的婦科主任醫師,也建議我要跟校長請辭,調養身體為重!感謝校長體諒,讓我減課到16堂,撐到合約到期,沒有違約金的困擾,離職後,就回家照顧老二和調養自己的身體。
卵巢炎超痛的!併發盆腔炎更痛!從早痛到晚!醫師說,抗生素吃半年了,就不能再吃了!感謝醫師沒讓我繼續吃下去!
而是勸我調整工作、生活和飲食!於是,我又開始大量食用蛋黃油和自己料理三餐,就這樣子,吃了半年,某天,突然驚覺:卵巢不痛了耶!
感謝神!在恢復前的每一時刻,我被疼痛纏身,影響情緒和睡眠,每天都不知要多久,才會好?!只是一直做該做的事,好好吃飯、休息,調養身心!直到恢復時,才發覺:哇!幸好,半年就好了!
這是蛋黃油在我近十年的健康危機中,第二次救了我!感謝神創造各樣美好食物,保守、祝福我們能有機會恢復祂起初創造我們的美好!
感謝近十幾年來,因著每一次的健康危機,即時食用蛋黃油排毒、調養身體、恢復正常的心律,讓我能在身體得滋養後再懷孕生下健康的孩子們,也讓我們的生命得到延續。
為此,我才投入後來的時間、金錢、精神在服務和我一樣有需要的案主們身上。感謝大家一起陪我攜手走過已過的十幾年。祝福每個人都能有機會認知到維護健康的身體,其奧秘就在於養成正確的日常飲食習慣!
筆者:蛋黃油男
電話:02-24978169
手機:0989-422508
為資深個案自主健康管理設計師,
目前服務於品蔚養生設計事務所。
https://liker.social/invite/pRwYGraC
https://www.facebook.com/eggsoil
https://www.facebook.com/nectw721
https://www.instagram.com/0989422508mdc
https://www.pinterest.com/twcctz500
https://www.linkedin.com/in/twcctz500
https://www.twitter.com/twcctz500
https://currents.google.com/117454133619976175486
https://www.youtube.com/shorts/o8Jaud7px4w
https://www.youtube.com/channel/UC5E4_uNgGl_omnUVfL7fUyA
https://fitness-center-727.business.site/
https://g.page/r/CUt-sGCWmpP2EBM/review
https://matters.news/@twcctz500
https://pastebin.com/NgugYMZP   來自 @pastebin 
https://hardbin.com/ipfs/QmSmgLoGrr4dcJ4EFmszDXJAGbXW2oZU5nNgksdjG7bb3P/
https://zerobin.net/?b4e43b1d09b3dcbe#C4rH4SwtqM4v4BsehLtwVf2DSEhto1JRx8PzKIsmrYo=
https://zerobin.net/?4fdb0ea46d78f4a6#z7E38he/vzywjsvzxBwTBm5dvCaKc5Pei8nDkUflgOs=
https://privatebin.net/?8abe23c5b0253b66#7Vq3VyzThWp2ga7tvVBQ4om6aiYdqvma4bpfWYNKMCgG
https://medium.com/@twcctz50
https://about.me/twcctz500
paypal.me/twcctz50

ASP.NET 伺服器控制項開發 :: 2008 iT 邦幫忙鐵人賽 https://ithelp.ithome.com.tw/users/20007956/ironman zh-TW Mon, 06 Jun 2022 20:19:06 +0800 [ASP.NET 控制項實作 Day29] 解決 DropDownList 成員 Value 值相同產生的問題(續) https://ithelp.ithome.com.tw/articles/10013458?sc=rss.iron https://ithelp.ithome.com.tw/articles/10013458?sc=rss.iron 接續上一文
接下來還要覆寫 LoadPostData 方法,取得 __EVENTARGUMENT 這個 HiddenField 的值,並判斷與原 SelectedIndex 屬性值是...]]>
接續上一文
接下來還要覆寫 LoadPostData 方法,取得 __EVENTARGUMENT 這個 HiddenField 的值,並判斷與原 SelectedIndex 屬性值是否不同,不同的話傳回 True,使其產生 SelectedIndexChanged 事件。

        Protected Overrides Function LoadPostData(ByVal postDataKey As String, ByVal postCollection As NameValueCollection) As Boolean
            Dim values As String()
            Dim iSelectedIndex As Integer

            Me.EnsureDataBound()
            values = postCollection.GetValues(postDataKey)

            If (Not values Is Nothing) Then
                iSelectedIndex = CInt(Me.Page.Request.Form("__EVENTARGUMENT"))
                If (Me.SelectedIndex <> iSelectedIndex) Then
                    MyBase.SetPostDataSelection(iSelectedIndex)
                    Return True
                End If
            End If
            Return False
        End Function

四、測試程式
在 TBDropDownList 的 SelectedIndexChanged 事件撰寫如下測試程式碼。

    Protected Sub DropDownList2_SelectedIndexChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles DropDownList2.SelectedIndexChanged
        Dim sText As String

        sText = String.Format("TBDropDownList: Index={0} Value={1}", DropDownList2.SelectedIndex, DropDownList2.SelectedValue)
        Me.Response.Write(sText)
    End Sub

執行程式,在 TBDropDownList 選取 "王五" 這個選項時,會正常顯示該成員的 SelectedIndex 及 SelectedValue 屬性值。

接下選取 Value 值相同的 "陳六" 這個選項,也會正常引發 SelectedIndexChanged ,並顯示該成員的 SelectedIndex 及 SelectedValue 屬性值。

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/30/5830.aspx

]]>
jeff377 2008-10-30 21:23:12
[ASP.NET 控制項實作 Day29] 解決 DropDownList 成員 Value 值相同產生的問題 https://ithelp.ithome.com.tw/articles/10013457?sc=rss.iron https://ithelp.ithome.com.tw/articles/10013457?sc=rss.iron DropDownList 控制頁的成員清單中,若有 ListItem 的 Value 值是相同的情形時,會造成 DropDownList 無法取得正確的 SelectedIndex 屬性值、且無...]]> DropDownList 控制頁的成員清單中,若有 ListItem 的 Value 值是相同的情形時,會造成 DropDownList 無法取得正確的 SelectedIndex 屬性值、且無法正確引發 SelectedIndexChanged 事件的問題;今天剛好在網路上看到有人在詢問此問題,所以本文將說明這個問題的源由,並修改 DropDownList 控制項來解決這個問題。
程式碼下載:ASP.NET Server Control - Day29.rar

一、DropDownList 的成員 Value 值相同產生的問題
我們先寫個測試程式來描述問題,在頁面上放置一個 DropDownList 控制項,設定 AutoPostBack=True,並加入四個 ListItem,其中 "王五" 及 "陳六" 二個 ListItem 的 Value 值相同。

    <asp:DropDownList ID="DropDownList1" runat="server" AutoPostBack="True">
            <asp:ListItem Value="0">張三</asp:ListItem>
            <asp:ListItem Value="1">李四</asp:ListItem>
            <asp:ListItem Value="2">王五</asp:ListItem>
            <asp:ListItem Value="2">陳六</asp:ListItem>
    </asp:DropDownList>

在 DropDownList 的 SelectedIndexChanged 事件,輸出 DropDownList 的 SelectedIndex 及 SelectedValue 屬性值。

    Protected Sub DropDownList1_SelectedIndexChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles DropDownList1.SelectedIndexChanged
        Dim sText As String

        sText = String.Format("DropDownList: Index={0} Value={1}", DropDownList1.SelectedIndex, DropDownList1.SelectedValue)
        Me.Response.Write(sText)
    End Sub

執行程式,在 DropDownList 選取 "李四" 這個選項時,會正常顯示該成員的 SelectedIndex 及 SelectedValue 屬性值。

接下來選取 "陳六" 這個選項時,竟然發生奇怪的現象,DorpDownList 竟然顯示相同 Value 值的 "王五" 這個成員的 SelectedIndex 及 SelectedValue 屬性值。

二、問題發生的原因
我們先看一下 DropDownList 輸出到用戶端的 HTML 原始碼。

<select name="DropDownList1" onchange="javascript:setTimeout('__doPostBack(\'DropDownList1\',\'\')', 0)" id="DropDownList1">
	<option selected="selected" value="0">張三</option>
	<option value="1">李四</option>
	<option value="2">王五</option>
	<option value="2">陳六</option>
</select>

DropDownList 是呼叫 __doPostBack 函式,只傳入 eventTarget參數 (對應到 __EVENTTARGET 這個 HiddenField) 為 DropDownList 的 ClientID;當 PostBack 回伺服端時,在 DropDownList 的 LoadPostData 方法中,會取得用戶端選取的 SelectedValue 值,並去尋找對應的成員的 SelectedIndex 值。可是問題來了,因為 "王五" 與 "陳六" 的 Value 是相同的值,當在尋找符合 Value 值的成員時,前面的選項 "王五" 會先符合條件而傳回該 Index 值,所以先造成取得錯誤的 SelectedIndex 。

Protected Overridable Function LoadPostData(ByVal postDataKey As String, ByVal postCollection As NameValueCollection) As Boolean
    Dim values As String() = postCollection.GetValues(postDataKey)
    Me.EnsureDataBound
    If (Not values Is Nothing) Then
        MyBase.ValidateEvent(postDataKey, values(0))
        Dim selectedIndex As Integer = Me.Items.FindByValueInternal(values(0), False)
        If (Me.SelectedIndex <> selectedIndex) Then
            MyBase.SetPostDataSelection(selectedIndex)
            Return True
        End If
    End If
    Return False
End Function

三、修改 DropDownList 控制項來解決問題
要解決這個問題最好的方式就是直接修改 DropDownList 控制項,自行處理前端呼叫 __doPostBack 的動作,將用戶端 DropDownList 選擇 SelectedIndex 一併傳回伺服端。所以我們繼承 DropDownList 命名為 TBDropDownList,覆寫 AddAttributesToRender 來自行輸出 PostBack 的用戶端指令碼,我們會用一個變數記錄 AutoPostBack 屬性,並強制將 AutoPostBack 屬性值設為 False,這是為了不要 MyBase 產生 PostBack 的指令碼;然後再自行輸出 AutoPostBack 用戶端指令碼,其中 __doPostBack 的 eventArgument 參數 (對應到 __EVENTARGUMENT 這個 HiddenField) 傳入 this.selectedIndex。

        Protected Overrides Sub AddAttributesToRender(ByVal writer As HtmlTextWriter)
            Dim bAutoPostBack As Boolean
            Dim sScript As String

            '記錄 AutoPostBack 值,並將 AutoPostBack 設為 False,不要讓 MyBase 產生 PostBack 的指令碼
            bAutoPostBack = Me.AutoPostBack
            Me.AutoPostBack = False

            MyBase.AddAttributesToRender(writer)

            If bAutoPostBack Then
                MyBase.Attributes.Remove("onchange")
                sScript = String.Format("__doPostBack('{0}',{1})", Me.ClientID, "this.selectedIndex")
                writer.AddAttribute(HtmlTextWriterAttribute.Onchange, sScript)
                Me.AutoPostBack = True
            End If
        End Sub

在頁面上放置一個 TBDropDownList 控制項,設定與上述案例相同的成員清單。

        <bee:TBDropDownList ID="DropDownList2" runat="server" AutoPostBack="True">
            <asp:ListItem Value="0">張三</asp:ListItem>
            <asp:ListItem Value="1">李四</asp:ListItem>
            <asp:ListItem Value="2">王五</asp:ListItem>
            <asp:ListItem Value="2">陳六</asp:ListItem>
        </bee:TBDropDownList>

執行程式查看 TBDropDownList 控制項的 HTML 原始碼,呼叫 __doPostBack 函式的參數已經被修改,eventArgument 參數會傳入該控制項的 selectedIndex。

<select name="DropDownList2" id="DropDownList2" onchange="__doPostBack('DropDownList2',this.selectedIndex)">
	<option selected="selected" value="0">張三</option>
	<option value="1">李四</option>
	<option value="2">王五</option>
	<option value="2">陳六</option>
</select>

[超過字數限制,下一篇接續本文]

]]>
jeff377 2008-10-30 21:15:23
[ASP.NET 控制項實作 Day28] 圖形驗證碼控制項(續) https://ithelp.ithome.com.tw/articles/10013365?sc=rss.iron https://ithelp.ithome.com.tw/articles/10013365?sc=rss.iron 接續一上文
二、實作圖形驗證碼控制項
雖然我們可以使用 Image 控制項來呈現 ValidateCode.aspx 頁面產生的驗證碼圖...]]>
接續一上文
二、實作圖形驗證碼控制項
雖然我們可以使用 Image 控制項來呈現 ValidateCode.aspx 頁面產生的驗證碼圖形,可是這樣只處理一半的動作,因為沒有處理「使用者輸入的驗證碼」是否與「圖形驗證碼」相符,所以我們將實作一個圖形驗證碼控制項,來處理掉所有相關動作。
即然上面的示範使用 Image 控制項來呈現驗證碼,所以圖形驗證碼控制項就繼承 Image 命名為 TBValidateCode。

    < _
    Description("圖形驗證碼控制項"), _
    ToolboxData("<{0}:TBValidateCode runat=server></{0}:TBValidateCode>") _
    > _
    Public Class TBValidateCode
        Inherits System.Web.UI.WebControls.Image
    
    End

新增 ValidateCodeUrl 屬性,設定圖形驗證碼產生頁面的網址。

        ''' <summary>
        ''' 圖形驗證碼產生頁面網址。
        ''' </summary>
        < _
        Description("圖形驗證碼產生頁面網址"), _
        DefaultValue("") _
        > _
        Public Property ValidateCodeUrl() As String
            Get
                Return FValidateCodeUrl
            End Get
            Set(ByVal value As String)
                FValidateCodeUrl = value
            End Set
        End Property

覆寫 Render 方法,若未設定 ValidateCodeUrl 屬性,則預設為 ~/Page/ValidateCode.aspx 這個頁面。另外我們在圖形的 ondbclick 加上一段用戶端指令碼,其作用是讓用戶可以滑鼠二下來重新產生一個驗證碼圖形。

        Protected Overrides Sub Render(ByVal writer As System.Web.UI.HtmlTextWriter)
            Dim sUrl As String
            Dim sScript As String

            sUrl = Me.ValidateCodeUrl
            If String.IsNullOrEmpty(sUrl) Then
                sUrl = "~/Page/ValidateCode.aspx"
            End If
            If Me.BorderWidth = Unit.Empty Then
                Me.BorderWidth = Unit.Pixel(1)
            End If
            If Me.AlternateText = String.Empty Then
                Me.AlternateText = "圖形驗證碼"
            End If
            Me.ToolTip = "滑鼠點二下可重新產生驗證碼"
            Me.ImageUrl = sUrl
            If Not Me.DesignMode Then
                sScript = String.Format("this.src='{0}?flag='+Math.random();", Me.Page.ResolveClientUrl(sUrl))
                Me.Attributes("ondblclick") = sScript
            End If
            Me.Style(HtmlTextWriterStyle.Cursor) = "pointer"

            MyBase.Render(writer)
        End Sub

另外新增一個 ValidateCode 方法,用來檢查輸入驗證碼是否正確。還記得我們在產生驗證碼圖形時,同時把該驗證碼的值寫入 Session("_ValidateCode") 中吧,所以這個方法只是把用戶輸入的值與 Seesion 中的值做比對。

        ''' <summary>
        ''' 檢查輸入驗證碼是否正確。
        ''' </summary>
        ''' <param name="Code">輸入驗證碼。</param>
        ''' <returns>驗證成功傳回 True,反之傳回 False。</returns>
        Public Function ValidateCode(ByVal Code As String) As Boolean
            If Me.Page.Session(SessionKey) Is Nothing Then Return False
            If SameText(CCStr(Me.Page.Session(SessionKey)), Code) Then
                Return True
            Else
                Return False
            End If
        End Function

三、測試程式
在頁面放置一個 TBValidateCode 控制項,另外加一個文字框及按鈕,供使用者輸入驗證碼後按下「確定」鈕後到伺服端做輸入值比對的動作。

        <bee:TBValidateCode ID="TBValidateCode1" runat="server" />
        <bee:TBTextBox ID="txtCode" runat="server"></bee:TBTextBox>
        <bee:TBButton ID="TBButton1" runat="server" Text="確定" />

在「確定」鈕的 Click 事件中,我們使用 TBValidateCode 控制項的 ValidateCode 方法判斷驗證碼輸入的正確性。

    Protected Sub TBButton1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles TBButton1.Click
        If TBValidateCode1.ValidateCode(txtCode.Text) Then
            Me.Response.Write("驗證碼輸入正確")
        Else
            Me.Response.Write("驗證碼輸入錯誤!")
        End If
    End Sub

執行程式,頁面就會隨機產生一個驗證碼圖形。

輸入正確的值按「確定」鈕,就會顯示「驗證碼輸入正確」的訊息。因為我們在同一頁面測試的關係,你會發現 PostBack 後驗證碼圖形又會重新產生,一般正常的做法是驗證正確後就導向另一個頁面。

當我們輸入錯誤的值,就會顯示「驗證碼輸入錯誤!」的訊息。

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/29/5818.aspx

]]>
jeff377 2008-10-29 20:34:22
[ASP.NET 控制項實作 Day28] 圖形驗證碼控制項 https://ithelp.ithome.com.tw/articles/10013361?sc=rss.iron https://ithelp.ithome.com.tw/articles/10013361?sc=rss.iron 在網頁上常把圖形驗證碼應用在登入或貼文的頁面中,因為圖形驗證碼具有機器不易識別的特性,可以防止機器人程式惡意的存取網頁。在本文中將實作一個圖形驗證碼的伺服器控制項,透過簡單的屬性設定就可以輕易地...]]> 在網頁上常把圖形驗證碼應用在登入或貼文的頁面中,因為圖形驗證碼具有機器不易識別的特性,可以防止機器人程式惡意的存取網頁。在本文中將實作一個圖形驗證碼的伺服器控制項,透過簡單的屬性設定就可以輕易地在網頁上套用圖形驗證碼。
程式碼下載:ASP.NET Server Control - Day28.rar

一、產生圖形驗證碼
我們先準備一個產生圖形驗證碼的頁面 (ValidateCode.aspx),這個頁面主要是繪製驗證碼圖形,並將其寫入記憶體資料流,最後使用 Response.BinaryWrite 將圖形輸出傳遞到用戶端。當我們輸出此驗證碼圖形的同時,會使用 Session("_ValidateCode") 來記錄驗證碼的值,以便後續與使用者輸入驗證碼做比對之用。

Partial Class ValidateCode
    Inherits System.Web.UI.Page

    ''' <summary>
    ''' 產生圖形驗證碼。
    ''' </summary>
    Public Function CreateValidateCodeImage(ByRef Code As String, ByVal CodeLength As Integer, _
        ByVal Width As Integer, ByVal Height As Integer, ByVal FontSize As Integer) As Bitmap
        Dim sCode As String = String.Empty
        '顏色列表,用於驗證碼、噪線、噪點
        Dim oColors As Color() = { _
            Drawing.Color.Black, Drawing.Color.Red, Drawing.Color.Blue, Drawing.Color.Green, _
            Drawing.Color.Orange, Drawing.Color.Brown, Drawing.Color.Brown, Drawing.Color.DarkBlue}
        '字體列表,用於驗證碼
        Dim oFontNames As String() = {"Times New Roman", "MS Mincho", "Book Antiqua", _
                                      "Gungsuh", "PMingLiU", "Impact"}
        '驗證碼的字元集,去掉了一些容易混淆的字元
        Dim oCharacter As Char() = {"2"c, "3"c, "4"c, "5"c, "6"c, "8"c, _
                                    "9"c, "A"c, "B"c, "C"c, "D"c, "E"c, _
                                    "F"c, "G"c, "H"c, "J"c, "K"c, "L"c, _
                                    "M"c, "N"c, "P"c, "R"c, "S"c, "T"c, _
                                    "W"c, "X"c, "Y"c}
        Dim oRnd As New Random()
        Dim oBmp As Bitmap
        Dim oGraphics As Graphics
        Dim N1 As Integer
        Dim oPoint1 As Drawing.Point
        Dim oPoint2 As Drawing.Point
        Dim sFontName As String
        Dim oFont As Font
        Dim oColor As Color

        '生成驗證碼字串
        For N1 = 0 To CodeLength - 1
            sCode += oCharacter(oRnd.Next(oCharacter.Length))
        Next

        oBmp = New Bitmap(Width, Height)
        oGraphics = Graphics.FromImage(oBmp)
        oGraphics.Clear(Drawing.Color.White)
        Try
            For N1 = 0 To 4
                '畫噪線
                oPoint1.X = oRnd.Next(Width)
                oPoint1.Y = oRnd.Next(Height)
                oPoint2.X = oRnd.Next(Width)
                oPoint2.Y = oRnd.Next(Height)
                oColor = oColors(oRnd.Next(oColors.Length))
                oGraphics.DrawLine(New Pen(oColor), oPoint1, oPoint2)
            Next

            For N1 = 0 To sCode.Length - 1
                '畫驗證碼字串
                sFontName = oFontNames(oRnd.Next(oFontNames.Length))
                oFont = New Font(sFontName, FontSize, FontStyle.Italic)
                oColor = oColors(oRnd.Next(oColors.Length))
                oGraphics.DrawString(sCode(N1).ToString(), oFont, New SolidBrush(oColor), CSng(N1) * FontSize + 10, CSng(8))
            Next

            For i As Integer = 0 To 30
                '畫噪點
                Dim x As Integer = oRnd.Next(oBmp.Width)
                Dim y As Integer = oRnd.Next(oBmp.Height)
                Dim clr As Color = oColors(oRnd.Next(oColors.Length))
                oBmp.SetPixel(x, y, clr)
            Next

            Code = sCode
            Return oBmp
        Finally
            oGraphics.Dispose()
        End Try
    End Function

    ''' <summary>
    ''' 產生圖形驗證碼。
    ''' </summary>
    Public Sub CreateValidateCodeImage(ByRef MemoryStream As MemoryStream, _
        ByRef Code As String, ByVal CodeLength As Integer, _
        ByVal Width As Integer, ByVal Height As Integer, ByVal FontSize As Integer)
        Dim oBmp As Bitmap

        oBmp = CreateValidateCodeImage(Code, CodeLength, Width, Height, FontSize)
        Try
            oBmp.Save(MemoryStream, ImageFormat.Png)
        Finally
            oBmp.Dispose()
        End Try
    End Sub

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        Dim sCode As String = String.Empty
        '清除該頁輸出緩存,設置該頁無緩存
        Response.Buffer = True
        Response.ExpiresAbsolute = System.DateTime.Now.AddMilliseconds(0)
        Response.Expires = 0
        Response.CacheControl = "no-cache"
        Response.AppendHeader("Pragma", "No-Cache")
        '將驗證碼圖片寫入記憶體流,並將其以 "image/Png" 格式輸出
        Dim oStream As New MemoryStream()
        Try
            CreateValidateCodeImage(oStream, sCode, 4, 100, 40, 18)
            Me.Session("_ValidateCode") = sCode
            Response.ClearContent()
            Response.ContentType = "image/Png"
            Response.BinaryWrite(oStream.ToArray())
        Finally
            '釋放資源
            oStream.Dispose()
        End Try
    End Sub
End Class

我們將此頁面置於 ~/Page/ValidateCode.aspx,當要使用此頁面的圖形驗證碼,只需要在使用 Image 控制項,設定 ImageUrl 為此頁面即可。

<asp:Image ID="imgValidateCode" runat="server" ImageUrl="~/Page/ValidateCode.aspx" />

[超過字數限制,下一篇接續本文]

]]>
jeff377 2008-10-29 20:31:45
[ASP.NET 控制項實作 Day27] 控制項依 FormView CurrentMode 自行設定狀態(續2) https://ithelp.ithome.com.tw/articles/10013241?sc=rss.iron https://ithelp.ithome.com.tw/articles/10013241?sc=rss.iron 接續上一文
接下來設定做為新增、編輯使用的 TBFormView 控制項,我們只使用 EditItemTemplate 來同時處理新增、刪除,所以 EditItemTemplate ...]]>
接續上一文
接下來設定做為新增、編輯使用的 TBFormView 控制項,我們只使用 EditItemTemplate 來同時處理新增、刪除,所以 EditItemTemplate 需要同時具有「新增」、「更新」、「取消」三個按鈕。其中 ProductID 為主索引欄位,所以我們使用 TBTextBox 來繫結 ProductID 欄位,設定 FormViewModeState.InsertMode="Enable" 使控制項在新增模式時為可編輯,設定 FormViewModeState.EditMode="Disable" 使控制項在修改模式是唯讀的。

        <bee:TBFormView ID="TBFormView1" runat="server" DataKeyNames="ProductID" DataSourceID="SqlDataSource1"
            DefaultMode="Edit" SingleTemplate="EditItemTemplate" BackColor="White" BorderColor="#CCCCCC"
            BorderStyle="None" BorderWidth="1px" CellPadding="3" GridLines="Both" Visible="False">
            <FooterStyle BackColor="White" ForeColor="#000066" />
            <RowStyle ForeColor="#000066" />
            <EditItemTemplate>
                ProductID:
                <bee:TBTextBox ID="TextBox1" runat="server" Text='<%# Bind("ProductID") %>'> 
                  <FormViewModeState EditMode="Disable" InsertMode="Enable">
                  </FormViewModeState>
                </bee:TBTextBox>

                '省略

                <asp:LinkButton ID="LinkButton1" runat="server" CausesValidation="True" CommandName="Insert"
                    Text="新增" />
                 <asp:LinkButton ID="UpdateButton" runat="server" CausesValidation="True" CommandName="Update"
                    Text="更新" />
                 <asp:LinkButton ID="UpdateCancelButton" runat="server" CausesValidation="False"
                    CommandName="Cancel" Text="取消" />
            </EditItemTemplate>
        </bee:TBFormView>

2. 測試新增模式
接下來執行程式,一開始為瀏覽模式,以 TBGridView 來呈現資料。

按下 Header 的「新增」鈕,就會隱藏 TBGridView,而切換到 TBFormView 的新增模式。其中繫結 ProductID 欄位的 TBTextBox 為可編輯模式,而下方的按鈕只會顯示「新增」及「取消」鈕。

在新增模式輸入完畢後,按下「新增」鈕,資料錄就會被寫入資料庫。

3. 測試修改模式
接下來測試修改模式,按下「編輯」鈕,就會隱藏 TBGridView,而切換到 TBFormView 的修改模式。其中繫結 ProductID 欄位的 TBTextBox 為唯讀模式,而下方的按鈕只會顯示「更新」及「取消」鈕。

在修改模式輸入完畢後,按下「更新」鈕,資料錄就會被寫入資料庫。

4. 頁面程式碼
示範了上述的操作後,接下來我們回頭看一下頁面的程式碼。你沒看錯,筆者也沒貼錯,真的是一行程式碼都沒有,因為所有相關動作都由控制項處理掉了。

Partial Class Day27
    Inherits System.Web.UI.Page

End Class

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/28/5806.aspx

]]>
jeff377 2008-10-28 13:57:23
[ASP.NET 控制項實作 Day27] 控制項依 FormView CurrentMode 自行設定狀態(續1) https://ithelp.ithome.com.tw/articles/10013239?sc=rss.iron https://ithelp.ithome.com.tw/articles/10013239?sc=rss.iron 接續上一文
二、讓 TextBox 控制項可自行維護狀態
接下來擴展 TextBox 控制項,繼承 TextBox 命名為 TBText...]]>
接續上一文
二、讓 TextBox 控制項可自行維護狀態
接下來擴展 TextBox 控制項,繼承 TextBox 命名為 TBTextBox。新增 FormViewModeState 屬性 (TBFormViewModeState 型別),依 FormView Mode 來設定控制項狀。並覆寫 PreRender 方法,在此方法中呼叫 DoFormViewModeStatus 私有方法,依 FormView 的模式來處理控制項狀態。

    ''' <summary>
    ''' 文字框控制項。
    ''' </summary>
    < _
    Description("文字框控制項。"), _
    ToolboxData("<{0}:TBTextBox runat=server></{0}:TBTextBox>") _
    > _
    Public Class TBTextBox
        Inherits TextBox
        Private FFormViewModeState As TBFormViewModeState

        ''' <summary>
        ''' 依 FormViewMode 來設定控制項狀態。
        ''' </summary>
        < _
        Category(WebCommon.Category.Behavior), _
        NotifyParentProperty(True), _
        DesignerSerializationVisibility(DesignerSerializationVisibility.Content), _
        PersistenceMode(PersistenceMode.InnerProperty), _
        DefaultValue("") _
        > _
        Public ReadOnly Property FormViewModeState() As TBFormViewModeState
            Get
                If FFormViewModeState Is Nothing Then
                    FFormViewModeState = New TBFormViewModeState
                End If
                Return FFormViewModeState
            End Get
        End Property

        ''' <summary>
        ''' 處理控制項狀態。
        ''' </summary>
        Private Sub DoControlStatus(ByVal ControlStatus As EControlState)
            Select Case ControlStatus
                Case EControlState.Enable
                    Me.Enabled = True
                Case EControlState.Disable
                    Me.Enabled = False
                Case EControlState.Hide
                    Me.Visible = False
            End Select
        End Sub

        ''' <summary>
        ''' 依 FormView 的模式來處理控制項狀態。
        ''' </summary>
        Private Sub DoFormViewModeStatus()
            Dim oFormView As FormView

            '若控制項置於 FormView 中,則依 FormView 的模式來處理控制項狀態
            If TypeOf Me.BindingContainer Is FormView Then
                oFormView = DirectCast(Me.BindingContainer, FormView)
                Select Case oFormView.CurrentMode
                    Case FormViewMode.Insert
                        DoControlStatus(Me.FormViewModeState.InsertMode)
                    Case FormViewMode.Edit
                        DoControlStatus(Me.FormViewModeState.EditMode)
                    Case FormViewMode.ReadOnly
                        DoControlStatus(Me.FormViewModeState.BrowseMode)
                End Select
            End If
        End Sub

        ''' <summary>
        ''' 覆寫。引發 PreRender 事件。
        ''' </summary>
        Protected Overrides Sub OnPreRender(ByVal e As EventArgs)
            MyBase.OnPreRender(e)
            '依 FormView 的模式來處理控制項狀態
            DoFormViewModeStatus()
        End Sub

    End Class

三、測試程式
1. 設定控制項相關屬性
我們使用 Northwnd 資料庫的 Products資料表為例,以 GridView+FormView 示範資料「新增/修改/刪除」的操作。在頁面拖曳 SqlDataSource 控制項後,在頁面上的使用 TBGridView 來顯示瀏覽資料。TBGridView 的 FormViewID 設為關連的 TBFormVIew 控制項;另外有使用到 TBCommandField,設定 ShowHeaderNewButton=True,讓命令列具有「新增」鈕。

        <bee:TBGridView ID="TBGridView1" runat="server" AutoGenerateColumns="False" DataKeyNames="ProductID"
            DataSourceID="SqlDataSource1" FormViewID="TBFormView1">
            <Columns>
                <bee:TBCommandField ShowDeleteButton="True" ShowEditButton="True" 
                    ShowHeaderNewButton="True" >
                </bee:TBCommandField>
                
                '省略
                
            </Columns>
        </bee:TBGridView>

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/28/5806.aspx

]]>
jeff377 2008-10-28 13:53:32
[ASP.NET 控制項實作 Day27] 控制項依 FormView CurrentMode 自行設定狀態 https://ithelp.ithome.com.tw/articles/10013233?sc=rss.iron https://ithelp.ithome.com.tw/articles/10013233?sc=rss.iron GridV...]]> GridView+FormView 示範資料 新增/修改/刪除(進階篇:伺服器控制項) 一文中,示範了擴展 GridView 及 FormView 控制項,讓 GridView 可以透過屬性與 FormView 做關連來處理資料的「新增/修改/刪除」的動作。因為在該案例中,只使用 FormView 的 EditTemplate 同時處理「新增」及「修改」的動作,所以還需要自行撰寫部分程式碼去判斷控制項在新增或修改的啟用狀態,例如編號欄位在新增時為啟用,修改時就不啟用。在該文最後也提及其實有辨法讓這個案例達到零程式碼的目標,那就是讓控制項 (如 TextBox) 自行判斷所在的 FormView 的 CurrentMode,自行決定本身是否要「啟用/不啟用」、「顯示/隱藏」等狀態。本文以 TextBox 為例,說明如何修改 TextBox 讓它可以達到上述的需求。
程式碼下載:ASP.NET Server Control - Day27.rar
Northwnd 資料庫下載:NORTHWND.rar

一、TBFormViewModeState 類別

我們先定義 EControlState (控制項狀態) 列舉,描述控制項在特定模式的狀態為何。

    ''' <summary>
    ''' 控制項狀態列舉。
    ''' </summary>
    Public Enum EControlState
        ''' <summary>
        ''' 不設定。
        ''' </summary>
        NotSet = 0
        ''' <summary>
        ''' 啟用。
        ''' </summary>
        Enable = 1
        ''' <summary>
        ''' 不啟用。
        ''' </summary>
        Disable = 2
        ''' <summary>
        ''' 隱藏。
        ''' </summary>
        Hide = 3
    End Enum

再來定義 TBFormViewModeState 類別,用來設定控制項在各種 FormView 模式 (瀏覽、新增、修改) 中的控制項狀態。

''' <summary>
''' 依 FormViewMode 來設定控制項狀態。
''' </summary>
< _
Serializable(), _
TypeConverter(GetType(ExpandableObjectConverter)) _
> _
Public Class TBFormViewModeState
    Private FInsertMode As EControlState = EControlState.NotSet
    Private FEditMode As EControlState = EControlState.NotSet
    Private FBrowseMode As EControlState = EControlState.NotSet

    ''' <summary>
    ''' 在新增模式(FormViewMode=Insert)的控制項狀態。
    ''' </summary>
    < _
    NotifyParentProperty(True), _
    DefaultValue(GetType(EControlState), "NotSet") _
    > _
    Public Property InsertMode() As EControlState
        Get
            Return FInsertMode
        End Get
        Set(ByVal value As EControlState)
            FInsertMode = value
        End Set
    End Property

    ''' <summary>
    ''' 在編輯模式(FormViewMode=Edit)的控制項狀態。
    ''' </summary>
    < _
    NotifyParentProperty(True), _
    DefaultValue(GetType(EControlState), "NotSet") _
    > _
    Public Property EditMode() As EControlState
        Get
            Return FEditMode
        End Get
        Set(ByVal value As EControlState)
            FEditMode = value
        End Set
    End Property

    ''' <summary>
    ''' 在瀏覽模式(FormViewMode=ReadOnly)的控制項狀態。
    ''' </summary>
    < _
    NotifyParentProperty(True), _
    DefaultValue(GetType(EControlState), "NotSet") _
    > _
    Public Property BrowseMode() As EControlState
        Get
            Return FBrowseMode
        End Get
        Set(ByVal value As EControlState)
            FBrowseMode = value
        End Set
    End Property
End Class

定義為 TBFormViewModeState 型別的屬性是屬於複雜屬性,要套用 TypeConverter(GetType(ExpandableObjectConverter)),讓該屬性可在屬性視窗 (PropertyGrid) 擴展以便設定屬性值,如下圖所示。

[超過字數限制,下一篇接續本文]

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/28/5806.aspx

]]> jeff377 2008-10-28 13:45:43
[ASP.NET 控制項實作 Day26] 讓你的 GridView 與眾不同 https://ithelp.ithome.com.tw/articles/10013209?sc=rss.iron https://ithelp.ithome.com.tw/articles/10013209?sc=rss.iron 在網路上可以找到相當多擴展 GridView 控制項功能的文章,在筆者的部落格中也有多篇提及擴展 GridView、DataControlField、BoundFIeld 功能的相關文章,在本文...]]> 在網路上可以找到相當多擴展 GridView 控制項功能的文章,在筆者的部落格中也有多篇提及擴展 GridView、DataControlField、BoundFIeld 功能的相關文章,在本文將這些關於擴展 GridView 控制項功能及欄位類別的相關文章做一整理簡介,若需要擴展 GridView 相關功能時可以做為參考。
1. 擴展 GridView 控制項 - 無資料時顯示標題列
摘要:當 GridView 繫結的 DataSource 資料筆數為 0 時,會依 EmptyDataTemplate 及 EmptyDataText 的設定來顯示無資料的狀態。若我們希望 GridView 在無資料時,可以顯示欄位標題,有一種作法是在 EmptyDataTemplate 中手動在設定一個標題列,不過這種作法很麻煩。本文擴展 GridView 控制項,直接透過屬性設定就可以在無資料顯示欄位標題。

2. 擴展 GridView 控制項 - 支援 Excel 及 Word 匯出
摘要:GridView 匯出 Excel 及 Word 文件是蠻常使用的需求,此篇文章將擴展 GridView 控制項提供匯出 Excel 及 Word 文件的方法。一般在 GridView 匯出的常見下列問題也會在此一併被解決。

3. GridView+FormView 示範資料 新增/修改/刪除(進階篇:伺服器控制項)
摘要:擴展 GridView 及 FormView 控制項,在 GridView 控制項中新增 FormViewID 屬性,關連至指定的 FormView 控制項 ID,就可以讓 GridView 結合 FormView 來做資料異動的動作。

4. 擴展 CommandField 類別 - 刪除提示訊息
摘要:新增 DeleteConfirmMessage 屬性,設定刪除提示確認訊息。

5. 擴展 CommandField 類別 - 刪除提示訊息含欄位值
摘要:設定刪除提示確認訊息中可包含指定 DataField 欄位值,明確提示要刪除的資料列。

6. 讓 CheckBoxField 繫結非布林值(0 或 1)欄位
摘要:CheckBoxField 若繫結的欄位值為 0 或 1 時 (非布林值) 會發生錯誤,本文擴展 CheckBoxField 類別,讓 CheckBoxField 有辨法繫結 0 或 1 的欄位值。

7. 擴展 CheckBoxField 類別 - 支援非布林值的雙向繫結
摘要:CheckBoxField 繫結的欄位值並無法直接使用 CBool 轉型為布林值,例如 "T/F"、"是/否" 之類的資料,若希望使用 CheckBoxField 來顯示就比較麻煩,一般的作法都是轉為 TemplateField,自行撰寫資料繫結的函式,而且只能支援單向繫結。在本文直接改寫 CheckBoxField 類別,讓 CheckBoxField 可以直接雙向繫結 "T/F" 或 "是/否" 之類的資料。

8. 擴展 CommandField 類別 - Header 加入新增鈕
摘要:支援在 CommandField 的 Header 的部分加入「新增」鈕,執行新增鈕會引發 RowCommand 事件。

9. GridView 自動編號欄位 - TBSerialNumberField
摘要:繼承 DataControlField 來撰寫自動編號欄位,若 GridView 需要自動編號欄位時只需加入欄位即可。

10. 自訂 GridVie 欄位類別 - 實作 TBDropDownField 欄位類別
摘要:支援在 GridView 中顯示下拉清單的欄位類別。

11. 自訂 GridView 欄位 - 日期欄位
摘要:支援在 GridView 中顯示日期下拉選單編輯的欄位類別。

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/27/5793.aspx

]]>
jeff377 2008-10-27 22:37:14
[ASP.NET 控制項實作 Day25] 自訂 GridView 欄位 - 日期欄位(續) https://ithelp.ithome.com.tw/articles/10013091?sc=rss.iron https://ithelp.ithome.com.tw/articles/10013091?sc=rss.iron 接續上一文
四、覆寫 ExtractValuesFromCell 方法 - 擷取儲存格的欄位值
當用戶端使用 GridView 編輯後執...]]>
接續上一文
四、覆寫 ExtractValuesFromCell 方法 - 擷取儲存格的欄位值
當用戶端使用 GridView 編輯後執行更新動作時,會呼叫 ExtractValuesFromCell 方法,來取得儲存格的欄位值,以便寫入資料來源。所以我們要覆寫 ExtractValuesFromCell 方法,將 Cell 或 TDateEdit 控制項的值取出填入具 IOrderedDictionary 介面的物件。

        ''' <summary>
        ''' 使用指定 DataControlFieldCell 的值填入指定的 IDictionary 物件。 
        ''' </summary>
        ''' <param name="Dictionary">用於儲存指定儲存格的值。</param>
        ''' <param name="Cell">包含要擷取值的儲存格。</param>
        ''' <param name="RowState">資料列的狀態。</param>
        ''' <param name="IncludeReadOnly">true 表示包含唯讀欄位的值,否則為 false。</param>
        Public Overrides Sub ExtractValuesFromCell( _
            ByVal Dictionary As IOrderedDictionary, _
            ByVal Cell As DataControlFieldCell, _
            ByVal RowState As DataControlRowState, _
            ByVal IncludeReadOnly As Boolean)

            Dim oControl As Control = Nothing
            Dim sDataField As String = Me.DataField
            Dim oValue As Object = Nothing
            Dim sNullDisplayText As String = Me.NullDisplayText
            Dim oDateEdit As TBDateEdit

            If (((RowState And DataControlRowState.Insert) = DataControlRowState.Normal) OrElse Me.InsertVisible) Then
                If (Cell.Controls.Count > 0) Then
                    oControl = Cell.Controls.Item(0)
                    oDateEdit = TryCast(oControl, TBDateEdit)
                    If (Not oDateEdit Is Nothing) Then
                        oValue = oDateEdit.Text
                    End If
                ElseIf IncludeReadOnly Then
                    Dim s As String = Cell.Text
                    If (s = " ") Then
                        oValue = String.Empty
                    ElseIf (Me.SupportsHtmlEncode AndAlso Me.HtmlEncode) Then
                        oValue = HttpUtility.HtmlDecode(s)
                    Else
                        oValue = s
                    End If
                End If

                If (Not oValue Is Nothing) Then
                    If TypeOf oValue Is String Then
                        If (CStr(oValue).Length = 0) AndAlso Me.ConvertEmptyStringToNull Then
                            oValue = Nothing
                        ElseIf (CStr(oValue) = sNullDisplayText) AndAlso (sNullDisplayText.Length > 0) Then
                            oValue = Nothing
                        End If
                    End If

                    If Dictionary.Contains(sDataField) Then
                        Dictionary.Item(sDataField) = oValue
                    Else
                        Dictionary.Add(sDataField, oValue)
                    End If
                End If
            End If
        End Sub

五、測試程式
我們使用 Northwnd 資料庫的 Employees 資料表為例,在 GridView 加入自訂的 TBDateField 欄位繫結 BirthDate 欄位,另外加入另一個 BoundField 的唯讀欄位,也同樣繫結 BirthDate 欄位來做比較。

            <bee:TBDateField DataField="BirthDate" HeaderText="BirthDate" 
                SortExpression="BirthDate" DataFormatString="{0:d}" 
                ApplyFormatInEditMode="True" CalendarStyle="Winter" />            
            <asp:BoundField DataField="BirthDate" HeaderText="BirthDate" 
                SortExpression="BirthDate" DataFormatString="{0:d}" 
                ApplyFormatInEditMode="True" ReadOnly="true" />

執行程式,在編輯資料列時,TBDateField 就會以 TDateEdit 控制項來進行編輯。

使用 TDateEdit 編輯欄位值後,按「更新」鈕,資料就會被寫回資料庫。

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/26/5777.aspx

]]>
jeff377 2008-10-26 17:04:50
[ASP.NET 控制項實作 Day25] 自訂 GridView 欄位 - 日期欄位 https://ithelp.ithome.com.tw/articles/10013083?sc=rss.iron https://ithelp.ithome.com.tw/articles/10013083?sc=rss.iron 前二篇文章介紹了自訂 GridView 使用的下拉清單欄位 (TBDropDownField),對如何繼承 BoundField 類別下來改寫自訂欄位應該有進一步的了解。在 GridView 中...]]> 前二篇文章介紹了自訂 GridView 使用的下拉清單欄位 (TBDropDownField),對如何繼承 BoundField 類別下來改寫自訂欄位應該有進一步的了解。在 GridView 中輸入日期也常蠻常見的需求,在本文將再實作一個 GridView 使用的日期欄位,在欄位儲存格使用 TBDateEdit 控制項來編輯資料。
程式碼下載:ASP.NET Server Control - Day25.rar
Northwnd 資料庫下載:NORTHWND.rar

一、繼承 TBBaseBoundField 實作 TDateField
GridView 的日期欄位需要繫結資料,一般的作法是由 BoundField 繼承下來改寫;不過我們之前已經有繼承 BoundField 製作一個 TBBaseBoundField 的自訂欄位基底類別 (詳見「 [ASP.NET 控制項實作 Day23] 自訂 GridVie 欄位類別 - 實作 TBDropDownField 欄位類別」 一文),所以我們要實作的日期欄位直接繼承 TBBaseBoundField 命名為 TDateField,並覆寫 CreateField 方法,傳回 TDateField 物件。

    ''' <summary>
    ''' 日期欄位。
    ''' </summary>
    Public Class TBDateField
        Inherits TBBaseBoundField

        Protected Overrides Function CreateField() As DataControlField
            Return New TBDateField()
        End Function
    End Class

自訂欄位類別主要是要覆寫 InitializeDataCell 方法做資料儲存格初始化、覆寫 OnDataBindField 方法將欄位值繫結至 BoundField 物件、覆寫 ExtractValuesFromCell 方法來擷取儲存格的欄位值,下面我們將針對這幾個需要覆寫的方法做一說明。

二、覆寫 InitializeDataCell 方法 - 資料儲存格初始化
首先覆寫 InitializeDataCell 方法處理資料儲存格初始化,當唯讀狀態時使用 Cell 來呈現資料;若為編輯狀態時,則在 Cell 中加入 TBDateEdit 控制項,並將 TBDateField 的屬性設定給 TBDateEdit 控制項的相關屬性。然後將儲存格 (DataControlFieldCell) 或日期控制項 (TDateEdit) 的 DataBinding 事件導向 OnDataBindField 事件處理方法。

        ''' <summary>
        ''' 資料儲存格初始化。
        ''' </summary>
        ''' <param name="Cell">要初始化的儲存格。</param>
        ''' <param name="RowState">資料列狀態。</param>
        Protected Overrides Sub InitializeDataCell(ByVal Cell As DataControlFieldCell, ByVal RowState As DataControlRowState)
            Dim oDateEdit As TBDateEdit
            Dim oControl As Control

            If Me.CellIsEdit(RowState) Then
                '編輯狀態在儲存格加入 TBDateEdit 控制項
                oDateEdit = New TBDateEdit()
                oDateEdit.FirstDayOfWeek = Me.FirstDayOfWeek
                oDateEdit.ShowWeekNumbers = Me.ShowWeekNumbers
                oDateEdit.CalendarStyle = Me.CalendarStyle
                oDateEdit.Lang = Me.Lang
                oDateEdit.ShowTime = Me.ShowTime
                oControl = oDateEdit
                Cell.Controls.Add(oControl)
            Else
                oControl = Cell
            End If

            If (oControl IsNot Nothing) AndAlso MyBase.Visible Then
                AddHandler oControl.DataBinding, New EventHandler(AddressOf Me.OnDataBindField)
            End If
        End Sub

TDateEdit 控制項為筆者自行撰寫的日期控制項,TDateEdit 控制項的相關細節可以參考筆者部落格下面幾篇文章有進一步說明。
日期控制項實作教學(1) - 結合 JavaScript
日期控制項實作教學(2) - PostBack 與 事件
TBDateEdit 日期控制項 - 1.0.0.0 版 (Open Source)

三、覆寫 OnDataBindField 方法 - 將欄位值繫結至 BoundField 物件
當 GridView 執行 DataBind 時,每個儲存格的 DataBinding 事件都會被導向 OnDataBindField 方法,此方法中我們會由資料來源取得指定欄位值,處理此欄位值的格式化時,將欄位值呈現在 Cell 或 TDateEdit 控制項上。

        ''' <summary>
        ''' 將欄位值繫結至 BoundField 物件。 
        ''' </summary>
        Protected Overrides Sub OnDataBindField(ByVal sender As Object, ByVal e As EventArgs)
            Dim oControl As Control
            Dim oDateEdit As TBDateEdit
            Dim oNamingContainer As Control
            Dim oDataValue As Object            '欄位值
            Dim bEncode As Boolean              '是否編碼
            Dim sText As String                 '格式化字串

            oControl = DirectCast(sender, Control)
            oNamingContainer = oControl.NamingContainer
            oDataValue = Me.GetValue(oNamingContainer)
            bEncode = ((Me.SupportsHtmlEncode AndAlso Me.HtmlEncode) AndAlso TypeOf oControl Is TableCell)
            sText = Me.FormatDataValue(oDataValue, bEncode)

            If TypeOf oControl Is TableCell Then
                If (sText.Length = 0) Then
                    sText = " "
                End If
                DirectCast(oControl, TableCell).Text = sText
            Else
                If Not TypeOf oControl Is TBDateEdit Then
                    Throw New HttpException(String.Format("{0}: Wrong Control Type", Me.DataField))
                End If

                oDateEdit = DirectCast(oControl, TBDateEdit)
                If Me.ApplyFormatInEditMode Then
                    oDateEdit.Text = sText
                ElseIf (Not oDataValue Is Nothing) Then
                    oDateEdit.Text = oDataValue.ToString
                End If
            End If
        End Sub

[超過字數限制,下一篇接續本文]

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/25/5772.aspx

]]>
jeff377 2008-10-26 16:56:36
[ASP.NET 控制項實作 Day24] TBDropDownField 的 Items 屬性的資料繫結(續) https://ithelp.ithome.com.tw/articles/10013047?sc=rss.iron https://ithelp.ithome.com.tw/articles/10013047?sc=rss.iron 接續上一文
三、由關連的資料來源擷取資料
再來就是重點就是要處理 PerformSelecrt 私有方法,來取得 Items 屬性的成員...]]>
接續上一文
三、由關連的資料來源擷取資料
再來就是重點就是要處理 PerformSelecrt 私有方法,來取得 Items 屬性的成員清單內容。PerformSelect 方法的作用是去尋找頁面上的具 IDataSource 介面的控制項,並執行此資料來源的 Select 方法,以取得資料來設定 Items 的清單內容。
step1. 尋找資料來源控制項
PerformSelect 方法中有使用 FindControlEx 方法,它是自訂援尋控制項的多載方法,是取代 FindControl 進階方法。程式碼中使用 FindControlEx 去是頁面中以遞迴方式尋找具有 IDataSource 介面的控制項,且 ID 屬性值為 TBDropDownList.ID 的屬性值。
step2. 執行資料來源控制項的 Select 方法
當找到資料來源控制項後 (如 SqlDataSource、ObjectDataSource ...等等),執行其 DataSourceView.Select 方法,此方法需入一個 DataSourceViewSelectCallback 函式當作參數,當資料來源控制項取得資料後回呼我們指定的 OnDataSourceViewSelectCallback 函式中做後序處理。
step3. 將取得的資料來設定生 Items 的清單內容
在 OnDataSourceViewSelectCallback 函式中接到回傳的具 IEnumerable 介面的資料,有可能是 DataView、DataTable ...等型別的資料。利用 DataBinder.GetPropertyValue 來取得 DataTextField 及 DataValueField 設定的欄位值,逐一建立 ListItem 項目,並加入 Items 集合屬性中。

        ''' <summary>
        ''' 從關聯的資料來源擷取資料。
        ''' </summary>
        Private Sub PerformSelect()
            Dim oControl As Control
            Dim oDataSource As IDataSource
            Dim oDataSourceView As DataSourceView

            '若未設定 DataSourceID 屬性則離開
            If StrIsEmpty(Me.DataSourceID) Then Exit Sub
            '找到具 IDataSource 介面的控制項
            oControl = FindControlEx(Me.Control.Page, GetType(IDataSource), "ID", Me.DataSourceID)
            If oControl Is Nothing Then Exit Sub

            oDataSource = DirectCast(oControl, IDataSource)
            oDataSourceView = oDataSource.GetView(String.Empty)
            oDataSourceView.Select(DataSourceSelectArguments.Empty, _
                        New DataSourceViewSelectCallback(AddressOf Me.OnDataSourceViewSelectCallback))
        End Sub

        ''' <summary>
        ''' 擷取資料的回呼函式。
        ''' </summary>
        ''' <param name="data">取得的資料。</param>
        Private Sub OnDataSourceViewSelectCallback(ByVal data As IEnumerable)
            Dim oCollection As ICollection
            Dim oValue As Object
            Dim oItem As ListItem

            Me.Items.Clear()
            If data Is Nothing Then Exit Sub

            oCollection = TryCast(data, ICollection)
            Me.Items.Capacity = oCollection.Count

            For Each oValue In data
                oItem = New ListItem()
                If StrIsNotEmpty(Me.DataTextField) Then
                    oItem.Text = DataBinder.GetPropertyValue(oValue, DataTextField, Nothing)
                End If
                If StrIsNotEmpty(Me.DataValueField) Then
                    oItem.Value = DataBinder.GetPropertyValue(oValue, DataValueField, Nothing)
                End If
                Me.Items.Add(oItem)
            Next
        End Sub

四、測試程式
使用上篇中同一個案例做測試,同樣以 Northwnd 資料庫的 Products 資料表為例。在 GridView 加入自訂的 TBDropDownField 欄位繫結 CategoryID 欄位,並設定 DataSourceID、DataTextField、DataValueField 屬性;另外加入另一個 BoundField 的唯讀欄位,也同樣繫結 CategoryID 欄位來做比較。

                <bee:TBDropDownField  HeaderText="CategoryID"  
                    SortExpression="CategoryID" DataField="CategoryID" 
                    DataTextField="CategoryName" DataValueField="CategoryID" 

DataSourceID="SqlDataSource2">
                </bee:TBDropDownField>
                <asp:BoundField DataField="CategoryID" HeaderText="CategoryID" 
                    SortExpression="CategoryID"  ReadOnly="true" />

執行程式,在 GridView 瀏覽的模式時,TBDropDownField 的儲存格已經會呈現 Items 對應成員的顯示文字。

執行資料列編輯時,也可以正常顯示下拉清單的內容。

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/25/5772.aspx

]]>
jeff377 2008-10-25 18:11:28
[ASP.NET 控制項實作 Day24] TBDropDownField 的 Items 屬性的資料繫結 https://ithelp.ithome.com.tw/articles/10013041?sc=rss.iron https://ithelp.ithome.com.tw/articles/10013041?sc=rss.iron 上篇中我們實作了 GridView 的 TBDropDownField 欄位類別,不過眼尖的讀者不知有沒有發覺我們並處理 Items 屬性取得成員清單的動作,而是直接設定儲存格內含的 TBDro...]]> 上篇中我們實作了 GridView 的 TBDropDownField 欄位類別,不過眼尖的讀者不知有沒有發覺我們並處理 Items 屬性取得成員清單的動作,而是直接設定儲存格內含的 TBDropDownList 控制項相關屬性 (DataSourceID、DataTextField、DataValueField 屬性) 後,就由 TDropDownList 控制項自行處理 Items 屬性的資料繫結。當 GridView 的資料列是編輯狀態時,下拉清單會顯示出 Items 的文字內容;可是瀏覽狀態的資料列,卻是顯示欄位原始值,無法呈現 Items 的文字內容。本文將說明如何自行處理 TBDropDownField 的 Items 屬性的資料繫結動作,並使唯讀狀態的資料列也可以呈現 Items 的文字內容。

程式碼下載:ASP.NET Server Control - Day24.rar
Northwnd 資料庫下載:NORTHWND.rar

一、Items 屬性的問題
我們重新看一次原本 TBDropDownField 類別在處理 Items 屬性的資料繫結取得清單內容的程式碼,在覆寫 InitializeDataCell 方法中,當儲存格為編輯模式時,會呈現 TBDropDownList 控制項並設定取得 Items 清單內容的相關屬性,讓 TBDropDownList 自行去處理它的 Items 屬性的清單內容。

'由資料來源控制項取得清單項目
oDropDownList.DataSourceID = Me.DataSourceID
oDropDownList.DataTextField = Me.DataTextField
oDropDownList.DataValueField = Me.DataValueField

不知你有沒有發覺,我們無論在 InitializeDataCell 及 OnDataBindField 方法中,都沒有針對 TBDropDownList 控制項做任何 DataBind 動作,那它是怎麼從 DataSourceID 關聯的資料來源擷取資料呢?因為 GridView 在執行 DataBind 時,就會要求所有的子控制項做 DataBind,所以我們只要設定好 BDropDownList 控制項相關屬性後,當 TBDropDownList 自動被要求資料繫結時就會取得 Items 的清單內容。
當然使用 TBDropDownList 控制項去處理 Items 的資料繫結動作最簡單,可是這樣唯讀的儲存格只能顯示原始欄位值,無法呈現 Items 中對應成員的文字;除非無論唯讀或編輯狀態,都要建立 TBDropDownList 控制項去取得 Items 清單內容,而唯讀欄位使用 TBDropDownList.Items 去找到對應成員的顯示文字,不過這樣的作法會怪怪的,而且沒有執行效能率。所以比較好的辨法,就是由 TBDropDownField 類別自行處理 Items 的資料繫結,同時提供給唯讀狀態的
DataControlFieldCell 及編輯狀態的 TBDropDownList 使用。

二、由 TBDropDownField 類別處理 Items 屬性的資料繫結
我們要自行處理 Items 屬性來取得成員清單,在 InitializeDataCell 方法中無須處理 Items 屬性,只需產生儲存格需要的子控制項,未來在執行子控制項的 DataBinding 時的 OnDataBindField 方法中再來處理 Items 屬性。

        Protected Overrides Sub InitializeDataCell( _
            ByVal Cell As DataControlFieldCell, _
            ByVal RowState As DataControlRowState)

            Dim oDropDownList As TBDropDownList
            Dim oControl As Control

            If Me.CellIsEdit(RowState) Then
                oDropDownList = New TBDropDownList()
                oControl = oDropDownList
                Cell.Controls.Add(oControl)
            Else
                oControl = Cell
            End If

            If (oControl IsNot Nothing) AndAlso MyBase.Visible Then
                AddHandler oControl.DataBinding, New EventHandler(AddressOf Me.OnDataBindField)
            End If
        End Sub

在 OnDataBindField 方法中,我們加上一段處理 Items 屬性的程式碼如下,會利用 PerformSelecrt 私有方法,由關聯的資料來源 (即 DataSrouceID 指定的資料來源控制項) 擷取資料並產生 Items 的成員清單,在後面會詳細講解 PerformSelecrt 方法處理擷取資料的細節。因為 TBDropDownField 每個資料儲存格都會執行 OnDataBindField 方法,但 Items 取得成員清單的動作只需做一次即可,所以會以 FIsPerformSelect 區域變數來判斷是否已取得 Items 的成員清單,若已取過就不重新取得,這樣比較有執行效能。

            If Not Me.DesignMode Then
                If Not FIsPerformSelect Then
                    '從關聯的資料來源擷取資料
                    PerformSelect()
                    FIsPerformSelect = True
                End If
            End If

當取得儲存儲的對應的欄位值時,依此欄位值由 Items 集合去取得對應的 ListItem 成員,並以此 ListItem.Text 的文字內容來做顯示。

            '由 Items 去取得對應成員的顯示內容
            oListItem = Me.Items.FindByValue(CCStr(sText))
            If oListItem IsNot Nothing Then
                sText = oListItem.Text
            End If

若是由 TBDropDownList 所引發的 OnDataBindField 方法時,使用 SetItems 私有方法將 TBDropDownField.Items 屬性複製給 TBDropDownList.Item 屬性。

                ODropDownList = DirectCast(oControl, TBDropDownList)
                SetItems(ODropDownList)

SetItems 私有方法的程式碼如下。

        Private Sub SetItems(ByVal DropDownList As TBDropDownList)
            Dim oItems() As ListItem

            If Not Me.DesignMode Then
                ReDim oItems(Me.Items.Count - 1)
                Me.Items.CopyTo(oItems, 0)
                DropDownList.Items.AddRange(oItems)
            End If
        End Sub

[超過字數限制,下一篇接續本文]

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/25/5772.aspx

]]>
jeff377 2008-10-25 18:09:12
[ASP.NET 控制項實作 Day23] 自訂GridVie欄位-實作TBDropDownField欄位(續3) https://ithelp.ithome.com.tw/articles/10012977?sc=rss.iron https://ithelp.ithome.com.tw/articles/10012977?sc=rss.iron 接續上一文
四、測試程式
辛苦寫好 TBDropDownField 欄位類別時,接下來就是驗收成果的時候。我們以 Northwnd 資料...]]>
接續上一文
四、測試程式
辛苦寫好 TBDropDownField 欄位類別時,接下來就是驗收成果的時候。我們以 Northwnd 資料庫的 Products 資料表為例,將 TBDropDownList .DataField 設為 CategoryID 欄位來做測試。首先我們測試沒有 DataSoruceID 的情況,在 GridView 加入自訂的 TBDropDownField 欄位繫結 CategoryID 欄位,另外加入另一個 BoundField 的唯讀欄位,也同樣繫結 CategoryID 欄位來做比較。

                <bee:TBDropDownField  HeaderText="CategoryID"  
                    SortExpression="CategoryID" DataField="CategoryID" >
                    <Items>
                    <asp:ListItem Value="">未對應</asp:ListItem>
                    <asp:ListItem Value="2">Condiments</asp:ListItem>
                    <asp:ListItem Value="3">Confections</asp:ListItem>
                    </Items>
                </bee:TBDropDownField>
                <asp:BoundField DataField="CategoryID" HeaderText="CategoryID" 
                    SortExpression="CategoryID"  ReadOnly="true" />

執行程式,在 GridView 在唯讀模式,TBDropDownFIeld 可以正確的繫結 CategoryID 欄位值。

編輯某筆資料列進入編輯狀態,就會顯示 TBDropDownList 控制項,清單成員為我們在 Items 設定的內容。

使用 TBDropDownList 來做編輯欄位值,按下更新鈕,這時會執行 TBDropDownField.ExtractValuesFromCell 方法,取得儲存格中的值;最後由資料來源控制項將欄位值寫回資料庫。

接下來測試設定 TBDropDownField.DataSourceID 的情況,把 DataSourcID 指向含 Categories 資料表內容的 SqlDataSoruce 控制項。

                <bee:TBDropDownField  HeaderText="CategoryID"  
                    SortExpression="CategoryID" DataField="CategoryID" 
                    DataTextField="CategoryName" DataValueField="CategoryID" DataSourceID="SqlDataSource2">
                </bee:TBDropDownField>

執行程式查看結果,可以發現 TBDropDownList 控制項的清單內容也可以正常顯示 SqlDataSoruce 控制項取得資料。

[超過字數限制,下一篇接續本文]

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/24/5762.aspx

]]>
jeff377 2008-10-24 00:32:30
[ASP.NET 控制項實作 Day23] 自訂GridVie欄位-實作TBDropDownField欄位(續2) https://ithelp.ithome.com.tw/articles/10012973?sc=rss.iron https://ithelp.ithome.com.tw/articles/10012973?sc=rss.iron 接續上一文
step4. 處理資料繫結
當 GridView 控制項在執行資料繫結時,儲存格的控制項就會引發 DataBinding 事...]]>
接續上一文
step4. 處理資料繫結
當 GridView 控制項在執行資料繫結時,儲存格的控制項就會引發 DataBinding 事件,而這些事件會被導向 OnDataBindField 方法來統一處理儲存格中控制項的繫結動作。

       ''' <summary>
        ''' 將欄位值繫結至 BoundField 物件。 
        ''' </summary>
        ''' <param name="sender">控制項。</param>
        ''' <param name="e">事件引數。</param>
        Protected Overrides Sub OnDataBindField(ByVal sender As Object, ByVal e As EventArgs)
            Dim oControl As Control
            Dim ODropDownList As TBDropDownList
            Dim oNamingContainer As Control
            Dim oDataValue As Object            '欄位值
            Dim bEncode As Boolean              '是否編碼
            Dim sText As String                 '格式化字串

            oControl = DirectCast(sender, Control)
            oNamingContainer = oControl.NamingContainer
            oDataValue = Me.GetValue(oNamingContainer)
            bEncode = ((Me.SupportsHtmlEncode AndAlso Me.HtmlEncode) AndAlso TypeOf oControl Is TableCell)
            sText = Me.FormatDataValue(oDataValue, bEncode)

            If TypeOf oControl Is TableCell Then
                If (sText.Length = 0) Then
                    sText = " "
                End If
                DirectCast(oControl, TableCell).Text = sText
            Else
                If Not TypeOf oControl Is TBDropDownList Then
                    Throw New HttpException(String.Format("{0}: Wrong Control Type", Me.DataField))
                End If

                ODropDownList = DirectCast(oControl, TBDropDownList)

                If Me.ApplyFormatInEditMode Then
                    ODropDownList.Text = sText
                ElseIf (Not oDataValue Is Nothing) Then
                    ODropDownList.Text = oDataValue.ToString
                End If
            End If
        End Sub

step5. 取得儲存格中的值
另外我們還需要覆寫 ExtractValuesFromCell 方法,取得儲存格中的值。這個方法是當 GridView 的編輯資料要準備寫入資料庫時,會經由 ExtractValuesFromCell 方法此來取得每個儲存格的值,並將這些欄位值加入 Dictionary 參數中,這個準備寫入的欄位值集合,可以在 DataSource 控制項的寫入資料庫的相關方法中取得使用。

        ''' <summary>
        ''' 使用指定 DataControlFieldCell 物件的值填入指定的 System.Collections.IDictionary 物件。 
        ''' </summary>
        ''' <param name="Dictionary">用於儲存指定儲存格的值。</param>
        ''' <param name="Cell">包含要擷取值的儲存格。</param>
        ''' <param name="RowState">資料列的狀態。</param>
        ''' <param name="IncludeReadOnly">true 表示包含唯讀欄位的值,否則為 false。</param>
        Public Overrides Sub ExtractValuesFromCell( _
            ByVal Dictionary As IOrderedDictionary, _
            ByVal Cell As DataControlFieldCell, _
            ByVal RowState As DataControlRowState, _
            ByVal IncludeReadOnly As Boolean)

            Dim oControl As Control = Nothing
            Dim sDataField As String = Me.DataField
            Dim oValue As Object = Nothing
            Dim sNullDisplayText As String = Me.NullDisplayText
            Dim oDropDownList As TBDropDownList

            If (((RowState And DataControlRowState.Insert) = DataControlRowState.Normal) OrElse Me.InsertVisible) Then
                If (Cell.Controls.Count > 0) Then
                    oControl = Cell.Controls.Item(0)
                    oDropDownList = TryCast(oControl, TBDropDownList)
                    If (Not oDropDownList Is Nothing) Then
                        oValue = oDropDownList.Text
                    End If
                ElseIf IncludeReadOnly Then
                    Dim s As String = Cell.Text
                    If (s = " ") Then
                        oValue = String.Empty
                    ElseIf (Me.SupportsHtmlEncode AndAlso Me.HtmlEncode) Then
                        oValue = HttpUtility.HtmlDecode(s)
                    Else
                        oValue = s
                    End If
                End If

                If (Not oValue Is Nothing) Then
                    If TypeOf oValue Is String Then
                        If (CStr(oValue).Length = 0) AndAlso Me.ConvertEmptyStringToNull Then
                            oValue = Nothing
                        ElseIf (CStr(oValue) = sNullDisplayText) AndAlso (sNullDisplayText.Length > 0) Then
                            oValue = Nothing
                        End If
                    End If

                    If Dictionary.Contains(sDataField) Then
                        Dictionary.Item(sDataField) = oValue
                    Else
                        Dictionary.Add(sDataField, oValue)
                    End If
                End If
            End If
        End Sub

[超過字數限制,下一篇接續本文]

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/24/5762.aspx

]]>
jeff377 2008-10-24 00:31:32
[ASP.NET 控制項實作 Day23] 自訂GridVie欄位-實作TBDropDownField欄位(續1) https://ithelp.ithome.com.tw/articles/10012971?sc=rss.iron https://ithelp.ithome.com.tw/articles/10012971?sc=rss.iron 接續上一文
step2. 加入 TBBaseBoundField 的屬性
TBBaseBoundField 類別會內含 DropDown...]]>
接續上一文
step2. 加入 TBBaseBoundField 的屬性
TBBaseBoundField 類別會內含 DropDownList 控制項,所以加入設定 DropDownList 控制項的對應屬性;我們在 TBBaseBoundField 類別加入了 Items 、DataSourceID、DataTextField、DataValueField 屬性。其中 Items 屬性的型別與 DropDownList.Items 屬性相同,都是 ListItemCollection 集合類別,且 Items 屬性會儲存於 ViewState 中。

        ''' <summary>
        ''' 清單項目集合。
        ''' </summary>
        < _
        Description("清單項目集合。"), _
        DefaultValue(CStr(Nothing)), _
        PersistenceMode(PersistenceMode.InnerProperty), _
        DesignerSerializationVisibility(DesignerSerializationVisibility.Content), _
        Editor(GetType(ListItemsCollectionEditor), GetType(UITypeEditor)), _
        MergableProperty(False), _
        Category("Default")> _
        Public Overridable ReadOnly Property Items() As ListItemCollection
            Get
                If (FItems Is Nothing) Then
                    FItems = New ListItemCollection()
                    If MyBase.IsTrackingViewState Then
                        CType(FItems, IStateManager).TrackViewState()
                    End If
                End If
                Return FItems
            End Get
        End Property

        ''' <summary>
        ''' 資料來源控制項的 ID 屬性。
        ''' </summary>
        Public Property DataSourceID() As String
            Get
                Return FDataSourceID
            End Get
            Set(ByVal value As String)
                FDataSourceID = value
            End Set
        End Property

        ''' <summary>
        ''' 提供清單項目文字內容的資料來源的欄位。
        ''' </summary>
        < _
        Description("提供清單項目文字內容的資料來源的欄位。"), _
        DefaultValue("") _
        > _
        Public Property DataTextField() As String
            Get
                Return FDataTextField
            End Get
            Set(ByVal value As String)
                FDataTextField = value
            End Set
        End Property

        ''' <summary>
        ''' 提供清單項目值的資料來源的欄位。
        ''' </summary>
        Public Property DataValueField() As String
            Get
                Return FDataValueField
            End Get
            Set(ByVal value As String)
                FDataValueField = value
            End Set
        End Property

step3.建立儲存格內含的控制項
GridView 是以儲存格 (DataControlFieldCell) 為單位,我們要覆寫 InitializeDataCell 方法來建立儲存格中的控制項;當儲存格為可編輯狀態時,就建立 DropDownList 控制項並加入儲存格中,在此使用上篇文章提及的 TBDropDownList 控制項來取代,以解決清單成員不存在造成錯誤的問題。若未設定 DataSourceID 屬性時,則由 Items 屬性取得自訂的清單項目;若有設定 DataSourceID 屬性,則由資料來源控制項 (如 SqlDataSource、ObjectDataSource 控制項) 來取得清單項目。
當建立儲存格中的控制項後,需要以 AddHeadler 的方法,將此控制項的 DataBinding 事件導向 OnDataBindField 這個事件處理方法,我們要在 OnDataBindField 處理資料繫結的動作。

        ''' <summary>
        ''' 資料儲存格初始化。
        ''' </summary>
        ''' <param name="Cell">要初始化的儲存格。</param>
        ''' <param name="RowState">資料列狀態。</param>
        Protected Overrides Sub InitializeDataCell( _
            ByVal Cell As DataControlFieldCell, _
            ByVal RowState As DataControlRowState)

            Dim oDropDownList As TBDropDownList
            Dim oItems() As ListItem
            Dim oControl As Control

            If Me.CellIsEdit(RowState) Then
                oDropDownList = New TBDropDownList()
                oControl = oDropDownList
                Cell.Controls.Add(oControl)

                If Not Me.DesignMode Then
                    If StrIsEmpty(Me.DataSourceID) Then
                        '自訂清單項目
                        ReDim oItems(Me.Items.Count - 1)
                        Me.Items.CopyTo(oItems, 0)
                        oDropDownList.Items.AddRange(oItems)
                    Else
                        '由資料來源控制項取得清單項目
                        oDropDownList.DataSourceID = Me.DataSourceID
                        oDropDownList.DataTextField = Me.DataTextField
                        oDropDownList.DataValueField = Me.DataValueField
                    End If
                End If
            Else
                oControl = Cell
            End If

            If (oControl IsNot Nothing) AndAlso MyBase.Visible Then
                AddHandler oControl.DataBinding, New EventHandler(AddressOf Me.OnDataBindField)
            End If

        End Sub

[超過字數限制,下一篇接續本文]

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/24/5762.aspx

]]>
jeff377 2008-10-24 00:25:02
[ASP.NET 控制項實作 Day23] 自訂GridVie欄位-實作TBDropDownField欄位 https://ithelp.ithome.com.tw/articles/10012965?sc=rss.iron https://ithelp.ithome.com.tw/articles/10012965?sc=rss.iron GridView 是 ASP.NET 中一個相當常用的控制項,在 GridView 可加入 BoundField、CheckBoxField、CommandField、TemplateField...]]> GridView 是 ASP.NET 中一個相當常用的控制項,在 GridView 可加入 BoundField、CheckBoxField、CommandField、TemplateField ... 等不同型別的欄位,可是偏偏沒有提供在 GridView 中可呈現 DropDownList 的欄位型別;遇到這類需求時,一般的作法都是使用 TemplateField 來處理。雖然 TemplateField 具有相當好的設計彈性。可是在當 GridView 需要動態產生欄位的需求時,TemplateField 就相當麻煩,要寫一堆程式碼自行去處理資料繫結的動作。相互比較起來,BoundField、CheckBoxField ...等這類事先定義類型的欄位,在 GridView 要動態產生這些欄位就相當方便。如果我們可以把一些常用的 GridView 的欄位,都做成類似 BoundField 一樣,只要設定欄位的屬性就好,這樣使用上就會方便許多,所以在本文將以實作 DropDownList 欄位為例,讓大家了解如何去自訂 GridView 的欄位類別。
程式碼下載:ASP.NET Server Control - Day23.rar
Northwnd 資料庫下載:NORTHWND.rar

一、選擇合適的父類別
一般自訂 GridView 的欄位類別時,大都是由 DataControlField 或 BoundField 繼承下來改寫。若是欄位不需繫結資料(如 CommandFIeld),可以由 DataControlFIeld 繼承下來,若是欄位需要做資料繫結時(如 CheckBoxFIld,可以直接由 BoundField 繼承下來改寫比較方便。
DataControlField 類別是所有類型欄位的基底類別,BoundField 類別也是由 DataControlField 類別繼承下來擴展了資料繫結部分的功能,所以我們要實作含 DropDownList 的欄位,也是由 BoundField 繼承下來改寫。

二、自訂欄位基底類別
在此我們不直接繼承 BoundFIeld,而是先撰寫一個繼承 BoundField 命名為 TBBaseBoundField 的基底類別,此類別提供一些通用的屬性及方法,使我們更方便去撰寫自訂的欄位類別。

    ''' <summary>
    ''' 資料欄位基礎類別。
    ''' </summary>
    Public MustInherit Class TBBaseBoundField
        Inherits BoundField

        Private FRowIndex As Integer = 0

        ''' <summary>
        ''' 資料列是否為編輯模式。
        ''' </summary>
        ''' <param name="RowState">資料列狀態。</param>
        Public Function RowStateIsEdit(ByVal RowState As DataControlRowState) As Boolean
            Return (RowState And DataControlRowState.Edit) <> DataControlRowState.Normal
        End Function

        ''' <summary>
        ''' 資料列是否為新增模式。
        ''' </summary>
        ''' <param name="RowState">資料列狀態。</param>
        Public Function RowStateIsInsert(ByVal RowState As DataControlRowState) As Boolean
            Return (RowState And DataControlRowState.Insert) <> DataControlRowState.Normal
        End Function

        ''' <summary>
        ''' 資料列是否為編輯或新增模式。
        ''' </summary>
        ''' <param name="RowState">資料列狀態。</param>
        Public Function RowStateIsEditOrInsert(ByVal RowState As DataControlRowState) As Boolean
            Return RowStateIsEdit(RowState) OrElse RowStateIsInsert(RowState)
        End Function

        ''' <summary>
        ''' 判斷儲存格是否可編輯(新增/修改)。
        ''' </summary>
        ''' <param name="RowState">資料列狀態。</param>
        Friend Function CellIsEdit(ByVal RowState As DataControlRowState) As Boolean
            Return (Not Me.ReadOnly) AndAlso RowStateIsEditOrInsert(RowState)
        End Function

        ''' <summary>
        ''' 資料列索引。
        ''' </summary>
        Friend ReadOnly Property RowIndex() As Integer
            Get
                Return FRowIndex
            End Get
        End Property

        ''' <summary>
        ''' 儲存格初始化。
        ''' </summary>
        ''' <param name="Cell">要初始化的儲存格。</param>
        ''' <param name="CellType">儲存格類型。</param>
        ''' <param name="RowState">資料列狀態。</param>
        ''' <param name="RowIndex">資料列之以零起始的索引。</param>
        Public Overrides Sub InitializeCell(ByVal Cell As DataControlFieldCell, ByVal CellType As DataControlCellType, _
            ByVal RowState As DataControlRowState, ByVal RowIndex As Integer)

            FRowIndex = RowIndex
            MyBase.InitializeCell(Cell, CellType, RowState, RowIndex)
        End Sub

        ''' <summary>
        ''' 是否需要執行資料繫結。
        ''' </summary>
        ''' <param name="RowState">資料列狀態。</param>
        Friend Function RequiresDataBinding(ByVal RowState As DataControlRowState) As Boolean
            If MyBase.Visible AndAlso StrIsNotEmpty(MyBase.DataField) AndAlso RowStateIsEdit(RowState) Then
                Return True
            Else
                Return False
            End If
        End Function
    End Class

三、實作 TBDropDownField 欄位類別
step1. 繼承 TBBaseBoundField 類別
首先新增一個類別,繼承 TBBaseBoundField 命名為 TBDropDownFIeld 類別,覆寫 CreateField 方法,傳回 TBDropDownFIeld 物件。

    Public Class TBDropDownField
        Inherits TBBaseBoundField

        Protected Overrides Function CreateField() As System.Web.UI.WebControls.DataControlField
            Return New TBDropDownField()
        End Function
    End Class

[超過字數限制,下一篇接續本文]

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/24/5762.aspx

]]>
jeff377 2008-10-24 00:19:12
[ASP.NET 控制項實作 Day22] 讓 DropDownList 不再因項目清單不存在而造成錯誤(續) https://ithelp.ithome.com.tw/articles/10012915?sc=rss.iron https://ithelp.ithome.com.tw/articles/10012915?sc=rss.iron 接續上篇文章內容
三、解決 TBDropDownList 設定 DataSourceID 造成資料無法繫結的問題
要解決上述 TBDro...]]>
接續上篇文章內容
三、解決 TBDropDownList 設定 DataSourceID 造成資料無法繫結的問題
要解決上述 TBDropDownList 設定 DataSourceID 問題,需在設定 SelectedValue 屬性時,若 Items.Count=0 先用一個 FCachedSelectedValue 變數將正確的值先暫存下來,然後覆寫 PerformDataBinding 方法,當 DorpDownList 取得 DataSoruceID 所對應的項目清單內容後,因為這時 Items 的內容才會完整取回,再去設定一次 SelectedValue 屬性就可以正確的繫結資料。

    Public Class TBDropDownList
        Inherits DropDownList

        Private FCachedSelectedValue As String

        ''' <summary>
        ''' 覆寫 SelectedValue 屬性。
        ''' </summary>
        Public Overrides Property SelectedValue() As String
            Get
                Return MyBase.SelectedValue
            End Get
            Set(ByVal value As String)
                If Me.Items.Count <> 0 Then
                    Dim oItem As ListItem = Me.Items.FindByValue(value)
                    If (oItem Is Nothing) Then
                        Me.SelectedIndex = -1 '當 Items 不存在時 
                    Else
                        MyBase.SelectedValue = value
                    End If
                Else
                    FCachedSelectedValue = value
                End If
            End Set
        End Property

        Protected Overrides Sub PerformDataBinding(ByVal data As System.Collections.IEnumerable)
            MyBase.PerformDataBinding(data)

            'DataSoruceID 資料繫結後再設定 SelectedValue 屬性值
            If (Not FCachedSelectedValue Is Nothing) Then
                Me.SelectedValue = FCachedSelectedValue
            End If
        End Sub

    End Class

重新執行程式,切換到編輯模式時,TBDropDownList 就可以正確的繫結欄位值了。

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/22/5749.aspx

]]>
jeff377 2008-10-23 07:03:54
[ASP.NET 控制項實作 Day22] 讓 DropDownList 不再因項目清單不存在而造成錯誤 https://ithelp.ithome.com.tw/articles/10012909?sc=rss.iron https://ithelp.ithome.com.tw/articles/10012909?sc=rss.iron DropDownList 控制項常常會因為項目清單中不存在繫結的欄位,而發生以下的錯誤訊息。因為繫結資料的不完整或異常就會造成這樣的異常錯誤,在設計上實在是相當困擾,而且最麻煩的是這個錯誤在頁面...]]> DropDownList 控制項常常會因為項目清單中不存在繫結的欄位,而發生以下的錯誤訊息。因為繫結資料的不完整或異常就會造成這樣的異常錯誤,在設計上實在是相當困擾,而且最麻煩的是這個錯誤在頁面的程式碼也無法使用 Try ... Catch 方式來略過錯誤。其實最簡單的方式就去直接去修改 DropDownList 控制項,讓 DropDownList 控制項繫結資料時,就算欄位值不存在清單項目中也不要釋出錯誤,本文就要說明如何繼承 DorpDownList 下來修改,來有效解決這個問題。

程式碼下載:ASP.NET Server Control - Day22.rar
Northwnd 資料庫下載:NORTHWND.rar

一、覆寫 SelectedValue 屬性解決資料繫結的問題
DropDownList 控制項繫結錯誤的原因,可以由上圖的錯誤訊息可以大概得知是寫入 SelectedValue 屬性時發生的錯誤;所以我們繼承 DorpDownList 下來命名為 TBDropDownList,並覆寫 SelectedValue 屬性來解決這個問題。解決方式是在寫入 SelectedValue 屬性時,先判斷準備寫入的值是否存在項目清單中,存在的話才寫入 SelectedValue 屬性,若不存在則直接設定 SelectedIndex 屬性為 -1。

    Public Class TBDropDownList
        Inherits DropDownList

        ''' <summary>
        ''' 覆寫 SelectedValue 屬性。
        ''' </summary>
        Public Overrides Property SelectedValue() As String
            Get
                Return MyBase.SelectedValue
            End Get
            Set(ByVal value As String)
                Dim oItem As ListItem = Me.Items.FindByValue(value)
                If (oItem Is Nothing) Then
                    Me.SelectedIndex = -1 '當 Items 不存在時 
                    Exit Property
                Else
                    MyBase.SelectedValue = value
                End If
            End Set
        End Property

    End Class

我們以 Northwnd 資料庫的 Products 資料表做為測試資料,事先定義 DropDownList 的 Items 內容,其中第一個加入 "未對應" 的項目,將 SelectedValue 屬性繫結至 CategoryID 欄位。

                <bee:TBDropDownList ID="DropDownList1" runat="server" 
                    SelectedValue='<%# Bind("CategoryID") %>'>
                    <asp:ListItem Value="">未對應</asp:ListItem>
                    <asp:ListItem Value="2">Condiments</asp:ListItem>
                    <asp:ListItem Value="3">Confections</asp:ListItem>
                </bee:TBDropDownList>

當資料的 CategoryID 欄位值不存在於 DropDownList 的 Items 集合屬性中時,就會顯示第一個 "未對應" 的項目。

二、TBDropDownList 設定 DataSoruceID 產生的問題
上述的解決方法在筆者的「讓 DropDownList DataBind 不再發生錯誤」一文中已經有提及,不過有讀者發現另一個問題,就是當 DropDownList 設定 DataSourceID 時卻會發生資料無法正常繫結,以下就來解決這個問題。
我們設定 TBDropDownList 的 DataSoruceID 來取得項目清單的內容,將 DataSourceID 設定為另一個取得 Categories 資料表內容的 SqlDataSource 控制項。

                <bee:TBDropDownList ID="DropDownList1" runat="server" 
                    SelectedValue='<%# Bind("CategoryID") %>' DataSourceID="SqlDataSource2" 
                    DataTextField="CategoryName" DataValueField="CategoryID">
                </bee:TBDropDownList>
                <asp:SqlDataSource ID="SqlDataSource2" runat="server" 
                    ConnectionString="<%$ ConnectionStrings:Northwnd %>" 
                    SelectCommand="SELECT CategoryID, CategoryName, Description, Picture FROM Categories" 
                    ProviderName="<%$ ConnectionStrings:Northwnd.ProviderName %>" >
                </asp:SqlDataSource>

當執行程式時,FormView 原本在瀏覽模式時的 CategoryID 欄位值為 7 (CategoryName 應為 Product)。

當按下「編輯」時切換到 EditItemTemplate 時,改用 TBDropDownList 繫結 CategoryID 欄位值,可以這時欲無法繫結正確的值。

[超過字數限制,下一篇接續本文]

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/22/5749.aspx

]]>
jeff377 2008-10-23 06:59:56
[ASP.NET 控制項實作 Day21] 實作控制項智慧標籤(續) https://ithelp.ithome.com.tw/articles/10012897?sc=rss.iron https://ithelp.ithome.com.tw/articles/10012897?sc=rss.iron 接續 [ASP.NET 控制項實作 Day21] 實作控制項智慧標籤 一文
step2. 在智慧標籤面板加入屬性項目
DesignerA...]]>
接續 [ASP.NET 控制項實作 Day21] 實作控制項智慧標籤 一文
step2. 在智慧標籤面板加入屬性項目
DesignerActionPropertyItem 類別是設定智慧標籤面上的屬性項目,DesignerActionPropertyItem 建構函式的第一個參數(memberName) 為屬性名稱,這個屬性指的是 TBDateEditActionList 類別中的屬性,所以要在 TBDateEditActionList 新增一個對應的屬性。
例如在智慧標籤中加入 AutoPostBack 屬性項目,則在 TBDateEditActionList 類別需有一個對應 AutoPostBack 屬性。

            oItems.Add(New DesignerActionPropertyItem("AutoPostBack", _
                "AutoPostBack", "Behavior", "是否引發 PostBack 動作。"))

TBDateEditActionList.AutoPostBack 屬性如下,其中 Me.Component 指的是目前的 TDateEdit 控制項,透過 GetPropertyValue 及 SetPropertyValue 方法來存取控制項的指定屬性。

        ''' <summary>
        ''' 是否引發 PostBack 動作。
        ''' </summary>
        Public Property AutoPostBack() As Boolean
            Get
                Return CType(GetPropertyValue(Me.Component, "AutoPostBack"), Boolean)
            End Get
            Set(ByVal value As Boolean)
                SetPropertyValue(Me.Component, "AutoPostBack", value)
            End Set
        End Property

    ''' <summary>
    ''' 設定物件的屬性值。
    ''' </summary>
    ''' <param name="Component">屬性值將要設定的物件。</param>
    ''' <param name="PropertyName">屬性名稱。</param>
    ''' <param name="Value">新值。</param>
    Public Shared Sub SetPropertyValue(ByVal Component As Object, ByVal PropertyName As String, ByVal Value As Object)
        Dim Prop As PropertyDescriptor = TypeDescriptor.GetProperties(Component).Item(PropertyName)
        Prop.SetValue(Component, Value)
    End Sub

    ''' <summary>
    ''' 取得物件的屬性值。
    ''' </summary>
    ''' <param name="Component">具有要擷取屬性的物件。</param>
    ''' <param name="PropertyName">屬性名稱。</param>
    Public Shared Function GetPropertyValue(ByVal Component As Object, ByVal PropertyName As String) As Object
        Dim Prop As PropertyDescriptor = TypeDescriptor.GetProperties(Component).Item(PropertyName)
        Return Prop.GetValue(Component)
    End Function

step3. 在智慧標籤面板加入方法項目
DesignerActionMethodItem 類別是設定智慧標籤面上的方法項目,DesignerActionPropertyItem 建構函式的第二個參數(memberName) 為方法名稱,這個方法指的是 TBDateEditActionList 類別中的方法,所以要在 TBDateEditActionList 新增一個對應的方法。
例如在智慧標籤中加入 About 方法項目,則在 TBDateEditActionList 類別需有一個對應 About 方法。

            oItems.Add(New DesignerActionMethodItem(Me, "About", _
                "關於 TDateEdit 控制項", "About", _
                "關於 TDateEdit 控制項。", True))

TBDateEditActionList 的 About 方法只是單純顯示一個訊息視窗,一般你可以在這方法加入任何想在設計階段處理的動作。例如自動產生 GridView 的欄位、在 FormView 加入控制項並自動排版,這些都可以在此實現的。

        Public Sub About()
            MsgBox("TDateEdit 是結合 The Coolest DHTML Calendar 日期選擇器實作的控制項")
        End Sub

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/22/5749.aspx

]]>
jeff377 2008-10-22 18:02:28
[ASP.NET 控制項實作 Day21] 實作控制項智慧標籤 https://ithelp.ithome.com.tw/articles/10012896?sc=rss.iron https://ithelp.ithome.com.tw/articles/10012896?sc=rss.iron 控制項通常會把常用屬性或功能顯示在智慧標籤中,提供使用者更簡便的快速設定,例如下圖為 GridView 的智慧。若要製作控制項的智慧標籤,需實作控制項的 ActionList 加入智慧標籤中要顯...]]> 控制項通常會把常用屬性或功能顯示在智慧標籤中,提供使用者更簡便的快速設定,例如下圖為 GridView 的智慧。若要製作控制項的智慧標籤,需實作控制項的 ActionList 加入智慧標籤中要顯示的項目,在本文將以 TDateEdit 控制項為例,進一步說明控制項的智慧標籤的實作方式。

程式碼下載:ASP.NET Server Control - Day21.rar

一、TDateEdit 控制項介紹
TDateEdit 控制項是筆者之前在部落格中實作的一個日期控制項,如下圖所示。它是結合 JavaScript 的 The Coolest DHTML Calendar 日期選擇器實作的控制項,我已將 TDateEdit 控制項的相關程式碼含入 Bee.Web.dll 組件中。TDateEdit 控制項的相關細節可以參考筆者部落格下面幾篇文章有進一步說明,本文將以 TDateEdit 控制項為例,只針對實作智慧標籤的部分做進一步說明。
日期控制項實作教學(1) - 結合 JavaScript
日期控制項實作教學(2) - PostBack 與 事件
TBDateEdit 日期控制項 - 1.0.0.0 版 (Open Source)

二、控制項加入智慧標籤
控制項要加入智慧標籤要實作控制項的 Designer,我們繼承 ControlDesigner 命名為 TBDateEditDesigner,然後覆寫 ActionLists 屬性,此屬性即是傳回智慧標籤中所包含的項目清單集合。在 ActionLists 屬性中一般會先加入父類別的 ActionLists 屬性,再加入自訂的 ActionList 類別,這樣才可以保留原父類別中智慧標籤的項目清單。

    ''' <summary>
    ''' TBDateEdit 控制項的設計模式行為。
    ''' </summary>
    Public Class TBDateEditDesigner
        Inherits System.Web.UI.Design.ControlDesigner

        ''' <summary>
        ''' 取得控制項設計工具的動作清單集合。
        ''' </summary>
        Public Overrides ReadOnly Property ActionLists() As DesignerActionListCollection
            Get
                Dim oActionLists As New DesignerActionListCollection()
                oActionLists.AddRange(MyBase.ActionLists)
                oActionLists.Add(New TBDateEditActionList(Me))
                Return oActionLists
            End Get
        End Property

    End Class

我們自訂的 ActionList 為 TBDateEditActionList 類別,它在智慧標籤呈現的項目清單如下圖所示,接下去我們會說明 TBDateEditActionList 類別的內容。

三、自訂智慧標籤面板的項目清單集合
DesignerActionList 類別定義用於建立智慧標籤面板的項目清單的基底類別,所以我們首先繼承 DesignerActionList 命名為 TBDateEditActionList。

    ''' <summary>
    ''' 定義 TBDateEdit 控制項智慧標籤面板的項目清單集合。
    ''' </summary>
    Public Class TBDateEditActionList
        Inherits DesignerActionList

        ''' <summary>
        ''' 建構函式。
        ''' </summary>
        Public Sub New(ByVal owner As ControlDesigner)
            MyBase.New(owner.Component)
        End Sub

    End Class

接下來要覆寫 GetSortedActionItems 方法,它會回傳 DesignerActionItemCollection 集合型別,此集合中會傳回要顯示在智慧標籤面板的項目清單集合,所以我們要在 DesignerActionItemCollection 集合中加入我們要呈現的項目清單內容。

        ''' <summary>
        ''' 傳回要顯示在智慧標籤面板的項目清單集合。
        ''' </summary>
        Public Overrides Function GetSortedActionItems() As System.ComponentModel.Design.DesignerActionItemCollection
            Dim oItems As New DesignerActionItemCollection()

            '在此加入智慧標籤面板的項目清單	           

            Return oItems
        End Function

step1. 在智慧標籤面板加入靜態標題項目
首先介紹 DesignerActionHeaderItem 類別,它是設定靜態標題項目,例如我們在 TDateEdit 的智慧標籤中加入「行為」、「外觀」二個標題項目,其中 DesignerActionHeaderItem 建構函式的 category 參數是群組名稱,我們可以將相關的項目歸類到同一個群組。

Dim oItems As New DesignerActionItemCollection()

oItems.Add(New DesignerActionHeaderItem("行為", "Behavior"))
oItems.Add(New DesignerActionHeaderItem("外觀", "Appearance"))

[超過字數限制,下一篇接續本文]

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/22/5749.aspx

]]>
jeff377 2008-10-22 18:01:29
[ASP.NET 控制項實作 Day20] 偵錯設計階段的程式碼 https://ithelp.ithome.com.tw/articles/10012807?sc=rss.iron https://ithelp.ithome.com.tw/articles/10012807?sc=rss.iron 上篇我們介紹了自訂 Designer 來輸出控制項設計階段的 HTML 碼,可是若你去對針 Designer 的程式碼下中斷點,你會發覺根本無法偵錯。因為程式在執行階段時期,根本不會執行 Des...]]> 上篇我們介紹了自訂 Designer 來輸出控制項設計階段的 HTML 碼,可是若你去對針 Designer 的程式碼下中斷點,你會發覺根本無法偵錯。因為程式在執行階段時期,根本不會執行 Designer 相關類別,所以你在 Designer 類別中下的中斷點完全無效;當然不可能這樣寫程式碼而用感覺去偵錯,本文將告訴你如何去偵錯設計階段的程式碼。
一、設計階段程式碼的錯誤
如果撰寫 Designer、Editor、ActionList 等設計階段的程式碼,當這些設計階段的程式碼發生錯誤,可能會發生設計頁面中控制項的錯誤情形,如下圖所示。因為控制項專案本身非啟動專案,在測試網站的設計頁面若控制項發生異常時會直接釋出錯誤,無法偵錯設計階段的程式碼;若真得要偵錯誤設計階段的問題,就要使用另一個 VS2008 來偵錯。

二、設定起始外部程式
要偵錯控制項設計階段的程式碼,要先將控制項專案(Bee.Web)設定為啟時專案。然後設定控制項專案的「屬性」,在「偵錯」頁籤中的起始動作選擇「起始外部程式」,選擇 VS2008 的執行檔位置,預設為 C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\devenv.exe。

三、開始偵錯設計階段程式碼
step1. 控制項專案開始偵錯
在設計階要偵錯的程式碼下中斷點,在控制項專案按下 F5 開始偵錯,這時會啟動另一個新的 VS2008 執行檔。

step2. 在新的 VS2008 的工具箱加入控制項
在新的 VS2008 中新增一個測試網站,在工具箱按右鍵執行「選擇項目」開啟「選擇工具箱項目」視窗,然後按「瀏覽」鈕按選擇控制項組件(Bee.Web.dll),將要偵錯的控制項加入工具箱中。


step3. 將控制項拖曳至頁面做設計動作
在新的 VS2008 中,將控制項拖曳至頁面,就會開始執行設計階段的程式碼,特定的設計動作就會執行到相對的設計階段程式碼,當執行到之前下的中斷點時就可以開始偵錯了。

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/21/5741.aspx

]]>
jeff377 2008-10-21 00:28:45
[ASP.NET 控制項實作 Day19] 控制項設計階段的外觀 https://ithelp.ithome.com.tw/articles/10012682?sc=rss.iron https://ithelp.ithome.com.tw/articles/10012682?sc=rss.iron 有一些控制項在執行階段是不會呈現,也就是說控制項本身在執行階段不會 Render 出 HTML 碼,例如 SqlDataSoruce、ScriptManager 這類控制項;那它們在設計階段的頁...]]> 有一些控制項在執行階段是不會呈現,也就是說控制項本身在執行階段不會 Render 出 HTML 碼,例如 SqlDataSoruce、ScriptManager 這類控制項;那它們在設計階段的頁面是如何呈現出來呢?本文將針對控制項設計階段的外觀做進一步的說明。
程式碼下載:ASP.NET Server Control - Day19.rar
一、控制項設計階段的 HTML 碼
Web 伺服器控制項的設計模式行為都是透過 ControlDesigner 來處理,連設計階段時控制項的外觀也是如此;控制項在設計階段與執行執行時呈現的外觀不一定相同,當然大部分會儘量一致,使其能所見即所得。
控制項在設計階段的 HTML 碼是透 ControlDesigner.GetDesignTimeHtml 方法來處理,在 ControlDesigner.GetDesignTimeHtml 預設會執行控制項的 RenderControl 方法,所以大部分的情況下設計階段與執行階段輸出的 HTML 碼會相同。當控制項的 Visible=False 時,執行階段是完全不會輸出 HTML 碼,可是在設計階段時會特別將控制項設定 Visible=True,使控制項能完整呈現。

ControlDesigner.GetDesignTimeHtml 方法

Public Overridable Function GetDesignTimeHtml() As String
    Dim writer As New StringWriter(CultureInfo.InvariantCulture)
    Dim writer2 As New DesignTimeHtmlTextWriter(writer)
    Dim errorDesignTimeHtml As String = Nothing
    Dim flag As Boolean = False
    Dim visible As Boolean = True
    Dim viewControl As Control = Nothing
    Try 
        viewControl = Me.ViewControl
        visible = viewControl.Visible
        If Not visible Then
            viewControl.Visible = True
            flag = Not Me.UsePreviewControl
        End If
        viewControl.RenderControl(writer2)
        errorDesignTimeHtml = writer.ToString
    Catch exception As Exception
        errorDesignTimeHtml = Me.GetErrorDesignTimeHtml(exception)
    Finally
        If flag Then
            viewControl.Visible = visible
        End If
    End Try
    If ((Not errorDesignTimeHtml Is Nothing) AndAlso (errorDesignTimeHtml.Length <> 0)) Then
        Return errorDesignTimeHtml
    End If
    Return Me.GetEmptyDesignTimeHtml
End Function

二、自訂控制項的 Designer
以 TBToolbar 為例,若我們在 RenderContents 方法未針對 Items.Count=0 做輸出 HTML 的處理,會發現未設定 Items 屬性時,在設計頁面上完全看不到 TBToolbar 控制項;像這種控制項設計階段的 HTML 碼,就可以自訂控制項的 Designer 來處理。

繼承 ControlDesigner 命名為 TBToolbarDesigner,這個類別是用來擴充 TBToolbar 控制項的設計模式行為。我們可以覆寫 GetDesignTimeHtml 方法,處理設計階段表示控制項的 HTML 標記,此方法回傳的 HTML 原始碼就是控制項呈現在設計頁面的外觀。所以我們可以在 TBToolbar.Items.Count=0 時,輸出一段提示的 HTML 碼,這樣當 TBToolbar 未設定 Items 屬性時一樣可以在設計頁面上呈現控制項。

    ''' <summary>
    ''' 擴充 TBToolbar 控制項的設計模式行為。
    ''' </summary>
    Public Class TBToolbarDesigner
        Inherits System.Web.UI.Design.ControlDesigner

        ''' <summary>
        ''' 用來在設計階段表示控制項的 HTML 標記。
        ''' </summary>
        Public Overrides Function GetDesignTimeHtml() As String
            Dim sHTML As String
            Dim oControl As TBToolbar

            oControl = CType(ViewControl, TBToolbar)
            If oControl.Items.Count = 0 Then
                sHTML = "<div style=""background-color: #C0C0C0; border:solid 1px; width:200px"">請設定 Items 屬性</div>"
            Else
                sHTML = MyBase.GetDesignTimeHtml()
            End If
            Return sHTML
        End Function

    End Class

在 TBToolbar 控制項套用 DesignerAttribute 設定自訂的 TBToolbarDesigner 類別。

    <Designer(GetType(TBToolbarDesigner))> _
    Public Class TBToolbar
        Inherits WebControl

    End Class

重建控制項組件,切換到設計頁面上的看 TBToolbar 控制項未設定 Items 屬性時的外觀,就是我們在 TBToolbarDesigner.GetDesignTimeHtml 方法回傳的 HTML 碼。

如果你覺得上述設計階段的控制項有點太陽春,我們也可以輸出類似 SqlDataSource 控制項的外觀,將未設定 Items 屬性時輸出 HTML 改呼叫 CreatePlaceHolderDesignTimeHtml 方法。

            If oControl.Items.Count = 0 Then
                sHTML = MyBase.CreatePlaceHolderDesignTimeHtml("請設定 Items 屬性")
            Else
                sHTML = MyBase.GetDesignTimeHtml()
            End If

來看一下這樣修改後的結果,是不是比較專業一點了呢。

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/20/5726.aspx

]]>
jeff377 2008-10-20 02:25:29
[ASP.NET 控制項實作 Day18] 修改集合屬性編輯器 https://ithelp.ithome.com.tw/articles/10012636?sc=rss.iron https://ithelp.ithome.com.tw/articles/10012636?sc=rss.iron 上篇我們實作了「集合屬性包含不同型別的成員」,不過若有去使用屬性視窗編輯 TBToolbar 的 Items 屬性,你會發覺這個集合屬性編輯器無法加入我們定義不同型別的成員,只能加入最原始的集合...]]> 上篇我們實作了「集合屬性包含不同型別的成員」,不過若有去使用屬性視窗編輯 TBToolbar 的 Items 屬性,你會發覺這個集合屬性編輯器無法加入我們定義不同型別的成員,只能加入最原始的集合成員。是不是只能在 aspx 程式碼中手動去輸入呢?當然不需要這樣人工作業,只要改掉集合屬性編輯器就可以達到我們的需求,本文將介紹修改集合屬性編輯器的相關作法。
程式碼下載:ASP.NET Server Control - Day18.rar

一、自訂集合屬性編輯器
我們先看一下 TBToolbar.Items 屬性套用的 EditorAttribute,它是使用 CollectionEditor 類別來當作屬性編輯器,所以我們就是要繼承 CollectionEditor 類別下來修改成自訂的屬性編輯器。

< _
Editor(GetType(CollectionEditor), GetType(UITypeEditor)) _
> _
Public ReadOnly Property Items() As TBToolbarItemCollection

新增一個繼承 CollectionEditor 的 TBToolbarItemCollectionEditor 類別,並加入建構函式。此類別屬於 Bee.WebControls.Design 命名空間,通常我們會把設計階段使用的類別歸類到特別的命名空間便於管理及使用。

Namespace WebControls.Design
    Public Class TBToolbarItemCollectionEditor
        Inherits CollectionEditor

        ''' <summary>
        ''' 建構函式。
        ''' </summary>
        ''' <param name="Type">型別。</param>
        Public Sub New(ByVal Type As Type)
            MyBase.New(Type)
        End Sub

    End Class
End Namespace

我們可以先修改 Items 屬性的 EditorAttribute,看看我們自訂的 TBToolbarItemCollectionEditor 是否能正常運作。不過這個屬性編輯器跟原本的沒什麼差異,因為我們只是單純繼承下來沒做任何異動,接下去我們就要開始來修改這個屬性編輯器。

< _
Editor(GetType(TBToolbarItemCollectionEditor), GetType(UITypeEditor)) _
> _
Public ReadOnly Property Items() As TBToolbarItemCollection

二、加入不同型別的集合成員
再來我們就要著手修改集合屬性編輯器,讓它可以加入不同型別的集合成員。覆寫 CollectionEditor 的 CanSelectMultipleInstances 方法傳回 True,這個方法是設定 CollectionEditor 是否允許加入多種不同型別的集合成員。

        Protected Overrides Function CanSelectMultipleInstances() As Boolean
            Return True
        End Function

再來覆寫 CreateNewItemTypes 方法,這個方法是取得這個集合編輯器可包含的資料型別,將集合可包含的資料型別以陣列傳回。

        ''' <summary>
        ''' 取得這個集合編輯器可包含的資料型別。
        ''' </summary>
        ''' <returns>這個集合可包含的資料型別陣列。</returns>
        Protected Overrides Function CreateNewItemTypes() As System.Type()
            Dim ItemTypes(2) As System.Type
            ItemTypes(0) = GetType(TBToolbarButton)
            ItemTypes(1) = GetType(TBToolbarTextbox)
            ItemTypes(2) = GetType(TBToolbarLabel)
            Return ItemTypes
        End Function

重建控制項組件,使用 Items 的集合屬性編輯器,就可以發現「加入」鈕的下拉清單就會出現我們所定義的三種型別的集合成員,如此可以加入不同型別的成員了。

三、設定清單項目的顯示文字
在成員清單項目中預設會顯示成員含命名空間的型別,若我們要修改成比較有識別的顯示文字,例如 TBToolbarButton(Key=Add) 可以顯示「按鈕-Add」,這時可以覆寫 GetDisplayText 方法來設定清單項目的顯示文字。

        ''' <summary>
        ''' 取出指定清單項目的顯示文字。
        ''' </summary>
        Protected Overrides Function GetDisplayText(ByVal value As Object) As String
            If TypeOf value Is TBToolbarButton Then
                Return String.Format("按鈕 - {0}", CType(value, TBToolbarButton).Key)
            ElseIf TypeOf value Is TBToolbarTextbox Then
                Return "文字框"
            ElseIf TypeOf value Is TBToolbarLabel Then
                Return String.Format("標籤 - {0}", CType(value, TBToolbarLabel).Text)
            Else
                Return value.GetType.Name
            End If
        End Function

四、集合編輯器的屬性視窗的屬性描述
一般屬性視窗下面都會有屬性描述,可以集合屬性編輯器中的屬性視窗下面竟沒有屬性描述。若我們要讓它的屬性描述可以顯示,可以覆寫 CreateCollectionForm 方法,取得集合屬性編輯表單,再去設定表單上的 PropertyGrid.HelpVisible
= True 即可。

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/19/5721.aspx

]]>
jeff377 2008-10-19 00:13:21
[ASP.NET 控制項實作 Day17] 集合屬性包含不同型別的成員 https://ithelp.ithome.com.tw/articles/10012600?sc=rss.iron https://ithelp.ithome.com.tw/articles/10012600?sc=rss.iron 我們知道在 GridView 的 Columns 集合屬性中,可以包含不同型別的欄位,如 BoundFIeld、CheckBoxField、HyperLinkField ...等不同型別的欄位。...]]> 我們知道在 GridView 的 Columns 集合屬性中,可以包含不同型別的欄位,如 BoundFIeld、CheckBoxField、HyperLinkField ...等不同型別的欄位。如果我們希望工具列中不只包含按鈕,可以包含其他不同類型的子控制項,那該怎麼做呢?本文就以上篇中的 TBToolbar 控制項為案例,讓 Items 集合屬性可以加入 Button、TextBox、Label ...等不同的子控制項。
程式碼下載:ASP.NET Server Control - Day17.rar
一、不同型別的集合成員
我們的需求是讓工具列可以加入 Button、TextBox、Label 三種子控制項,所以繼承原來的 TBToolbarItem (只保留 Enabled 屬性),新增了 TBToolbarButton、TBToolbarTextbox、TBToolbarLabel 三個類別。

這些新增的成員類別都是繼承至 TBToolbarItem,所以在 aspx 程式碼中,手動輸入 Items 的成員時,就會列出這幾種定義的成員型別。

二、建立不同型別集合成員的子控制項
因為 Items 屬性的成員具不同型別,所以我們要改寫 RenderContents 方法,判斷成員型別來建立對應類型的子控制項。若為 TBToolbarButton 型別建立 Button 控制項、若為 TBToolbarTextbox 型別則建立 TextBox 控制項、若為 TBToolbarLabel 型別則建立 Label 控制項。其中 TBToolbarButton 建立的控制項為 TBButton,這個控制項是我們在「 [ASP.NET 控制項實作 Day3] 擴展現有伺服器控制項功能」一文中實作的具詢問訊息的按鈕控制項。

        ''' <summary>
        ''' 覆寫 RenderContents 方法。
        ''' </summary>
        Protected Overrides Sub RenderContents(ByVal writer As System.Web.UI.HtmlTextWriter)
            Dim oItem As TBToolbarItem
            Dim oControl As Control

            For Each oItem In Me.Items
                If TypeOf oItem Is TBToolbarButton Then
                    '建立 Button 控制項
                    oControl = CreateToolbarButton(CType(oItem, TBToolbarButton))
                ElseIf TypeOf oItem Is TBToolbarTextbox Then
                    '建立 Textbox 控制項
                    oControl = CreateToolbarTextbox(CType(oItem, TBToolbarTextbox))
                Else
                    '建立 Label 控制項
                    oControl = CreateToolbarLabel(CType(oItem, TBToolbarLabel))
                End If
                Me.Controls.Add(oControl)
            Next

            MyBase.RenderContents(writer)
        End Sub

        ''' <summary>
        ''' 建立工具列按鈕。
        ''' </summary>
        Private Function CreateToolbarButton(ByVal Item As TBToolbarButton) As Control
            Dim oButton As TBButton
            Dim sScript As String

            oButton = New TBButton()
            oButton.Text = Item.Text
            oButton.Enabled = Item.Enabled
            oButton.ID = Item.Key
            oButton.ConfirmMessage = Item.ConfirmMessage
            sScript = Me.Page.ClientScript.GetPostBackEventReference(Me, Item.Key)
            oButton.OnClientClick = sScript

            Return oButton
        End Function

        ''' <summary>
        ''' 建立工具列文字框。
        ''' </summary>
        Private Function CreateToolbarTextbox(ByVal Item As TBToolbarTextbox) As Control
            Dim oTextBox As TextBox

            oTextBox = New TextBox
            Return oTextBox
        End Function

        ''' <summary>
        ''' 建立工具列標籤。
        ''' </summary>
        Private Function CreateToolbarLabel(ByVal Item As TBToolbarLabel) As Control
            Dim oLabel As Label

            oLabel = New Label()
            oLabel.Text = Item.Text
            Return oLabel
        End Function

我們手動在 aspx 程式碼中輸入不同型別的成員,TBToolbar 控制項就會呈現對應的子控制項。

三、執行程式
執行程式,就可以在瀏覽器看到呈現的工具列,當按下「刪除」時也會出現我們定義的詢問訊息。

輸出的 HTML 碼如下

<span id="TBToolbar1">
<input type="submit" name="TBToolbar1$Add" value="新增" onclick="__doPostBack('TBToolbar1','Add');" id="TBToolbar1_Add" />
<input type="submit" name="TBToolbar1$Edit" value="修改" onclick="__doPostBack('TBToolbar1','Edit');" id="TBToolbar1_Edit" />
<input type="submit" name="TBToolbar1$Delete" value="刪除" onclick="if (confirm('確定刪除嗎?')==false) {return false;}__doPostBack('TBToolbar1','Delete');" id="TBToolbar1_Delete" />
<span>關鍵字</span>
<input name="TBToolbar1$ctl01" type="text" />
<input type="submit" name="TBToolbar1$Search" value="搜尋" onclick="__doPostBack('TBToolbar1','Search');" id="TBToolbar1_Search" />
</span>

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/18/5718.aspx

]]>
jeff377 2008-10-18 00:05:57
[ASP.NET 控制項實作 Day16] 繼承 WebControl 實作 Toolbar 控制項 https://ithelp.ithome.com.tw/articles/10012507?sc=rss.iron https://ithelp.ithome.com.tw/articles/10012507?sc=rss.iron 前面我們討論過「繼承 CompositeControl 實作 Toolbar 控制項」,本文將繼承 WebControl 來實作同樣功能的 Toolbar 控制項,用不同的方式來實作同一個控制項...]]> 前面我們討論過「繼承 CompositeControl 實作 Toolbar 控制項」,本文將繼承 WebControl 來實作同樣功能的 Toolbar 控制項,用不同的方式來實作同一個控制項,進而比較二者之間的差異。
程式碼下載:ASP.NET Server Control - Day16.rar

一、繼承 WebControl 實作 TBToolbar 控制項
step1. 新增繼承 WebControl 的 TBToolbar 控制項
新增繼承 WebControl 的 TBToolbar 控制項,你也可以直接原修改原 TBToolbar 控制項,繼承對象由 CompositeControl 更改為 WebControl即可。跟之前一樣在 TBToolbar 控制項加入 Items 屬性及 Click 事件。
另外 TBToolbar 控制項需實作 INamingContainer 界面,此界面很特殊沒有任何屬性或方法,INamingContainer 界面的作用是子控制項的 ClientID 會在前面加上父控制項的 ClickID,使每個子控制項有唯一的 ClientID。

step2. 建立工具列按鈕集合
覆寫 RenderContents 方法,將原本 TBToolbar (複合控制項) 的 CreateChildControls 方法中建立工具列按鈕程式碼,搬移至 RenderContents 方法即可。

        Private Sub ButtonClickEventHandler(ByVal sender As Object, ByVal e As EventArgs)
            Dim oButton As Button
            Dim oEventArgs As ClickEventArgs

            oButton = CType(sender, Button)
            oEventArgs = New ClickEventArgs()
            oEventArgs.Key = oButton.ID
            OnClick(oEventArgs)
        End Sub

        ''' <summary>
        ''' 覆寫 RenderContents 方法。
        ''' </summary>
        Protected Overrides Sub RenderContents(ByVal writer As System.Web.UI.HtmlTextWriter)
            Dim oItem As TBToolbarItem
            Dim oButton As Button

            For Each oItem In Me.Items
                oButton = New Button()
                oButton.Text = oItem.Text
                oButton.Enabled = oItem.Enabled
                oButton.ID = oItem.Key
                AddHandler oButton.Click, AddressOf ButtonClickEventHandler
                Me.Controls.Add(oButton)
            Next

            If Me.Items.Count = 0 AndAlso Me.DesignMode Then
                oButton = New Button()
                oButton.Text = "請設定 Items 屬性。"
                Me.Controls.Add(oButton)
            End If

            MyBase.RenderContents(writer)
        End Sub

上述的直接搬移過來的程式碼還有個問題,就是原來的使用 AddHandler 來處理按鈕事件的方式變成沒有作用了?因為現在不是複合式控制項,當前端的按鈕 PostBack 傳回伺服端時,TBToolbar 不會事先建立子控制槓,所以機制會找不到原來產生的按鈕,也就無法使用 AddHandler 來處理事件了。

AddHandler oButton.Click, AddressOf ButtonClickEventHandler

step3. 處理 Click 事件
因為不能使用 AddHandler 來處理按鈕事件,所以我們就自行使用 Page.ClientScript.GetPostBackEventReference 方法來產生 PostBack 動作的用戶端指令碼,按鈕的 OnClientClick 去執行 PostBack 的動作。

            For Each oItem In Me.Items
                oButton = New Button()
                oButton.Text = oItem.Text
                oButton.Enabled = oItem.Enabled
                oButton.ID = oItem.Key
                sScript = Me.Page.ClientScript.GetPostBackEventReference(Me, oItem.Key)
                oButton.OnClientClick = sScript
                Me.Controls.Add(oButton)
            Next

TBToolar 控制項輸出的 HTML 碼如下

<span id="TBToolbar1">
<input type="submit" name="TBToolbar1$Add" value="新增" onclick="__doPostBack('TBToolbar1','Add');" 

id="TBToolbar1_Add" />
<input type="submit" name="TBToolbar1$Edit" value="修改" onclick="__doPostBack('TBToolbar1','Edit');" 

id="TBToolbar1_Edit" />
<input type="submit" name="TBToolbar1$Delete" value="刪除" onclick="__doPostBack('TBToolbar1','Delete');" 

id="TBToolbar1_Delete" />
</span>

要自行處理 PostBack 的事件,需實作 IPostBackEventHandler 介面,在 RaisePostBackEvent 方法來引發 TBToolbar 的 Click 事件。

    Public Class TBToolbar
        Inherits WebControl
        Implements INamingContainer
        Implements IPostBackEventHandler

        Public Sub RaisePostBackEvent(ByVal eventArgument As String) Implements 

System.Web.UI.IPostBackEventHandler.RaisePostBackEvent
            Dim oEventArgs As ClickEventArgs

            oEventArgs = New ClickEventArgs()
            oEventArgs.Key = eventArgument
            Me.OnClick(oEventArgs)
        End Sub

    End Class

二、測試程式
在測試頁面上放置 TBToolbar 控制項,在 Click 事件撰寫測試程式碼。

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/17/5706.aspx

]]>
jeff377 2008-10-17 00:05:40
[ASP.NET 控制項實作 Day15] 複合控制項隱藏的問題 https://ithelp.ithome.com.tw/articles/10012425?sc=rss.iron https://ithelp.ithome.com.tw/articles/10012425?sc=rss.iron 上一篇我們使用複合控制項(繼承 CompositeControl)的方式來實作 TBToolbar 控制項,本文將針對複合控制項做一些測試,說明在使用複合控制項要注意的一些問題。
程...]]>
上一篇我們使用複合控制項(繼承 CompositeControl)的方式來實作 TBToolbar 控制項,本文將針對複合控制項做一些測試,說明在使用複合控制項要注意的一些問題。
程式碼下載:ASP.NET Server Control - Day15.rar
一、複合控制項建立子控制項的時機
還記得我們之前介紹複合控制項時有談到 CompositeControl 類別會確保我們存取子控制項時,它的子控制項一定會事先建立;也就是當我們使用 Controls 屬性去存取子控制項時,一定會執行 CreateChildControls 方法,以確保子控制項事先被建立。我們看一下 CompositeControl 類別的 Controls 屬性的寫法就可以了解其中的原由,在存取 CompositeControl.Controls 屬性時,它會先執行 Control.EnsureChildControls 方法;而 EnsureChildControls 方法會去判斷子控制項是否已建立,若未建立會去執行 CreateChildControls 方法,這也就是為什麼 CompositeControl 有辨法確保子控制項事先被建立的原因。

CompositeControl.Controls 屬性如下

Public Overrides ReadOnly Property Controls As ControlCollection
    Get
        Me.EnsureChildControls
        Return MyBase.Controls
    End Get
End Property

Control.EnsureChildControls 方法如下

Protected Overridable Sub EnsureChildControls()
    If (Not Me.ChildControlsCreated AndAlso Not Me.flags.Item(&H100)) Then
        Me.flags.Set(&H100)
        Try 
            Me.ResolveAdapter
            If (Not Me._adapter Is Nothing) Then
                Me._adapter.CreateChildControls
            Else
                Me.CreateChildControls
            End If
            Me.ChildControlsCreated = True
        Finally
            Me.flags.Clear(&H100)
        End Try
    End If
End Sub

二、複合控制項隱藏的問題
我們以上篇的 TBToolbar 控制項為例,撰寫一些測試案例來說明複合控制項的問題。在撰寫測試案例之前,我們先修改一下 TBToolbar 控制項,覆寫 LoadViewState 及 SaveViewState 方法,將 Items 屬性儲存於 ViewState 中以維持狀態。

在測試頁面上放置「測試一」、「測試二」、「PostBack」三個按鈕,這三個按鈕的動作如下。
「測試一」按鈕:在工具列直接新增一個按鈕。
「測試二」按鈕:先使用 FindControl 取得工具列的按鈕,然後在在工具列再新增一個按鈕。
「PostBack」按鈕:單純執行 PostBack,不撰寫程式碼。

三個按鈕的程式碼如下所示。

    Protected Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles 

Button1.Click
        Dim oItem As TBToolbarItem

        '加入新按鈕
        oItem = New TBToolbarItem()
        oItem.Text = "新按鈕"
        oItem.Key = "NewButton"
        TBToolbar1.Items.Add(oItem)
        Me.Response.Write("「測試一」按鈕")
    End Sub

    Protected Sub Button2_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles 

Button2.Click
        Dim oItem As TBToolbarItem
        Dim oButton As Button

        '先執行 FindControl 去取得 ID="Add" 的按鈕
        oButton = TBToolbar1.FindControl("Add")

        '再加入新按鈕
        oItem = New TBToolbarItem()
        oItem.Text = "新按鈕"
        oItem.Key = "NewButton"
        TBToolbar1.Items.Add(oItem)
        Me.Response.Write("「測試二」按鈕")
    End Sub

    Protected Sub Button3_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles 

Button3.Click
        '單純 PostBack,無程式碼
        Me.Response.Write("「PostBack」按鈕")
    End Sub

案例一:執行「測試一」按鈕,在工具列直接新增一個按鈕。
當按下「測試一」按鈕時,工具列可以正常加入我們新增的按鈕。

案例二:執行「測試二」按鈕,先使用 FindControl 取得工具列的按鈕,然後在在工具列再新增一個按鈕。
重新執行程式,當按下「測試二」按鈕時,你會發現奇怪的現象,工具列竟然沒有加入我們新增的按鈕?

此時再按下「PostBack」按鈕,工具列才會出現我們剛剛加入的按鈕。

為什麼會發生這種怪現象呢?其實原因很簡單,因為 FindControl 時會去存取 Controls 屬性,而這時子控制項已經被建立了;而之前再用 Items 屬性加入新按鈕,它已經不會在重建子控制項,導致第一時間沒有加入新按鈕。不過 Items 屬性會被存在 ViewState 中,所以當執行「PostBack」按鈕時,就會出現我們剛剛新增的按鈕。

三、解決方式
要解決上述「測試二」的問題,只要覆寫 TBToolbar 控制項的 Render 方法,在 Render 前執行 RecreateChildControls 方法,強制重建子控制項。

        ''' <summary>
        ''' 覆寫 Render 方法。
        ''' </summary>
        Protected Overrides Sub Render(ByVal writer As System.Web.UI.HtmlTextWriter)
            Me.RecreateChildControls()
            MyBase.Render(writer)
        End Sub

再一次執行「測試二」的動作,就會發現執行結果就會正常了。

四、結語
在複合控制項的 Render 前執行 RecreateChildControls 方法可以強制重建子控制項,可是這樣又會引發另一個問題,那就是當直接存取子控制項去修改子控制項的屬性後,一旦在 Render 又重建子控制項,那之前設定子控制項狀態又被全部重建了,所以需特別注意有這樣的情形。另外複合控制項有可能重覆執行建立子控制的動作,在執行效能上也比較不佳。

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/16/5695.aspx

]]>
jeff377 2008-10-16 00:14:12
[ASP.NET 控制項實作 Day14] 繼承 CompositeControl 實作 Toolbar 控制項 https://ithelp.ithome.com.tw/articles/10012339?sc=rss.iron https://ithelp.ithome.com.tw/articles/10012339?sc=rss.iron 之前我們簡單介紹過繼承 CompositeControl 來實作複合控制項,在本文我們將以 Toolbar 控制項為例,以複合控制項的作法(繼承 CompositeControl )來實作 To...]]> 之前我們簡單介紹過繼承 CompositeControl 來實作複合控制項,在本文我們將以 Toolbar 控制項為例,以複合控制項的作法(繼承 CompositeControl )來實作 Toolbar 控制項,此工具列控制項包含 Items 屬性來描述工具列項目集合,依 Items 屬性的設定來建立工具列按鈕,另外包含 Click 事件可以得知使用按了那個按鈕。
程式碼下載:ASP.NET Server Control - Day14.rar
一、工具列項目集合類別
工具列包含多個按鈕,新增 TBToolbarItem 類別來描述工具列項目,TBToolbarItem 類別包含 Key、Text、Enabled 三個屬性;而 TBToolbarItemCollection 為 TBToolbarItem 的集合類別來描述工具列按鈕集合。

二、實作 TBToolbar 控制項
step1. 新增繼承 CompositeControl 的 TBToolbar 控制項

    < _
    Description("工具列控制項。"), _
    ParseChildren(True, "Items"), _
    ToolboxData("<{0}:TBToolbar runat=server ></{0}:TBToolbar>") _
    > _
    Public Class TBToolbar
        Inherits CompositeControl
    End Class 

step2. 新增 Items 屬性,描述工具列項目集合

        ''' <summary>
        ''' 工具列項目集合。
        ''' </summary>
        < _
        Description("工具列項目集合。"), _
        PersistenceMode(PersistenceMode.InnerProperty), _
        DesignerSerializationVisibility(DesignerSerializationVisibility.Content), _
        Editor(GetType(CollectionEditor), GetType(UITypeEditor)) _
        > _
        Public ReadOnly Property Items() As TBToolbarItemCollection
            Get
                If FItems Is Nothing Then
                    FItems = New TBToolbarItemCollection()
                End If
                Return FItems
            End Get
        End Property

step3. 新增 Click 事件
TBToolbar 類別新增 Click 事件,當按下按鈕時會引發 Click 事件,由 Click 的事件引數 e.Key 可以得知使用者按了那個按鈕。

        ''' <summary>
        ''' Click 事件引數。
        ''' </summary>
        Public Class ClickEventArgs
            Inherits System.EventArgs
            Private FKey As String = String.Empty

            ''' <summary>
            ''' 項目鍵值。
            ''' </summary>
            Public Property Key() As String
                Get
                    Return FKey
                End Get
                Set(ByVal value As String)
                    FKey = value
                End Set
            End Property
        End Class

        ''' <summary>
        ''' 按下工具列按鈕所引發的事件。
        ''' </summary>
        < _
        Description("按下工具列按鈕所引發的事件。") _
        > _
        Public Event Click(ByVal sender As Object, ByVal e As ClickEventArgs)

        ''' <summary>
        ''' 引發 Click 事件。
        ''' </summary>
        Protected Overridable Sub OnClick(ByVal e As ClickEventArgs)
            RaiseEvent Click(Me, e)
        End Sub

step4. 建立工具列按鈕集合
覆寫 CreateChildControls 方法,依 Items 屬性的設定,來建立工具列中的按鈕集合。每個按鈕的 Click 事件都導向 ButtonClickEventHandler 方法,來處理所有按鈕的 Click 動作,並引發 TBToolbar 的 Click 事件。

        Private Sub ButtonClickEventHandler(ByVal sender As Object, ByVal e As EventArgs)
            Dim oButton As Button
            Dim oEventArgs As ClickEventArgs

            oButton = CType(sender, Button)
            oEventArgs = New ClickEventArgs()
            oEventArgs.Key = oButton.ID
            OnClick(oEventArgs)
        End Sub

        ''' <summary>
        ''' 建立子控制項。
        ''' </summary>
        Protected Overrides Sub CreateChildControls()
            Dim oItem As TBToolbarItem
            Dim oButton As Button

            For Each oItem In Me.Items
                oButton = New Button()
                oButton.Text = oItem.Text
                oButton.Enabled = oItem.Enabled
                oButton.ID = oItem.Key
                AddHandler oButton.Click, AddressOf ButtonClickEventHandler
                Me.Controls.Add(oButton)
            Next
            MyBase.CreateChildControls()
        End Sub

三、測試程式
在頁面拖曳 TBToolbar 控制項,並設定 Items 屬性,如入新增、修改、刪除三個按鈕。

在 TBToolbar 控制項的 Click 事件加入測試程式碼,輸出引發 Click 事件的 e.Key。

    Protected Sub TBToolbar1_Click(ByVal sender As Object, ByVal e As Bee.Web.WebControls.TBToolbar.ClickEventArgs) Handles TBToolbar1.Click
        Me.Response.Write(String.Format("您按了 {0}", e.Key))
    End Sub

執行程式,當按了工具列上的按鈕時,就會引發 Click 事件,並輸出該按鈕對應的 Key。

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/15/5687.aspx

]]>
jeff377 2008-10-15 00:13:50
[ASP.NET 控制項實作 Day13] Flash 控制項 https://ithelp.ithome.com.tw/articles/10012267?sc=rss.iron https://ithelp.ithome.com.tw/articles/10012267?sc=rss.iron Flash 也是網頁常用的 ActiveX 插件,在本文中將繼承 TBActiveX 下來撰寫 TBFlash 控制項,用來輸出網頁套用 Flash 的相關 HTML 碼。
程式碼下...]]>
Flash 也是網頁常用的 ActiveX 插件,在本文中將繼承 TBActiveX 下來撰寫 TBFlash 控制項,用來輸出網頁套用 Flash 的相關 HTML 碼。
程式碼下載:ASP.NET Server Control - Day13.rar

一、網頁 Flash 的原始 HTML 碼
我們先觀查在網頁中套用 Flash 插件的原始 HTML 碼,以點部落首頁抬頭的 Flash 原始碼為例如下,其中 <object> tag 的 codebase attribute 是指 Flash 插件的下載位置及版本。

<object id="ShockwaveFlash2" height="90" width="728" 
  codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,29,0" 
  classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000">
<param value="http://files.dotblogs.com.tw/dotjum/ad/debug.swf" name="movie"/>
<param value="high" name="quality"/>
<param value="#000000" name="bgcolor"/>
<embed height="90" width="728" type="application/x-shockwave-flash" 
  pluginspage="http://www.macromedia.com/go/getflashplayer" quality="high" 
  src="http://files.dotblogs.com.tw/dotjum/ad/debug.swf"/>
</object>

在 <object> tag 中必要的 attribute 為 classid、codebase、movie、width、height,而 <embed> tag 的必要 attribute 為 src、pluginspage、width、height,其他選擇性的 attribute 可參閱以下網頁。

Flash OBJECT and EMBED tag attributes
http://kb.adobe.com/selfservice/viewContent.do?externalId=tn\_12701

二、實作 TFlash 控制項
了解 Flash 的原始 HTML 碼後,我們就可以開始著手撰寫 TBFlash 控制項,想辨法來輸出所需要的 HTML 碼。

step1. 新增 TBFlash 控制項繼承至 TBActiveX
我們先在 TBActiveX 控制項新增一個 CodeBase 屬性,用來設定 ActiveX 插入的下載位置及版本,然後新增 TBFlash 控制項繼承至 TBActiveX,並在建構函式中設定 MyBase.ClassId 及 MyBase.CodeBase 屬性。

    Public Class TBFlash
        Inherits TBActiveX

        ''' <summary>
        ''' 建構函式。
        ''' </summary>
        Sub New()
            MyBase.ClassId = "D27CDB6E-AE6D-11CF-96B8-444553540000"
            MyBase.CodeBase = "http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,29,0"
        End Sub
    End Class 

step2. 加入相關屬性
在 TBFlash 加入 MovieUrl 及 Quality 屬性,MovieUrl 為 Flash 檔案來源,Quality 為影音品質。

step3. 輸出 Flash 相關參數
覆寫 CreateChildControls 方法,輸出 MovieUrl 及 Quality 屬性對應的參數,以及在 Params 集合屬性設定的參數。

        ''' <summary>
        ''' 加入 MediaPlayer 參數。
        ''' </summary>
        ''' <param name="Name">參數名稱。</param>
        ''' <param name="Value">參數值。</param>
        Private Sub AddParam(ByVal Name As String, ByVal Value As String)
            Dim oParam As TBActiveXParam

            oParam = New TBActiveXParam(Name, Value)
            Me.Params.Add(oParam)
        End Sub

        ''' <summary>
        ''' 建立 Embed 標記。
        ''' </summary>
        Private Function CreateEmbed() As HtmlControls.HtmlGenericControl
            Dim oEmbed As HtmlControls.HtmlGenericControl
            Dim oParam As TBActiveXParam

            oEmbed = New HtmlControls.HtmlGenericControl()
            oEmbed.TagName = "embed"
            oEmbed.Attributes("src") = Me.ResolveClientUrl(Me.MovieUrl)
            oEmbed.Attributes("pluginspage") = "http://www.macromedia.com/go/getflashplayer"
            oEmbed.Attributes("height") = Me.Height.ToString
            oEmbed.Attributes("width") = Me.Width.ToString

            'Embed 的 Attributes 加入 Params 集合屬性的設定
            For Each oParam In Me.Params
                If oParam.Name <> "movie" Then
                    oEmbed.Attributes(oParam.Name) = oParam.Value
                End If
            Next
            Return oEmbed
        End Function

        ''' <summary>
        ''' 建立子控制項。
        ''' </summary>
        Protected Overrides Sub CreateChildControls()
            Dim oEmbed As HtmlControls.HtmlGenericControl

            '加入 movie 參數
            AddParam("movie", Me.ResolveClientUrl(Me.MovieUrl))

            '加入 quality 參數
            If Me.Quality <> EQuality.NotSet Then
                AddParam("quality", Me.Quality.ToString.ToLower)
            End If

            MyBase.CreateChildControls()

            oEmbed = CreateEmbed()
            Me.Controls.Add(oEmbed)
        End Sub

三、測試程式
在頁面拖曳 TBFlash 控制項,設定 MovieUrl 及 Quality 屬性,若有需要加入其他參數,可自行設定 Params 集合屬性。執行程式就可以在頁面上看到呈現出來的 Flash。

        <bee:TBFlash ID="TBFlash1" runat="server" Height="90px" 
            MovieUrl="http://files.dotblogs.com.tw/dotjum/ad/debug.swf" Quality="High" 
            Width="728px">
        </bee:TBFlash>

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/14/5674.aspx

]]>
jeff377 2008-10-14 00:16:30
[ASP.NET 控制項實作 Day12] 繼承 TBActiveX 重新改寫 TBMediaPlayer 控制項 https://ithelp.ithome.com.tw/articles/10012196?sc=rss.iron https://ithelp.ithome.com.tw/articles/10012196?sc=rss.iron 上篇介紹的 TBActiveX 控制項,它可以支援網頁 Media Player 的設定,這跟前面提及的 TBMediaPlayer 功能相同。TBActiveX 具有網頁設定 ActiveX ...]]> 上篇介紹的 TBActiveX 控制項,它可以支援網頁 Media Player 的設定,這跟前面提及的 TBMediaPlayer 功能相同。TBActiveX 具有網頁設定 ActiveX 通用屬性,所以 TBMediaPlayer 基本上是可以由 TBActiveX 繼承下來,再加入 Media Player 特有的屬性即可。本文將原來的 TBMediaPlayer 控制項,繼承的父類別由 WebControl 改為 TBActiveX 類別,重新改寫 TBMediaPlayer 控制項。
程式碼下載:ASP.NET Server Control - Day12.rar

一、改寫 TBMediaPlayer 控制項
TBMediaPlayer 控制項原本是繼承 WebControl,現改繼承對象為 TBActiveX,來重新改寫 TBMediaPlayer 控制項。

step1. TBMediaPlayer 繼承至 TBActiveX
新增 TBMediaPlayer 控制項,繼承至 TBActiveX,並在建構函式設定 Media Player ActiveX 的 ClassId。

    Public Class TBMediaPlayer
        Inherits TBActiveX

        ''' <summary>
        ''' 建構函式。
        ''' </summary>
        Sub New()
            MyBase.ClassId = "6BF52A52-394A-11D3-B153-00C04F79FAA6"
        End Sub
    End Class

step2. 加入相關屬性
跟原來的 TBMediaPlayer 控制項一樣,加入 Url、AutoStart、UIMode 三個屬性,可視情形加入需要設定的屬性。

step3. 加入 Media Player 參數
覆寫 CreateChildControls 方法,動態依屬性設定在 Params 集合屬性加入參數。雖然 TBMediaPlayer 控制項目前只有 Url、AutoStart、UIMode 三個屬性,但是父類別 TBActiveX 具有 Params 集合屬性,所以開發人員可以視需求加入其他未定義的參數。

        ''' <summary>
        ''' 加入 MediaPlayer 參數。
        ''' </summary>
        ''' <param name="Name">參數名稱。</param>
        ''' <param name="Value">參數值。</param>
        Private Sub AddParam(ByVal Name As String, ByVal Value As String)
            Dim oParam As TBActiveXParam

            oParam = New TBActiveXParam(Name, Value)
            Me.Params.Add(oParam)
        End Sub

        ''' <summary>
        ''' 覆寫 CreateChildControls 方法。
        ''' </summary>
        Protected Overrides Sub CreateChildControls()
            '加入 Url 參數
            If Me.Url <> String.Empty Then
                AddParam("URL", Me.ResolveClientUrl(Me.Url))
            End If
            '加入 autoStart 參數
            If Me.AutoStart Then
                AddParam("autoStart", "true")
            End If
            '加入 uiMode 參數
            If Me.UIMode <> EUIMode.NotSet Then
                AddParam("uiMode", Me.UIMode.ToString)
            End If
            MyBase.CreateChildControls()
        End Sub

二、執行程式
在頁面拖曳 TBMediaPlayer 控制項,設定 Url、AutoStart、UIMode 屬性,若有需要加入其他參數,可自行設定 Params 集合屬性。執行程式就可以在頁面上看到呈現出來的 Media Player。

        <bee:TBMediaPlayer ID="TBMediaPlayer1" runat="server" AutoStart="True" 
            Height="249px" Url="D:\Movie_01.wmv" Width="250px">
        </bee:TBMediaPlayer>

備註:本文同步發佈於筆者「ASP.NET 魔法學院」部落格
http://www.dotblogs.com.tw/jeff377/archive/2008/10/13/5663.aspx

]]>
jeff377 2008-10-13 00:13:29
[ASP.NET 控制項實作 Day11] ActiveX 伺服器控制項 https://ithelp.ithome.com.tw/articles/10012159?sc=rss.iron https://ithelp.ithome.com.tw/articles/10012159?sc=rss.iron Media Player 與 Flash 之類在網頁上執行的外掛控制項,都是屬於 ActiveX 控制項,它們套用在 HTML 碼中的方式差不多,除了要指定 ClassID 以外,ActiveX...]]> Media Player 與 Flash 之類在網頁上執行的外掛控制項,都是屬於 ActiveX 控制項,它們套用在 HTML 碼中的方式差不多,除了要指定 ClassID 以外,ActiveX 使用的參數(相當於 ActiveX 控制項的屬性)以 Param Tag 來表示。本文標題命名為「ActiveX 伺服器控制項」就是避免誤解為 ActiveX 控制項,而是在 ASP.NET 中輸出 ActiveX 相關 HTML 碼的伺服器控制項;我們可透過 ActiveX 伺服器控制項可以用來輸出網頁上引用 ActiveX 的通用 HTML 碼,另外 ActiveX 的參數會以集合屬性來呈現,所以也會一併學習到集合屬性的撰寫方式。
程式碼下載:ASP.NET Server Control - Day11.rar

一、集合屬性
ActiveX 的 Param 參數是集合屬性,所以我們定義了 TBActiveParam 類別描述 ActiveX 參數,包含 Name 及 Value 屬性;而 TBActiveXParamCollection 為 TBActiveParam 的集合類別,用來描述 ActiveX 參數集合。TBActiveXParamCollection 繼承 CollectionBase,加入操作集合的 Add、Insert、Remove、IndexOf、Contains 等方法,關於集合屬性的用法可以參閱筆者在部落格的「撰寫伺服器控制項的集合屬性 (CollectionBase)」一文中有詳細說明。

二、實作 ActiveX 伺服器控制項
step1. 新增繼承 WebControl 的 TBActiveX

step2. 覆寫 TagKey 屬性,傳回 object 的 Tag

        Protected Overrides ReadOnly Property TagKey() As HtmlTextWriterTag
            Get
                Return HtmlTextWriterTag.Object
            End Get
        End Property

step3. 新增 ClassId 屬性,描述 ActiveX 的 ClassId
定義 ClassId 屬性,並覆寫 AddAttributesToRender 來輸出此屬性。

        ''' <summary>
        ''' 覆寫 AddAttributesToRender 方法。
        ''' </summary>
        Protected Overrides Sub AddAttributesToRender(ByVal writer As HtmlTextWriter)
            '加入 MediaPlayer ActiveX 元件的 classid
            writer.AddAttribute("classid", String.Format("clsid:{0}", Me.ClassId))
            MyBase.AddAttributesToRender(writer)
        End Sub

step4. 新增 Params 屬性,描述 ActiveX 的參數集合
定義 Params 屬性,型別為 TBActiveXParamCollection 類別,套用 EditorAttribute 設定 CollectionEditor 為集合編輯器。

        ''' <summary>
        ''' ActiveX 控制項參數集合。
        ''' </summary>
        < _
        Description("控制項參數集合。"), _
        PersistenceMode(PersistenceMode.InnerProperty), _
        DesignerSerializationVisibility(DesignerSerializationVisibility.Content), _
        Editor(GetType(CollectionEditor), GetType(UITypeEditor)) _
        > _
        Public ReadOnly Prope