有點意外,上一篇 閒談軟體設計:來煮碗拉麵吧 回響還不錯,果然標題是很重要的 (咦~重點不應該是內容嗎?),本來這一篇是要來回答和前同事討論中的一個問題,什麼是 Dependency-Inversion Principle?但例子後來弄得有點太複雜了,解釋起來反而有點模糊焦點。
再仔細看了一下,也許可以用來完結先前一直未完成的 library vs. framework 的三部曲,起源是當時 Facebook 有篇文章討論不少人分不清楚上述二者的差別,當時寫了首部曲 閒談軟體設計:API Naming Style,接著是 閒談軟體設計:內部函式庫,但始終沒談到 library 和 framework 的差別,主要是沒有好的例子,這次這例子還蠻不錯的。同樣,一開始不講解理論,先從例子說起。
因為拉麵賣得很好,你的老闆開始想要開另一個海鮮拉麵品牌,注意,是一個新的品牌,不是一家新的分店喔。於是找你規劃,如何把目前成功的方式複製到另一個品牌,這時,你開始思考,怎樣煮好一碗拉麵?
客人點好餐,廚房照著食譜,開始熱碗、準備湯底、煮麵、準備風味油、加入高湯,最後放入配料。
接著,陷入沉思,什麼是目前的品牌有的,但另一個新品牌不會有,或是目前品牌沒有的,但新品牌會有的?
目前品牌有兩種口味:豚骨拉麵和鹽味拉麵,高湯有豚骨湯和雞湯,客人可以調製濃度,湯底則有醬油與鹽味,風味油有豬背油與蔥油,麵是細直麵,分三種不同的硬度;新的品牌暫定只有一種口味,高湯是魚干高湯,客人無法調濃度,湯底則是昆布鹽,風味油是蝦油,麵則是細捲麵,硬度則提供軟、適中、硬和超硬四種。
於是你發現到,高湯和風味油可能會是差最多的,其他流程依然很相似,於是決定把流程的定下來,讓新品牌的主廚來決定實際的內容,並且將高湯和風味油的製作也委託新品牌的主廚。
為了讓新主廚有個可參考的範本,首先,從上面的引述的文字 (亦可稱之為 problem statement,一般是 OOAD 開始的起點,但我蠻懷疑實務上真的有人用這方式嗎?笑),得到一個設計:

VendingMachine(借用自動販賣機) 處理使用者輸入的選項,轉成訂單RamenOrder代表使用者的訂餐,會轉成食譜RamenRecipe代表拉麵的食譜,主要是用cook()函式來煮拉麵,餐廳可以覆寫 (overwrite) 已經定義好的幾個函式 (class diagram 是用 abstract class 表示,但實際的範例程式碼則是用 Java 特有的 interface 加 default method)SoupMaker及ScentedOilMaker若餐廳有較複雜的湯及風味油調理,可以提供多種實作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 則是使用框架的兩個實例。



















