閒談軟體設計:再來一碗

2024/02/24閱讀時間約 8 分鐘

有點意外,上一篇 閒談軟體設計:來煮碗拉麵吧 回響還不錯,果然標題是很重要的 (咦~重點不應該是內容嗎?),本來這一篇是要來回答和前同事討論中的一個問題,什麼是 Dependency-Inversion Principle?但例子後來弄得有點太複雜了,解釋起來反而有點模糊焦點。

再仔細看了一下,也許可以用來完結先前一直未完成的 library vs. framework 的三部曲,起源是當時 Facebook 有篇文章討論不少人分不清楚上述二者的差別,當時寫了首部曲 閒談軟體設計:API Naming Style,接著是 閒談軟體設計:內部函式庫,但始終沒談到 library 和 framework 的差別,主要是沒有好的例子,這次這例子還蠻不錯的。同樣,一開始不講解理論,先從例子說起。


因為拉麵賣得很好,你的老闆開始想要開另一個海鮮拉麵品牌,注意,是一個新的品牌,不是一家新的分店喔。於是找你規劃,如何把目前成功的方式複製到另一個品牌,這時,你開始思考,怎樣煮好一碗拉麵?

客人點好餐,廚房照著食譜,開始熱碗、準備湯底、煮麵、準備風味油、加入高湯,最後放入配料。

接著,陷入沉思,什麼是目前的品牌有的,但另一個新品牌不會有,或是目前品牌沒有的,但新品牌會有的?

目前品牌有兩種口味:豚骨拉麵和鹽味拉麵,高湯有豚骨湯和雞湯,客人可以調製濃度,湯底則有醬油與鹽味,風味油有豬背油與蔥油,麵是細直麵,分三種不同的硬度;新的品牌暫定只有一種口味,高湯是魚干高湯,客人無法調濃度,湯底則是昆布鹽,風味油是蝦油,麵則是細捲麵,硬度則提供軟、適中、硬和超硬四種。

於是你發現到,高湯和風味油可能會是差最多的,其他流程依然很相似,於是決定把流程的定下來,讓新品牌的主廚來決定實際的內容,並且將高湯和風味油的製作也委託新品牌的主廚。

為了讓新主廚有個可參考的範本,首先,從上面的引述的文字 (亦可稱之為 problem statement,一般是 OOAD 開始的起點,但我蠻懷疑實務上真的有人用這方式嗎?笑),得到一個設計:

raw-image


  • VendingMachine (借用自動販賣機) 處理使用者輸入的選項,轉成訂單
  • RamenOrder 代表使用者的訂餐,會轉成食譜
  • RamenRecipe 代表拉麵的食譜,主要是用 cook() 函式來煮拉麵,餐廳可以覆寫 (overwrite) 已經定義好的幾個函式 (class diagram 是用 abstract class 表示,但實際的範例程式碼則是用 Java 特有的 interface 加 default method)
  • SoupMakerScentedOilMaker 若餐廳有較複雜的湯及風味油調理,可以提供多種實作
  • RamenStore 提供一個 start() 代表開店,不停地接 VendingMachine 產生的訂單,然後煮拉麵。

接著,開始 refactor 原先的拉麵品牌 Okawari (再來一碗) 的程式碼,為了能符合上面的設計,於是提供一個 OkwariVendingMachine 來處理口味、湯底、濃度和、麵的硬度等不同的組合,這裡使用 JCommander 來處理參數的解析 (parsing),於是可以用 -flavor=tonkotsu -c=stronger -hardness=hard 建立一張鹽味豚骨拉麵、湯濃麵應的訂單。

然後,將原先的程式碼 (詳見閒談軟體設計:來煮碗拉麵吧最後 Java 的版本),根據新的設計進行 refactor,若仔細看會發現,大多只是搬來搬去,例如:風味油的調製變成 lambda 物件傳入 recipe 中,在各自的 cook 中在對的順序呼叫 prepareScentedOil,移除不再需要的 preparePorkScentedOil 和 prepareScallionScentedOil,並將高湯、濃度、湯底、麵的硬度、口味、食譜及訂單的選項變成獨立的檔案 (完整程式碼詳見 GitHub repo)。

最後,提供 OkawariRamenStore 程式的入口,用 OkawariVendingMachine 建立一個 RamenStore 的實體,呼叫 start() 就開店了。

一切運作順利,接下來就是換新的品牌準備要開店了。


新品牌 Tako Ramen (小八拉麵店,是的,取自海賊王中的小八) 的主廚在看了範本後,依樣畫葫蘆,建立了一個 TakoVendingMachine,由於只有一種口味、一種高湯和一種湯底,所以販賣機只需要處理麵的硬度:

然後定義,TakoRamenOrder 及 TakoRamenReceipe 等類別。由於,新品牌的廚房導入保溫碗櫃,所以拿出來的時候,碗就是熱的,不用再加熱,因此也不需要呼叫 heatBowl

很快地,小八拉麵店就開張了,老闆相當開心。

看完了上面的例子,如果想要再開一個新的品牌,大家覺得會很難嗎?


在這例子中,okawai-store 的 maven 設定中正好加了兩個 dependencies,一個是 ramen-store-framework,另一個則是 jcommander,也正好分別一個是 framework 一個是 libaray。可能有人會問,看不出差別是什麼啊?

也因為差異沒那麼明顯,加上很多廠商也都稱自家的 library 為 framework (事實上 Apple 的共享套件也稱為 framework),因此漸漸大家也就將兩字交互使用,若真要說差別,大概有兩個:

  • framework 通常會主導流程 (或控制程序),以剛剛例子為例,當呼叫 start() 後,process 的控制權就交給 framework,因為 start() 內部是個無窮迴圈,後面的程式碼再也不會執行,除非 framework 想要結束執行並放棄執行權,這時 process 控制權才又會回到使用 framework 的程式手上。反之,library 不會持續掌握控制權,library 的使用者通常呼叫 library 的函式後會馬上得到想要的結果,並返回控制權。

Java Swing 倒是有點特別,main thread 在執行 JFrame.setVisible(true) 後 會繼續執行後面的程式,Java Swing 會啟動一個 event loop 專用的 thread 處理 UI 的繪製與事件的處理,一般來說,後面通常沒有其他程式,main thread 會結束,JVM 的 process 則是會等到所有的 thread 都結束後才會結束,變成視窗仍在提供服務,但 main thread 其實早已經結束的現象。

  • framework 一般來說會有個解決問題的核心思想,開發者以 framework 建議的方式在 framework 的基礎上加入你想要的功能,像是提供某個介面的實作。以 Java Swing 為例,它便是提供如何處理 GUI 的呈現與事件處理的解決方案,想處理按鈕,可以提供一個 Action 的實作,想呈現表格,可以提供一個 TableModel 的實作。回到拉麵的例子,為 RamenStore 提供 VendingMachine 的實作。反觀,library 比較像是一把瑞士刀,提供多種工具讓開發者可以使用並解決問題,比較沒有一個非得怎麼處理問題的核心思想,例如 Apache 的 Commons IO 提供非常多有用的函式進行 I/O 的存取,但並沒有規定你的應用程式該怎麼寫。

當然,上論兩個差異是我比較 old school 的想法,這年頭,這兩者的差異越來越不明顯了,以 reuse 的角度來說,framework 或是 library 都是開發程式時不可少的重要組成。最後,這裡放上 Wikipedia 的說法作為結束。


本來還要聊 strategy pattern,但拉麵吃多了有點膩,所以下次換個例子來聊聊 strategy pattern 吧!至於原本想聊的 Dependency-Inversion Principle 呢?我得再想想有沒有更好的例子。


完整的範例程式可在我的 GitHub 找到:ramen-store,分成三個目錄 (Maven module),ramen-store-framework 是框架,okawari-store 和 tako-store 則是使用框架的兩個實例。

51會員
100內容數
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
留言0
查看全部
發表第一個留言支持創作者!