閒談軟體設計:State 與語言

更新於 2024/11/16閱讀時間約 10 分鐘

永遠有狀態

會想寫這篇是因為先前看到一篇網路文章討論用物件管理狀態的問題,看完後我只能說,不論用 OOP 還是 FP 語言,終究得處理狀態,程式也許沒有狀態,但系統有狀態。因為,如果系統沒有狀態,那會非常難用,沒有購物車記住選購的東西,那網頁購物系統會多難用應該不難想像吧!

先出個題目給大家,隨處可見的自動販賣機,能投幣、選飲料和退幣,就這三個功能,想想看會產生多複雜的狀態圖 (state chart),等等公布答案。

Pure function 沒有 side effect,但作為一個系統,總有個地方保存狀態,並將狀態的一部分作為參數傳入 pure function 計算取得到下個狀態,然後保存起來;stateless request handler 沒有保存狀態,得以分散到不同的伺服器實體執行,但系統仍會將執行後的結果 (狀態) 以某種方式 (例如:資料庫) 儲存起來,當下個 request 進來時,再將狀態讀取出來。這兩個例子中,狀態不是存在 function 或是 handler 中,而是在別的地方。

提到 side effect,讓我想起之前跟指導教授討論論文時 (我的論文是視覺化程式語言,和 FP 有很多相似處),當時的我也把 side effect 當成壞東西,但老師提了個問題讓我思考一下,side effect 真的是壞東西嗎?如果沒有 side effect,我們現在的電腦系統還能運行嗎?我想了一下,組合語言的 JMP 指令 (改寫 program counter內容) 會無法運作了,那 function call 和 return 就做不出來,這時才瞭解到,side effect 不全然是壞事,不預期的 side effect 才是壞事。

Immutability

最近隨著 FP 的流行,immutability 一直被提倡,物件有狀態,會被修改好像是一種惡,但真是如此?immutability 很好,但所謂的狀態就是會隨著操作變動,差別只在於變動發生在哪裡?

先忘掉物件這個詞,從記憶體的角度來看,FP 做法比較像 Figure 1 的左邊,橫軸代表時間演進,記憶體中有個區塊紀錄資料 A,有個指標指向它,代表系統目前的狀態為 A,經過一個操作,產生另一個區塊紀錄資料 A',指標改指向新區塊,狀態因此變成 A',再一次操作,一個區塊產生,指標改指向 A’’ 區塊,A 和 A' 在每次操作都沒有變動,因此它們是 immutable。

Figure 1 — FP way and OOP way

Figure 1 — FP way and OOP way

而 OOP 的方式則是像 Figure 1 的右邊,一開始有個區塊記錄狀態 A,然後有個指標指向該區塊,進行操作後,同個記憶體區塊儲存的狀態變成 A’(虛線表示從現在時間往回看該狀態已不存在),再一次操作後,同個記憶體區塊中狀態變成 A'',因此物件是 mutable。若從記憶體的角度看,一個是變動指向狀態的指標,一個是變動裝載狀態的容器 [1]。

事實上,OOP 當初被提出,有個很重要的基礎:把狀態與修改狀態的方法封裝在一起,希望改善過去大量修改 global variables 引起的不受控狀態變化。既然如此,那為什麼現在會覺得物件有狀態是件壞事呢?個人覺得是毫無設計的 setter 被濫用,物件的狀態以不受控的方式被修改。事實上透過 setter 修改狀態跟存取 global variables 是差不多的,囉嗦且沒有保護到狀態。

Kent Beck 的《Implementation Patterns》書中提到:

比起 getting method,我更不願意讓 setting method 可見。setting method 是根據實作來命名的,而不是根據意圖。

那什麼是根據意圖設計的 method?以剛剛提到的自動販賣機為例,為投入的金額提供 setter 就不是一個好設計,雖然 UI (或任何使用物件的第三方) 在投入時,取得目前金額加上投入的金額再呼叫 setter 更新資料讓狀態是對的,但如果 UI 沒有這樣做,直接呼叫 setter 設定一個錯誤的金額,這時狀態的變化就是不預期的。

以自動販賣機為例

別小看自動販賣機 (有點後悔當初選它作為例子),平常的操作 (happy path) 只是複雜狀態轉換中的一小部分,就如下圖中深藍色的路徑:機器插上電,因為有東西可以選購,所以從 {初始狀態} 進入 {等待狀態},投入第一個硬幣後進到 {投幣狀態},但金額可能還不夠 [2],所以繼續投幣,但不論投幾個,仍停留在 {投幣狀態},假設金額夠了,選取飲料後,飲料掉下來,但可能因為餘額還夠選購其他飲料,所以仍停在 {投幣狀態} [3],按下退幣,機器把餘額找回後回到 {等待狀態}

但這只是一個 happy path,若考慮零錢箱空了或是滿了,架上商品都賣完了等諸多情況,或是不足額時按下選擇飲料該怎麼辦?於是一個複雜的狀態圖就出現了 (實際上這已經不知道是第幾個版本了,但我也不敢保證下圖是完整的)。一般來說,當思考的越完整,系統就能夠越強健 (robust)。

Figure 2 — Slot machine state chart

Figure 2 — Slot machine state chart

說這麼多,那怎樣才是比較好的設計?首先,定義系統功能,若將建立物件的動作視為 power on ,銷毀物件視為 power off,排除後,要提供給消費者的功能有:投幣 insert(coin)、選擇飲料 select(slot)、退幣 refund(),提供給管理者的功能有:補貨 refill(drink, slot, amount) 和整理零錢箱 setupCoinBox(coin, amount)。這五個組合自動販賣機的主要介面。如果考慮到不同使用者能看到不同的介面,可拆成兩個介面 (SlotMachineManageableSlotMachine),但由同一個類別同時實作兩個介面。

定義介面時要思考參數的型別,例如:介面是否可以接受各種面額的銅板?要支援各種國家的貨幣嗎?有些語言有支援無號數 (unsigned),有些語言則沒有 (很可惜 Java 8 有些 API 輔助但不算有支援),那要不要限制只能使用正整數 (unsigned integer) 或是更嚴格地只能使用列舉 (enum),就像 Coin 定義了銅板的列舉,這邊稍微簡化一下,貨幣都是新台幣。

有些參數可能無法列舉或不好列舉,例如有多少貨架 (slot),每個貨架能放多少飲料,它們可能有最大值或最小值,但用列舉會不太實際,最重要的是參數與回傳值能不能被修改。再來是思考,要提供什麼資訊給外界使用,像是有多少貨架,每個貨架上是什麼飲料,要多少錢等等。

到目前為止,都還沒有談到怎麼設計狀態該怎麼儲存,但有了這些介面,再設計狀態的資料結構會比較容易。這大概是我這幾年習慣先設計 interface 再實作的原因之一:先讓自己思考這個物件需要提供什麼服務給外部使用,getter 和 setter 都是其次。

每個貨架的資訊,用一個 SlotInfo 介面表示,仔細看會發現,這個介面只有 getter,並沒有 setter,因為不希望讓取得貨架資訊物件的人可以任意修改內容。飲料資訊也是類似的情況,Drink 是一個 immutable 類別,在建構子傳入必要的資訊後,再也沒有其他函式可以改變內部的狀態。

最後,開始實作 SlotMachine 和 ManageableSlotMachine 介面,這時才開始思考怎麼儲存狀態所需要的資訊,為了處理可變狀態,DefaultSlotMachine建立一個只有內部才能存取 (private class) 的 RefillableSlot 類別,它實作 SlotInfo 介面,因此在 DefaultSlotMachine 的第 70 行可以直接將物件丟出去,但對外部來說並不知道它是可變的。

從上例可以看到,內部狀態怎麼儲存和公開的介面不同,內部資料結構沒有提供 getter 也沒有提供 setter,只能透過公開介面以規範的方式存取。也就是說,內部實作不管怎麼變,公開的介面維持不變,對使用者來說,它就是投幣自動販賣機。

進階使用案例

假設我們想用 state pattern 來處理不同狀態下,refund()select(slot) 或 insert(coin) 的不同行為,沒問題,儘管嘗試,由於不是透過 setter 讓外部更新狀態,內部怎麼改都行,只要維持公開介面不變,滿足介面的合約 (預期行為) 就行,這才是 OOP 想做到的事情。

事實上,用 FP 寫程式其實也是如此,先思考有什麼行為,狀態的資料結構會是函式參數的一部分,這不見得是一件壞事,因為寫單元測試時,資料的 set up 相對容易,但也不完全是好事,因為當想改變資料結構時,同時會影響到使用者。

由於內部狀態被保護,所以在測試時確實很難像 FP 那樣,可以將任意狀態當作參數測試函式的行為,但同樣地,這也不見得是一件壞事,測試程式碼可以自己設計合適的資料結構表示狀態,只是需要提供一個像下例的輔助函式將測試資料的資料結構轉換成待測物件。

這同樣也適用備份跟還原狀態上,假設要每個行為後的狀態記錄下來,當要還原到前一個動作的狀態,可以將紀錄的狀態還原回來。因此,並不是因為用了 OOP 或是封裝就無法做到時光機,只是需要額外的程式。

小結

選擇語言是一種 trade-off,試想我們有多常用到這功能,若沒有,我們也不需要花這個功不是嗎?所以並不是哪一種 paradigm 就無法做到什麼事,而是要花多少時間才能做到想做的事。生產力 (productivity) 也不是唯一要考慮的事情,可維護性、可讀性、效率等,都會影響到選擇,這也是為什麼有人喜歡 FP,有人喜歡 OOP,有人喜歡動態型別語言,有人喜歡靜態行別語言了。


註釋

註 1:左邊因為過去的所有狀態都保留下來 (其實還是要另外處理,不然記憶體空間會被回收才對),但在 debug 時有前後可以對照,確實清楚許多。
註 2:如果架上所有飲料價格都一樣,拆出足額與不足額狀態,確實能增加對系統的理解,但實際上價格可能不一樣,很難用一個系統狀態表示 (倒是可以作為 slot 的狀態)。
註 3:這個部分可能因為不同的設定,行為不一樣,有的是直接找零然後回到 {等待狀態})。

avatar-img
53會員
104內容數
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
Spirit的沙龍 的其他內容
針對這議題,從 devOps 的角度看,團隊應抱有持續不斷地改進的精神,努力降低上版的風險,最後,哪一天上版就僅僅是風險控管的問題了。風險控管除了考量到損失,當然還要考慮到團隊要怎麼 on-call,on-call 的資源夠不夠應付上版後的突發狀況,才能做出適當的決策。
這文章來自網友在 在 Medium 上的留言 (有人幫忙想題目也挺不錯的),問到:Singleton 對於好的架構來說是否能避免就避免呢?我簡單地回了一下我的想法 ,但 Singleton 其實很有趣,所以就寫篇文章來聊聊吧!
真的要符合 single responsibility,通常會得到很多很小的類別或是函式,各別完成一個小的功能,然後在某個地方被聚合起來完成一個使用案例 (use case),而不是一個很大的類別,包山包海,然後最後變成一個狀態超複雜,超級難測試的類別。
這是幾年來我對於軟體架構師的心路歷程,上述不保證讓你成為軟體架構師,但希望會對軟體工程師職涯有幫助。也希望台灣的軟體公司能稍微多注重一下軟體架構,甚至能像 91App 不只工程師團隊,還有軟體架構團隊。
我個人是盡可能不寫 switch statement,但觀察這幾年程式語言的趨勢,會發現許多語言把 switch statement 擴充成為實作 pattern matching 的工具,說不定以後 switch statement 會越來越廣泛使用也說不定。
長遠的角度來看,內部函式庫還是值得投資的公司資產,只是它需要時間、人力與管理才能做得好。若有不錯的內部函式庫也可以回饋給open-source社群,畢竟,現在開發軟體已經不太可能沒有用到任何open-source的東西。雖然說是將公司資產以 open-source 釋出,但換取的利益卻不見得是零。
針對這議題,從 devOps 的角度看,團隊應抱有持續不斷地改進的精神,努力降低上版的風險,最後,哪一天上版就僅僅是風險控管的問題了。風險控管除了考量到損失,當然還要考慮到團隊要怎麼 on-call,on-call 的資源夠不夠應付上版後的突發狀況,才能做出適當的決策。
這文章來自網友在 在 Medium 上的留言 (有人幫忙想題目也挺不錯的),問到:Singleton 對於好的架構來說是否能避免就避免呢?我簡單地回了一下我的想法 ,但 Singleton 其實很有趣,所以就寫篇文章來聊聊吧!
真的要符合 single responsibility,通常會得到很多很小的類別或是函式,各別完成一個小的功能,然後在某個地方被聚合起來完成一個使用案例 (use case),而不是一個很大的類別,包山包海,然後最後變成一個狀態超複雜,超級難測試的類別。
這是幾年來我對於軟體架構師的心路歷程,上述不保證讓你成為軟體架構師,但希望會對軟體工程師職涯有幫助。也希望台灣的軟體公司能稍微多注重一下軟體架構,甚至能像 91App 不只工程師團隊,還有軟體架構團隊。
我個人是盡可能不寫 switch statement,但觀察這幾年程式語言的趨勢,會發現許多語言把 switch statement 擴充成為實作 pattern matching 的工具,說不定以後 switch statement 會越來越廣泛使用也說不定。
長遠的角度來看,內部函式庫還是值得投資的公司資產,只是它需要時間、人力與管理才能做得好。若有不錯的內部函式庫也可以回饋給open-source社群,畢竟,現在開發軟體已經不太可能沒有用到任何open-source的東西。雖然說是將公司資產以 open-source 釋出,但換取的利益卻不見得是零。
你可能也想看
Google News 追蹤
Thumbnail
徵的就是你 🫵 超ㄅㄧㄤˋ 獎品搭配超瞎趴的四大主題,等你踹共啦!還有機會獲得經典的「偉士牌樂高」喔!馬上來參加本次的活動吧!
Thumbnail
隨著理財資訊的普及,越來越多台灣人不再將資產侷限於台股,而是將視野拓展到國際市場。特別是美國市場,其豐富的理財選擇,讓不少人開始思考將資金配置於海外市場的可能性。 然而,要參與美國市場並不只是盲目跟隨標的這麼簡單,而是需要策略和方式,尤其對新手而言,除了選股以外還會遇到語言、開戶流程、Ap
Thumbnail
這篇文章探討了在軟體開發中的技術債可能來自哪些原因,以及如何自動化偵測與修復技術債。作者透過分享不同情境下的技術債選擇,提供了對於技術債的思考與建議,針對開發人員在需要做出無奈的技術決策時,提供了一些建議。此外,還提供了一些在做出技術決策時的方法,如保留抽象層和避免vendor lock-in。
Thumbnail
今天來聊個最近很夯的主題 DDD,但不是 DDD 的本尊 Domain Driven Design,而是無所不在的 Database Driven Design,Database Driven Design 不是不好,只是你的模型容易變成貧血模型,邏輯都集中在 service 層等等。
Thumbnail
有趣的是,Model 其實沒什麼嚴格的定義,所以每個人對 Model 的解讀也不盡相同,有人覺得資料怎麼儲存屬於 Model 的一部份 (受 ORM 工具的影響),有人覺得工作流程 (workflow) 是 Model 的一部份,我個人也有自己的想法,而且隨專案的規模和特性,也不是總是一樣的。
Thumbnail
起源是當時 Facebook 有篇文章討論不少人分不清楚上述二者的差別,當時寫了首部曲《閒談軟體設計:API Naming Style》,接著是《閒談軟體設計:內部函式庫》,但始終沒談到 library 和 framework 的差別,主要是沒有好的例子,這次這例子還蠻不錯的。
Thumbnail
我自己偏好用 Repository 搭配 decorator 來管理 cache,而不是在 controller 層或是到處都有快取的邏輯,如果程式都是透過 Repository 更新資料,Repository 就會是一個不錯的地方更新快取,邏輯也就不會散亂在各處了。
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
遊戲裡,汪達憑著一把劍、一把弓、一匹馬,為愛.寧死不退。 現實中,為了生活、目標、所愛的家人,只能勇敢向前。
Thumbnail
關於片名   台灣片名《花漾女子》,原文片名《Promising Young Woman》,台灣譯名將時間定格在悲劇發生前,而原文片名則進一步帶我們看見另一個可能性結果
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作
Thumbnail
徵的就是你 🫵 超ㄅㄧㄤˋ 獎品搭配超瞎趴的四大主題,等你踹共啦!還有機會獲得經典的「偉士牌樂高」喔!馬上來參加本次的活動吧!
Thumbnail
隨著理財資訊的普及,越來越多台灣人不再將資產侷限於台股,而是將視野拓展到國際市場。特別是美國市場,其豐富的理財選擇,讓不少人開始思考將資金配置於海外市場的可能性。 然而,要參與美國市場並不只是盲目跟隨標的這麼簡單,而是需要策略和方式,尤其對新手而言,除了選股以外還會遇到語言、開戶流程、Ap
Thumbnail
這篇文章探討了在軟體開發中的技術債可能來自哪些原因,以及如何自動化偵測與修復技術債。作者透過分享不同情境下的技術債選擇,提供了對於技術債的思考與建議,針對開發人員在需要做出無奈的技術決策時,提供了一些建議。此外,還提供了一些在做出技術決策時的方法,如保留抽象層和避免vendor lock-in。
Thumbnail
今天來聊個最近很夯的主題 DDD,但不是 DDD 的本尊 Domain Driven Design,而是無所不在的 Database Driven Design,Database Driven Design 不是不好,只是你的模型容易變成貧血模型,邏輯都集中在 service 層等等。
Thumbnail
有趣的是,Model 其實沒什麼嚴格的定義,所以每個人對 Model 的解讀也不盡相同,有人覺得資料怎麼儲存屬於 Model 的一部份 (受 ORM 工具的影響),有人覺得工作流程 (workflow) 是 Model 的一部份,我個人也有自己的想法,而且隨專案的規模和特性,也不是總是一樣的。
Thumbnail
起源是當時 Facebook 有篇文章討論不少人分不清楚上述二者的差別,當時寫了首部曲《閒談軟體設計:API Naming Style》,接著是《閒談軟體設計:內部函式庫》,但始終沒談到 library 和 framework 的差別,主要是沒有好的例子,這次這例子還蠻不錯的。
Thumbnail
我自己偏好用 Repository 搭配 decorator 來管理 cache,而不是在 controller 層或是到處都有快取的邏輯,如果程式都是透過 Repository 更新資料,Repository 就會是一個不錯的地方更新快取,邏輯也就不會散亂在各處了。
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
遊戲裡,汪達憑著一把劍、一把弓、一匹馬,為愛.寧死不退。 現實中,為了生活、目標、所愛的家人,只能勇敢向前。
Thumbnail
關於片名   台灣片名《花漾女子》,原文片名《Promising Young Woman》,台灣譯名將時間定格在悲劇發生前,而原文片名則進一步帶我們看見另一個可能性結果
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作