閒談軟體設計:State 與語言

閱讀時間約 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:這個部分可能因為不同的設定,行為不一樣,有的是直接找零然後回到 {等待狀態})。

53會員
104內容數
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
留言0
查看全部
發表第一個留言支持創作者!
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
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
美國總統大選只剩下三天, 我們觀察一整週民調與金融市場的變化(包含賭局), 到本週五下午3:00前為止, 誰是美國總統幾乎大概可以猜到60-70%的機率, 本篇文章就是以大選結局為主軸來討論近期甚至到未來四年美股可能的改變
Thumbnail
Faker昨天真的太扯了,中國主播王多多點評的話更是精妙,分享給各位 王多多的點評 「Faker是我們的處境,他是LPL永遠繞不開的一個人和話題,所以我們特別渴望在決賽跟他相遇,去直面我們的處境。 我們曾經稱他為最高的山,最長的河,以為山海就是盡頭,可是Faker用他28歲的年齡...
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
遊戲裡,汪達憑著一把劍、一把弓、一匹馬,為愛.寧死不退。 現實中,為了生活、目標、所愛的家人,只能勇敢向前。
Thumbnail
關於片名   台灣片名《花漾女子》,原文片名《Promising Young Woman》,台灣譯名將時間定格在悲劇發生前,而原文片名則進一步帶我們看見另一個可能性結果
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作
Thumbnail
那天我問隊友,怎樣才算是一部小說呢?按字數計算嗎? 他說:「故事內起承轉合都有,就算」 所以我大膽地按著他的標準,將自己寫過的故事,粗略整理出一個明細。
纏中說禪,本名李彪,專欄筆名木子,其人是中國股市比較早期的操盤手,所以他比較熟悉a股的市場情況。他以“纏中說禪”為筆名,從2002年開始寫博客,直到2008年癌症病重停更,期間寫下了不少文章。而博客文章中最為著名的就是他的“教你炒股票”系列文章,他在這個系列裡講到的炒股理論和方法被粉絲稱為“纏論
Thumbnail
婚姻是人生大事,對溥儀尤其如此,因為如果皇帝大婚,就代表溥儀可以脫離眾多便宜老媽的束縛而得以親政。 但詭異的是,這個可以讓他脫離便宜老媽掌控的婚姻,卻還是要由便宜老媽進行主導並且居中角力......
Thumbnail
上次我提到:溥儀就是個死小孩。其實這不能全怪溥儀,而要怪詭異的宮廷教育及生活制度......
Thumbnail
近期電影「末代皇帝」重新修復上映。 為了推坑這部經典之作,本人決定以溥儀本身的自傳《我的前半生》為主要基底,和大家談一些電影中礙於篇幅或是藝術改編,而不容易察覺或是沒有呈現的真實歷史。
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
美國總統大選只剩下三天, 我們觀察一整週民調與金融市場的變化(包含賭局), 到本週五下午3:00前為止, 誰是美國總統幾乎大概可以猜到60-70%的機率, 本篇文章就是以大選結局為主軸來討論近期甚至到未來四年美股可能的改變
Thumbnail
Faker昨天真的太扯了,中國主播王多多點評的話更是精妙,分享給各位 王多多的點評 「Faker是我們的處境,他是LPL永遠繞不開的一個人和話題,所以我們特別渴望在決賽跟他相遇,去直面我們的處境。 我們曾經稱他為最高的山,最長的河,以為山海就是盡頭,可是Faker用他28歲的年齡...
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
遊戲裡,汪達憑著一把劍、一把弓、一匹馬,為愛.寧死不退。 現實中,為了生活、目標、所愛的家人,只能勇敢向前。
Thumbnail
關於片名   台灣片名《花漾女子》,原文片名《Promising Young Woman》,台灣譯名將時間定格在悲劇發生前,而原文片名則進一步帶我們看見另一個可能性結果
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作
Thumbnail
那天我問隊友,怎樣才算是一部小說呢?按字數計算嗎? 他說:「故事內起承轉合都有,就算」 所以我大膽地按著他的標準,將自己寫過的故事,粗略整理出一個明細。
纏中說禪,本名李彪,專欄筆名木子,其人是中國股市比較早期的操盤手,所以他比較熟悉a股的市場情況。他以“纏中說禪”為筆名,從2002年開始寫博客,直到2008年癌症病重停更,期間寫下了不少文章。而博客文章中最為著名的就是他的“教你炒股票”系列文章,他在這個系列裡講到的炒股理論和方法被粉絲稱為“纏論
Thumbnail
婚姻是人生大事,對溥儀尤其如此,因為如果皇帝大婚,就代表溥儀可以脫離眾多便宜老媽的束縛而得以親政。 但詭異的是,這個可以讓他脫離便宜老媽掌控的婚姻,卻還是要由便宜老媽進行主導並且居中角力......
Thumbnail
上次我提到:溥儀就是個死小孩。其實這不能全怪溥儀,而要怪詭異的宮廷教育及生活制度......
Thumbnail
近期電影「末代皇帝」重新修復上映。 為了推坑這部經典之作,本人決定以溥儀本身的自傳《我的前半生》為主要基底,和大家談一些電影中礙於篇幅或是藝術改編,而不容易察覺或是沒有呈現的真實歷史。