閒談軟體設計:來煮碗拉麵吧

更新於 發佈於 閱讀時間約 10 分鐘

前言

為什麼是煮拉麵呢?主題是來自前同事在問我為什麼有人的程式好像常常會歪掉,或是變得難維護,後續的討論中,他用的例子就是拉麵,所以... 今天就用程式來煮拉麵吧!

說真的,要煮出一碗好吃的拉麵,其實超級複雜的,網路上可以找到很多影片,下面是我找了一個蠻完整的影片,從熬製湯頭開始,滷製叉燒,到製麵,最後煮麵到上桌,步驟相當繁複。所以,這次我們只從影片的 13 分 35 秒開始,也就是真的煮一碗拉麵開始。


從簡單版開始

這次,雖然反而讓我想超久的,但我刻意不加入任何物件的概念,希望都是由 function 來完成這次的例子,另外,我知道可以用 SOLID、DIP 或是很多聽起來很專業的術語,但只要對解釋幫助不大的,我也盡量不用。先從第一個版本開始,我們用 cookRamen 來製作一碗拉麵:

一般來說,剛開始學程式,這樣寫沒什麼問題,甚至在正式工作上,能先用最簡單的方式將「功能」做對,我覺得也可以,醜但是對的程式,比起花俏但錯的程式有用。只是這樣的程式有什麼問題?

這沒有標準答案,全看用什麼角度來看,以我來說,這程式的抽象度不太夠,滿滿的細節,並沒有將 domain term 使用進去 (等等會解釋,如果不是很懂可以先暫時忽略這些術語)。另外,這個 cookRamen 只能煮豚骨拉麵,沒法煮鹽味拉麵、味增拉麵或是魚介拉麵。

加上變化

假設,先不管美醜,因為需求馬上又進來了,現在希望 cookeRamen 也可以煮鹽味拉麵,加上時間又很趕,於是有人可能選擇,讓 cookRamen 帶個參數,由參數決定煮什麼拉麵,如果是鹽味拉麵,那就是加入鹽醬汁以及雞高湯,完美,不用幾分鐘就改完上線了,因為速度很快,老闆也很開心。

那不是很好嗎?但軟體開發都是這樣的,後面會有越來越多的調整,除了少數只提供一種拉麵的店,大多都提供多種口味,麵的硬度、湯的濃淡都可以調整,像是我蠻喜歡的凪 Nagi,就有五種口味,以及很多可以調整的項目:

raw-image

另外一個我也蠻喜歡的一幻,有三種口味搭四種湯頭,另外有一個特殊口味只使用雞白湯。

raw-image

也就是說,如果繼續使用 if-else 或是 switch-case,即便將參數細化,最後只會讓程式變成一個超恐怖,難以維護的怪物,如果又因為沒時間沒有寫測試案例支撐,之後連修改都會膽顫心驚。

建立抽象

所以,也許打從一開始,我們提供的抽象就是錯的,事實上,這回到一開始我說的,抽象程度不夠,缺少 domain 的 term,如果去問拉麵師傅,或是問拉麵迷,問他們拉麵是怎麼煮的,他們也不會說的這麼細,相反,他們可能會這樣說 (這只是個範例,我不會煮拉麵,只是看了很多漫畫得到的冷知識,想學美食相關的知識,可以看《美味大挑戰》,第 38 集有很多拉麵相關的知識):

  • 加熱碗 (冷的碗會讓高湯變冷,喝起來會覺得比較鹹)
  • 調製醬汁 (或叫湯底)
  • 調製風味油 (決定湯頭風味的關鍵)
  • 加入高湯 (濃淡也是在這個步驟調整)
  • 煮麵 (時間決定硬度,越硬越地道,最好麵心還有點生)
  • 加入煮好的麵跟配料

會發現,這個說法不像我們第一個版本那樣充滿細節,這就是一種抽象,將原本很複雜很細瑣的東西,用更高層次的方式去描述,使事情更容易理解,而抽象化的過程中,就會看到 domain expert 用哪些詞彙描述一件事,而這些詞彙便是 domain term。

因此,我們可以用這些 domain term 改寫 (refactor) 第一個版本的程式碼,得到一個更容易讀的版本:

然後,會發現另一件事,即便是要同時能煮豚骨拉麵、味增拉麵、鹽味拉麵或是魚介拉麵,雖然不同的拉麵在某些步驟裡的細節可能不同,但主流程都是類似的,這時要設計彈性的組合也會覺得便容易了,例如:

到目前為止,透過將流程抽象化,搭配參數,已經讓 cookeRamen 變成一個還不錯的程式,要增加味增拉麵,在不需要改動主流程的情況下,能夠很輕鬆地就完成。

優化抽象

但現實有時候會更加複雜一點,而且雖然能做出豚骨拉麵、醬油拉麵和味增拉麵,非常有彈性,但也有一些問題:

  • 程式看不出來這家店有提供什麼拉麵,也可以解讀成,我們仍然是用接近程式的語言在描述問題,而不是接近 domain 的語言在描述問題。
  • 如果要限制醬汁與湯頭的組合,例如味增不能搭配雞高湯 (我隨便說說的,這組合應該還是很好吃),似乎在不改動主流程的情況下做不到?
  • 為了滿足不同喜好的客人,高湯要能有不同的準備方式,例如兌水降低濃度,或是用 1.5 倍的湯煮成更濃的湯,這是行為上的不同,無法簡單用參數就可以替換。

上述的問題中,第三個相較是比較容易解決的,既然無法用單純的參數,那乾脆就把製作的方式當作參數:

如此一來,我們就可以煮出一碗濃的豚骨拉麵和一碗正常濃度的鹽味拉麵,這裡稍微用了 JavaScript closure 的小技巧,其實用跟不用都可以,只是這裡使用 closure 的用意是,提供一個對 cookRamen 來說,不論之後因為其他原因要改變實作,仍然一致的 function signature:一個沒有參數的 function 。如此一來,cookRamen 永遠只需呼叫 prepareSoup 即可。有人猜出來這是什麼了嗎?

讓程式說行話

解決了第三個問題,再回頭看第一個問題,以目前第五個版本來說,我覺得已經是相當不錯了,硬要雞蛋裡挑骨頭的話,就是 options 這個字,不太像是廚房裡會用的詞彙,如果真要選一個字,也許 Recipe 食譜會合適一點。

所以只是把 options 換成 recipe?當然不是,一般來說聽到食譜,會覺得食譜是什麼?指定的食材加上有一定順序的執行步驟,聽起來,好像可以搭配一起解決第二個問題,加入食譜的概念:

在這個版本中,除了 recipe 之外,也加入了 order 的概念,order 就是消費者填寫的那張紙,上面可以選擇口味 (flavor)、醬汁(部分口味可選)、麵的硬度、湯的濃度、配料等等。此外,我們也在程式中可以知道這家店目前提供兩種口味:豚骨拉麵與醬油拉麵,以及這兩種口味各別的食譜,若仔細看食譜會發現,醬油拉麵並沒有加豬背脂,而是在最後配料加完後,淋上滾燙的蔥油,且醬汁跟湯頭都是固定的。

如此,我們在不用 if-else 與 switch-case 的情況下 (個人對 switch-case 的想法可參考《閒談軟體架構:Switch 壞味道》),完成了能夠煮出不同口味、麵的硬度、湯的濃度等各式組合的程式,某種程度上,每個 function 應該都非常易懂,在每個層級上都提供了合適的抽象:

  • 將訂單轉成食譜
  • 然後 cookRamen 照著食譜煮
  • 每個食譜都是用合乎餐廳慣用語的步驟
  • 每個步驟則是由可執行的細節組成

要新增一個新口味時,只需要組合既有的步驟即可,不會影響到其他既有的口味。

到這裡為止,我覺得目前的版本算是一個還蠻不錯的寫法。裡面其實用了很多可以拿來說嘴的東西,像是 strategy pattern (大家覺得哪邊是 strategy pattern 呢?),又或者是 dependency injection (這裡使用哪幾種 injection) 等等,但我如果先講 strategy pattern 然後再講怎麼使用在例子中,似乎幫助不大,畢竟光講完 intent、context 等理論,很多人就已經受不了想睡覺了。

反而這次,不斷地用拆解加組合,慢慢一步一步 refactor 的方式,寫出我心目中的樣子,我自己覺得反而更容易解說為什麼要這樣寫,即便沒有剛剛那一段的描述,應該還是可以看得懂怎麼去改善一段程式。

結論

至於所謂資深的工程師是否第一次就要寫出最終版的樣子,我覺得倒未必,視已知的需求而定,但最起碼,要能寫到 v3 的樣子,然後能夠依需求的變化,改寫到 v6 或是更適合新需求的樣子。如果需求就是只煮一種拉麵,花很多時間寫出 v6 其實效益不大,但我個人不會寫出 v1,因為不好讀。切記,只要是有一定複雜度的需求,就沒有一次就寫好的程式,重點是如何隨著需求變化,不斷地改善程式。


番外篇

好了,時間到這邊也差不多該收尾了,這次雖然用 JavaScript 設計例子,但真的要用 Java 或是語言改寫成由 interface/class 組成的程式也很簡單,有興趣的可以自己試試。最後,煮拉麵的程式不只有一種寫法,上面有六種不同的版本,我相信有其他更好的版本,如果看官覺得你的方式更好,也歡迎來煮碗拉麵並分享吧!

大概用了半小時,把 Java 的版本寫出來,注意,主要用 interface、enum 和 record,盡可能簡潔寫,但行數確實比 JavaScript 的版本多一些。


夜深了,來碗雞白湯拉麵當消夜吧!


後記

這是首篇從 Medium 搬過來的文章,整體體驗跟 Medium 還蠻像的,但有幾點比較可惜,貼上 gist 和 YouTube 連結時,似乎無法像 Medium 自動轉換,最可惜的應該是程式碼區塊吧,Medium 最近改版的程式碼區塊很好用,在新文章中我都懶得用 gist,但這邊的程式碼區塊還是得嵌入 gist。


留言
avatar-img
留言分享你的想法!
avatar-img
Spirit的沙龍
53會員
104內容數
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
Spirit的沙龍的其他內容
2024/03/23
這篇文章探討了在軟體開發中的技術債可能來自哪些原因,以及如何自動化偵測與修復技術債。作者透過分享不同情境下的技術債選擇,提供了對於技術債的思考與建議,針對開發人員在需要做出無奈的技術決策時,提供了一些建議。此外,還提供了一些在做出技術決策時的方法,如保留抽象層和避免vendor lock-in。
Thumbnail
2024/03/23
這篇文章探討了在軟體開發中的技術債可能來自哪些原因,以及如何自動化偵測與修復技術債。作者透過分享不同情境下的技術債選擇,提供了對於技術債的思考與建議,針對開發人員在需要做出無奈的技術決策時,提供了一些建議。此外,還提供了一些在做出技術決策時的方法,如保留抽象層和避免vendor lock-in。
Thumbnail
2024/03/09
今天來聊個最近很夯的主題 DDD,但不是 DDD 的本尊 Domain Driven Design,而是無所不在的 Database Driven Design,Database Driven Design 不是不好,只是你的模型容易變成貧血模型,邏輯都集中在 service 層等等。
Thumbnail
2024/03/09
今天來聊個最近很夯的主題 DDD,但不是 DDD 的本尊 Domain Driven Design,而是無所不在的 Database Driven Design,Database Driven Design 不是不好,只是你的模型容易變成貧血模型,邏輯都集中在 service 層等等。
Thumbnail
2024/03/02
有趣的是,Model 其實沒什麼嚴格的定義,所以每個人對 Model 的解讀也不盡相同,有人覺得資料怎麼儲存屬於 Model 的一部份 (受 ORM 工具的影響),有人覺得工作流程 (workflow) 是 Model 的一部份,我個人也有自己的想法,而且隨專案的規模和特性,也不是總是一樣的。
Thumbnail
2024/03/02
有趣的是,Model 其實沒什麼嚴格的定義,所以每個人對 Model 的解讀也不盡相同,有人覺得資料怎麼儲存屬於 Model 的一部份 (受 ORM 工具的影響),有人覺得工作流程 (workflow) 是 Model 的一部份,我個人也有自己的想法,而且隨專案的規模和特性,也不是總是一樣的。
Thumbnail
看更多
你可能也想看
Thumbnail
大家好,我是一名眼科醫師,也是一位孩子的媽 身為眼科醫師的我,我知道視力發展對孩子來說有多關鍵。 每到開學季時,診間便充斥著許多憂心忡忡的家屬。近年來看診中,兒童提早近視、眼睛疲勞的案例明顯增加,除了3C使用過度,最常被忽略的,就是照明品質。 然而作為一位媽媽,孩子能在安全、舒適的環境
Thumbnail
大家好,我是一名眼科醫師,也是一位孩子的媽 身為眼科醫師的我,我知道視力發展對孩子來說有多關鍵。 每到開學季時,診間便充斥著許多憂心忡忡的家屬。近年來看診中,兒童提早近視、眼睛疲勞的案例明顯增加,除了3C使用過度,最常被忽略的,就是照明品質。 然而作為一位媽媽,孩子能在安全、舒適的環境
Thumbnail
我的「媽」呀! 母親節即將到來,vocus 邀請你寫下屬於你的「媽」故事——不管是紀錄爆笑的日常,或是一直想對她表達的感謝,又或者,是你這輩子最想聽她說出的一句話。 也歡迎你曬出合照,分享照片背後的點點滴滴 ♥️ 透過創作,將這份情感表達出來吧!🥹
Thumbnail
我的「媽」呀! 母親節即將到來,vocus 邀請你寫下屬於你的「媽」故事——不管是紀錄爆笑的日常,或是一直想對她表達的感謝,又或者,是你這輩子最想聽她說出的一句話。 也歡迎你曬出合照,分享照片背後的點點滴滴 ♥️ 透過創作,將這份情感表達出來吧!🥹
Thumbnail
為什麼是煮拉麵呢?主題是來自前同事在問我為什麼有人的程式好像常常會歪掉,或是變得難維護,後續的討論中,他用的例子就是拉麵,所以... 今天就用程式來煮拉麵吧!
Thumbnail
為什麼是煮拉麵呢?主題是來自前同事在問我為什麼有人的程式好像常常會歪掉,或是變得難維護,後續的討論中,他用的例子就是拉麵,所以... 今天就用程式來煮拉麵吧!
Thumbnail
廚藝,是創意。 但在你成就創意之前,須先模仿。模仿是... 把你想吃的,想做的,基本功先學好,並且清楚了解完成模仿之後,你能有幾分像? 你買來吃過,看家人做過,你想吃,但沒做過,或者做不好,或者總是差一點成功,那就執著的累積 差點成功的經驗。 所謂~差點成功的經驗,也不過就是一種你吃別人比較好吃,自
Thumbnail
廚藝,是創意。 但在你成就創意之前,須先模仿。模仿是... 把你想吃的,想做的,基本功先學好,並且清楚了解完成模仿之後,你能有幾分像? 你買來吃過,看家人做過,你想吃,但沒做過,或者做不好,或者總是差一點成功,那就執著的累積 差點成功的經驗。 所謂~差點成功的經驗,也不過就是一種你吃別人比較好吃,自
Thumbnail
菜鳥小廚娘總是有一個 廚師魂。 老幻想一些無厘頭的菜單,尤其是 自以為是 清單(哈哈)。 做菜,是一種想法,一種創意,一種如果這樣煮能吃嗎,那樣煮好吃嗎.... 有一種做菜,就是..大家都喜歡這樣吃,所以一直煮這樣的菜色。 有一種做菜,就是..這個沒人試過,後來覺得這樣也不錯吃,所以隨心所欲地玩煮樣
Thumbnail
菜鳥小廚娘總是有一個 廚師魂。 老幻想一些無厘頭的菜單,尤其是 自以為是 清單(哈哈)。 做菜,是一種想法,一種創意,一種如果這樣煮能吃嗎,那樣煮好吃嗎.... 有一種做菜,就是..大家都喜歡這樣吃,所以一直煮這樣的菜色。 有一種做菜,就是..這個沒人試過,後來覺得這樣也不錯吃,所以隨心所欲地玩煮樣
Thumbnail
記得曾經說過,誰說煮泡麵不能寫一篇?這就來一篇~ 最近總算是把搬家這件事搞定,冰箱也漸漸被老婦我填滿,已經開始替搞怪孩子做車上吃的早餐、晚餐,還有替她做專屬便當⋯然後咱倆老就吃泡麵⋯⋯ 啊當然不是啦~
Thumbnail
記得曾經說過,誰說煮泡麵不能寫一篇?這就來一篇~ 最近總算是把搬家這件事搞定,冰箱也漸漸被老婦我填滿,已經開始替搞怪孩子做車上吃的早餐、晚餐,還有替她做專屬便當⋯然後咱倆老就吃泡麵⋯⋯ 啊當然不是啦~
Thumbnail
有點糟糕,書寫計畫才進行到第四天,就不知道要寫什麼了;是因為生活太貧乏了嗎?還是,實在是最近一直在抱怨一些事情,覺得自己不能再寫這些東西了。 恩,那就來說一下昨天煮飯時的小心得。 昨天中午煮了一道炒烏龍麵。 食材有:松阪豬肉絲、花枝丸、魚板、毛豆、高麗菜、香菇絲、金勾蝦。首先,加入一點油熱鍋,再把豬
Thumbnail
有點糟糕,書寫計畫才進行到第四天,就不知道要寫什麼了;是因為生活太貧乏了嗎?還是,實在是最近一直在抱怨一些事情,覺得自己不能再寫這些東西了。 恩,那就來說一下昨天煮飯時的小心得。 昨天中午煮了一道炒烏龍麵。 食材有:松阪豬肉絲、花枝丸、魚板、毛豆、高麗菜、香菇絲、金勾蝦。首先,加入一點油熱鍋,再把豬
Thumbnail
再3.4天前開始想菜單、材料、流程、器具、朋友家可利用的東西.....很燒腦啊!
Thumbnail
再3.4天前開始想菜單、材料、流程、器具、朋友家可利用的東西.....很燒腦啊!
Thumbnail
在加拿大待過一陣子,因為外食又貴又不方便又沒台灣的食物好吃,所以常常自己下廚,懶惰的時候做個粥、泡麵、水煮蛋、果醬麵包......等懶人料理,但當室友點菜的時候或是有空的時候,就會找個時間好好.....
Thumbnail
在加拿大待過一陣子,因為外食又貴又不方便又沒台灣的食物好吃,所以常常自己下廚,懶惰的時候做個粥、泡麵、水煮蛋、果醬麵包......等懶人料理,但當室友點菜的時候或是有空的時候,就會找個時間好好.....
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News