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

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

前言

為什麼是煮拉麵呢?主題是來自前同事在問我為什麼有人的程式好像常常會歪掉,或是變得難維護,後續的討論中,他用的例子就是拉麵,所以... 今天就用程式來煮拉麵吧!
說真的,要煮出一碗好吃的拉麵,其實超級複雜的,網路上可以找到很多影片,下面是我找了一個蠻完整的影片,從熬製湯頭開始,滷製叉燒,到製麵,最後煮麵到上桌,步驟相當繁複。所以,這次我們只從影片的 13 分 35 秒開始,也就是真的煮一碗拉麵開始。

從簡單版開始

這次,雖然反而讓我想超久的,但我刻意不加入任何物件的概念,希望都是由 function 來完成這次的例子,另外,我知道可以用 SOLID、DIP 或是很多聽起來很專業的術語,但只要對解釋幫助不大的,我也盡量不用。先從第一個版本開始,我們用 cookRamen 來製作一碗拉麵:
一般來說,剛開始學程式,這樣寫沒什麼問題,甚至在正式工作上,能先用最簡單的方式將「功能」做對,我覺得也可以,醜但是對的程式,比起花俏但錯的程式有用。只是這樣的程式有什麼問題?
這沒有標準答案,全看用什麼角度來看,以我來說,這程式的抽象度不太夠,滿滿的細節,並沒有將 domain term 使用進去 (等等會解釋,如果不是很懂可以先暫時忽略這些術語)。另外,這個 cookRamen 只能煮豚骨拉麵,沒法煮鹽味拉麵、味增拉麵或是魚介拉麵。

加上變化

假設,先不管美醜,因為需求馬上又進來了,現在希望 cookeRamen 也可以煮鹽味拉麵,加上時間又很趕,於是有人可能選擇,讓 cookRamen 帶個參數,由參數決定煮什麼拉麵,如果是鹽味拉麵,那就是加入鹽醬汁以及雞高湯,完美,不用幾分鐘就改完上線了,因為速度很快,老闆也很開心。
那不是很好嗎?但軟體開發都是這樣的,後面會有越來越多的調整,除了少數只提供一種拉麵的店,大多都提供多種口味,麵的硬度、湯的濃淡都可以調整,像是我蠻喜歡的凪 Nagi,就有五種口味,以及很多可以調整的項目:
另外一個我也蠻喜歡的一幻,有三種口味搭四種湯頭,另外有一個特殊口味只使用雞白湯。
也就是說,如果繼續使用 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
53會員
104內容數
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
你可能也想看
Google News 追蹤
Thumbnail
隨著理財資訊的普及,越來越多台灣人不再將資產侷限於台股,而是將視野拓展到國際市場。特別是美國市場,其豐富的理財選擇,讓不少人開始思考將資金配置於海外市場的可能性。 然而,要參與美國市場並不只是盲目跟隨標的這麼簡單,而是需要策略和方式,尤其對新手而言,除了選股以外還會遇到語言、開戶流程、Ap
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
Thumbnail
晚餐想不到吃什麼時會去吃麵,雖說麵派或飯派應該是選飯。
Thumbnail
  好奇~大家吃泡麵都怎麼吃?   通常我都是加菜加蛋,肉的話,以前會加現在不會,太多了,吃不完。   網路上也很多奇怪的吃法,炒泡麵、加起司或牛奶是最常見的。最近出現附湯拿去蒸蛋的吃法,感覺不錯,但我家沒微波爐,而且泡麵碗也不知道能不能微波,好恐。   還有流傳已久的加布丁我完全
Thumbnail
週末帶媽媽出去吃晚餐,意外發現一家拉麵店,店員親切,餐點多樣化,拉麵選項豐富。試過舒肥牛塩味拉麵、黑蒜油豚骨拉麵,肉質嫩滑,湯頭層次豐富,更有意想不到的配料,讓對日式拉麵的刻板印象有所改變。下次一定要試試甜點巴斯克蛋糕。店家資訊:麵屋本心,地址:臺中市東區福仁街22號。
Thumbnail
目前在大阪有五間門市,京都也有新開一間。拉麵和炒飯都相當棒的拉麵店,拉麵主打鹽味拉麵,清澈的湯頭除了雞鮮味之外更多了濃縮至近乎柴魚調性的甘香,很讓人難以抗拒的好味道。
大家喜歡吃什麼樣的泡麵? 先加調味料還是先加麵?
Thumbnail
《田中麵廠》推出的四個口味,都是非常基本的基本款,雖然沒有創新,但好處是保守牌不太容易出錯,況且他們是代工起家,無論是麵條還是調味醬汁,都從多年的經驗裡找到美味密碼,因此試吃的四種口味都不錯。
Thumbnail
  接續上一篇文章,換言之,黃金泡菜就是配麵條的小菜,就跟去一般麵攤點滷味黑白切來搭配一樣,可以增添麵的口感,讓吃麵的過程中可以有不同的刺激。不過,其實不用加點泡菜也可以的,因為麵的內容本身就很完整。雖然長得跟切仔麵有點相像,但吃起來卻是完全不一樣的口感。而日本拉麵的行銷哲學,也是在吃拉麵
Thumbnail
  拉麵是一種容易好吃且單價又不會太高的料理,所以說當肚子餓的時候,經過拉麵店就會想要來上一碗,也不用像火鍋那樣還要放料進去等煮熟才能吃,而是可以直接享用的熱食料理。但拉麵大多是搭配過於濃厚的湯頭,雖然說不一定會把湯喝光光,卻也帶來讓身體負擔增加的隱憂。所以如果有其他的湯頭可以選擇,那當然
Thumbnail
   接續上一篇文章,湯頭越濃厚,就對味覺的刺激越大,就會認為好喝,但為什麼不能是清淡的湯頭呢?清淡的湯頭也是好喝的,更加趨近剛熬好的雞骨湯,應該也要試著去品味這樣的美好。同樣是店裡的料理之一,也是充滿著店家的自信之處。拉麵真的是一種很方便的料理,只要吃上一碗,就能夠快速補充熱量,又能繼續後
Thumbnail
隨著理財資訊的普及,越來越多台灣人不再將資產侷限於台股,而是將視野拓展到國際市場。特別是美國市場,其豐富的理財選擇,讓不少人開始思考將資金配置於海外市場的可能性。 然而,要參與美國市場並不只是盲目跟隨標的這麼簡單,而是需要策略和方式,尤其對新手而言,除了選股以外還會遇到語言、開戶流程、Ap
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
Thumbnail
晚餐想不到吃什麼時會去吃麵,雖說麵派或飯派應該是選飯。
Thumbnail
  好奇~大家吃泡麵都怎麼吃?   通常我都是加菜加蛋,肉的話,以前會加現在不會,太多了,吃不完。   網路上也很多奇怪的吃法,炒泡麵、加起司或牛奶是最常見的。最近出現附湯拿去蒸蛋的吃法,感覺不錯,但我家沒微波爐,而且泡麵碗也不知道能不能微波,好恐。   還有流傳已久的加布丁我完全
Thumbnail
週末帶媽媽出去吃晚餐,意外發現一家拉麵店,店員親切,餐點多樣化,拉麵選項豐富。試過舒肥牛塩味拉麵、黑蒜油豚骨拉麵,肉質嫩滑,湯頭層次豐富,更有意想不到的配料,讓對日式拉麵的刻板印象有所改變。下次一定要試試甜點巴斯克蛋糕。店家資訊:麵屋本心,地址:臺中市東區福仁街22號。
Thumbnail
目前在大阪有五間門市,京都也有新開一間。拉麵和炒飯都相當棒的拉麵店,拉麵主打鹽味拉麵,清澈的湯頭除了雞鮮味之外更多了濃縮至近乎柴魚調性的甘香,很讓人難以抗拒的好味道。
大家喜歡吃什麼樣的泡麵? 先加調味料還是先加麵?
Thumbnail
《田中麵廠》推出的四個口味,都是非常基本的基本款,雖然沒有創新,但好處是保守牌不太容易出錯,況且他們是代工起家,無論是麵條還是調味醬汁,都從多年的經驗裡找到美味密碼,因此試吃的四種口味都不錯。
Thumbnail
  接續上一篇文章,換言之,黃金泡菜就是配麵條的小菜,就跟去一般麵攤點滷味黑白切來搭配一樣,可以增添麵的口感,讓吃麵的過程中可以有不同的刺激。不過,其實不用加點泡菜也可以的,因為麵的內容本身就很完整。雖然長得跟切仔麵有點相像,但吃起來卻是完全不一樣的口感。而日本拉麵的行銷哲學,也是在吃拉麵
Thumbnail
  拉麵是一種容易好吃且單價又不會太高的料理,所以說當肚子餓的時候,經過拉麵店就會想要來上一碗,也不用像火鍋那樣還要放料進去等煮熟才能吃,而是可以直接享用的熱食料理。但拉麵大多是搭配過於濃厚的湯頭,雖然說不一定會把湯喝光光,卻也帶來讓身體負擔增加的隱憂。所以如果有其他的湯頭可以選擇,那當然
Thumbnail
   接續上一篇文章,湯頭越濃厚,就對味覺的刺激越大,就會認為好喝,但為什麼不能是清淡的湯頭呢?清淡的湯頭也是好喝的,更加趨近剛熬好的雞骨湯,應該也要試著去品味這樣的美好。同樣是店裡的料理之一,也是充滿著店家的自信之處。拉麵真的是一種很方便的料理,只要吃上一碗,就能夠快速補充熱量,又能繼續後