在上篇文章有提到,FP 的重點是要管理因副作用而產生的程式碼複雜性,而要如何管理副作用造成的意外結果呢?
首先我們要先辨別程式碼中哪些部分會有副作用,才能進一步去管理它。那要如何辨別呢? 根據《簡約的軟體開發思維:用 Functional Programming 重構程式 - 以 Javascript 為例》書中所述,在 FP 的世界裡會將程式碼歸類為 3 類 :
舉例來說,以下這些程式碼我們可以分成三類來看它 :
// 關於某人的資訊,屬於 Data
{
"firstname": "Eric",
"lastname": "Normand"
}
// * 寄送電子郵件,屬於 Actions
sendEmail(to, from, subject, body)
// 求出一串數字總和,屬於 Calculations
sum(numbers)
// * 將資料儲存到資料庫,系統其他部分就可看到資料,屬於 Actions
saveUserDB(user)
// 對函式輸入相同字串不論多少次,每次輸出結果永遠相同,屬於 Calculations
string_length(str)
// * 每次呼叫,傳回的時間都不一樣,屬於 Actions
getCurrentTime()
// 單純數字陣列,屬於 Data
其中,Actions 類型的程式碼特別受 FP 程式設計師關注,因為 Actions 類型的函式,其執行結果會受呼叫時間與次數影響。例如 sendEmail(to, from, subject, body)
,呼叫一次是寄一封信,呼叫兩次是寄兩封信,這兩者代表的意義不同,也會造成不同的結果。
要如何快速地區分程式碼屬於哪種類型呢?在看到一段程式時,我們只需要問一個關鍵問題:「這段程式碼的執行結果,會不會因為『執行的時間點』或『執行的次數』而有所不同?」
如果答案是「不會」,我們再問第二個問題:「它能被執行嗎?」
sum()
函式)。[5, 6, 7]
陣列)。
圖 1 判斷 Actions、Calculations 與 Data 的方式(資料來源: 自行繪製)
透過這簡單的分類方式,可讓我們辨識出哪些是需要特別注意的 Actions,哪些是比較能放心的 Calculation 和 Data,能幫助我們聚焦副作用的管理。
假設我們在一個電商網站上購物,當使用者點擊「加入購物車」時,網站會更新購物車內容並重新計算總金額。這個過程可以拆解成以下幾個步驟 :
{ itemId: 'A-123', quantity: 1 }
。這段 JSON 內容本身只是靜態的資訊,它不會執行任何事。這是 Data。透過這個購物流程的案例,我們可以看到一個看似單純的功能,其實是由多個 Actions、Calculations 與 Data 交織而成。為了更深入理解為何這樣區分很重要,讓我們先來看看這三者各自的特徵。
稍微介紹這三種類型的特徵如下:
2 + 3
)都可以被它的運算結果(5
)直接取代,而不會改變程式的行為。 sum([5, 6, 7])
的結果是什麼。關於 Calculations,也就是純函數(pure function),會在後面提到更多內容。
了解了這三者的特徵後,我們就能進一步探討區分它們的價值是什麼。
為何程式寫得好好的,還要去區分程式碼屬於哪一類?因為這能幫助我們隔離複雜性。
區分 Actions、Calculations 和 Data 的好處,在現代軟體開發中尤其明顯,特別是當我們面對分散式系統 (distributed systems) 時。
FP 並不是什麼新潮的技術,它背後的數學基礎早已存在。但它之所以在近年來越來越受歡迎,很大程度上是因為網路的普及。現在的應用程式,大多需要讓多個程式(例如前端、後端、微服務)透過網路溝通。
這種溝通方式帶來了新的挑戰:訊息可能不會按照我們預期的順序抵達、同樣的訊息可能重複傳送、甚至根本沒有送達。這代表我們必須更謹慎地處理「事件在何時發生」以及「發生了幾次」這類問題。
這時,Actions、Calculations 和 Data 的分類就顯示出它的威力了:
因此就浮現了一個解決方案:當我們的程式對執行時間與次數的依賴度越低,出錯的機率就越低。透過提高程式中 Data 與 Calculations 的佔比,並使用專門的工具來小心管理 Actions,我們的軟體就能更從容地應對分散式系統帶來的挑戰。因為我們要重點關注的是 Actions,當 Actions 範圍越小,我們要關注的就越少,畢竟人的注意力有限嘛,總不能全部的程式碼都要小心翼翼其副作用造成的結果,當注意力集中在少數 Actions,我們管理起來就會更容易。
了解了 Actions、Calculations 和 Data 的概念與重要性後,我們該如何在實際開發中應用這套思維模式呢?以下是幾個可應用的時機:
上篇我們理解了 FP 重點是管理因副作用而產生的程式碼複雜性,而要如何管理呢?第一步就是先辨識程式碼中需要被注意的部分,在 《簡約的軟體開發思維:用 Functional Programming 重構程式 - 以 Javascript 為例》 書中提到,可以將程式分成 Actions、Calculations 和 Data 三個分類,可用來審視我們的程式碼中需要被注意的部分。
另外也將這三類整理成一個簡單的表格如下:
類別 | 執行結果是否受呼叫影響? | 是否可執行? | 特性 | 範例 |
---|---|---|---|---|
Actions | 是 | 是 | 具備副作用,是複雜性的主要來源,屬於不純的函數。 | sendEmail(to, from, subject, body) 、saveUserDB(user) 、 getCurrentTime() |
Calculations | 否 | 是 | 純粹的運算,可靠且易於測試,也就是所謂的純函數(pure function) | sum(numbers) 、string_length(str) |
Data | 否 | 否 | 靜態、透明的資訊,是最單純的。 | [1, 10, 2, 45, 3, 98] 、{"firstname": "Eric", "lastname": "Normand"} |
辨識出這三者,我們就能知道程式碼中哪些部分是穩定可靠的,哪些部分是需要我們特別小心管理的「危險區域」。