有點意外,上一篇 閒談軟體設計:來煮碗拉麵吧 回響還不錯,果然標題是很重要的 (咦~重點不應該是內容嗎?),本來這一篇是要來回答和前同事討論中的一個問題,什麼是 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),因此漸漸大家也就將兩字交互使用,若真要說差別,大概有兩個:
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 其實早已經結束的現象。
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
則是使用框架的兩個實例。