此為軟體設計模式輕鬆小品,附圖是一張早餐店菜單的部分截圖。
在我首次造訪店面時,發現店裡的菜單複雜度實在是有點高啊。我當時實在是花了太多的時間在做「選擇」,以軟體的術語來說——我是在考慮怎麼「組合物件」,或是苦思著如何從他提供的「物件結構」來反推 API 的正確使用方法。
在數分鐘後我才突然意識到,Wow 原來左邊那一排 A, B, C, D 叫做「麵包」,而右邊那一排「蔬菜、嫩彈、玉米蛋…」叫做「主食」。而麵包可以和主食自由搭配,價錢為「麵包」和「主食」價格的加總,或是有一些特殊定價規則(如:搭配蛋餅都不額外加價,但只有蔬菜蛋餅要補足到 $40)。
於是我就依照我的直覺點了「(A) 香Q蛋餅 + 爆漿乳酪起士」,因為我實在太喜歡吃起司蛋餅,更何況是爆漿乳酪起士。
沒想到,金額一算起來竟然要價 $90 !!!
等等!起司蛋餅明明只要 $35,可是爆漿乳酪起士蛋餅竟然要價 $90!這到底是怎麼回事?從這邊我似乎聞到了「套用完某設計模式之後會有的後果 (Resulting Context) 」,於是就和身邊友人開了個玩笑:「哇靠,這是套了 XXX 模式吧!」
因此就誕生了這篇小品文,讓我用物件導向分析的角度來展開這個推理。
在進入下文之前,各位可以先以個人的角度來說說看,你覺得這是什麼設計模式呢?
物件導向分析
這份菜單其實「複雜度」滿高的,如果是一份正常的早餐店菜單大致上看起來比較像以下這樣,只具備著各項單品:
- 玉米蛋餅:$40
- 里肌蛋土司: $40
- 火腿漢堡:$55
每一項單品都具備名字和價格這兩個屬性,並且會有它獨特的口味繪製初版的類別圖如下。
這種單品制的方式早已滿足了一般的早餐店的需求了。
但是本文這間特殊的早餐店卻面臨著一道使這間早餐店不願甘於平凡的單品制菜單的因素:「主廚愛亂搞啊!」
從這個「主廚愛亂搞」的項目中可以推論說:「嗯,這間店的主廚一定很喜歡發明各種餐點組合。」
我們來仔細揣摩一下主廚的想法,他到底是怎麼想的?
「口味!變化多端的口味最重要!一陳不變的口味是無法留住客人的。那要怎麼做才好呢?」
「嗯⋯⋯我認為每一位客人,在享用餐點時,都是先感受到了『某種主要的味道』,而味道會接著『落在某種口感』之上,像是嚼勁或是某種澱粉製的飽足感。」
「怎麼說呢,因此每一道單品,就都該由一個主要的味道襯托而成,像是起司、火腿、香腸等等,而這些味道⋯⋯落在了不同的餅皮、麵包或是漢堡之上。」
「如果這位客人對味道膩了,我希望能允許他選擇不同的味道。那當然,也希望能允許他搭配不同的口感。」。
因此,早餐店的菜單設計就會面臨著以下幾道 Forces (你可以將 Force 想成是早餐店在設計菜單時的困擾):
- 主廚認為每一項單品都是某種味道落在了某種口感之上,像是起司香Q蛋餅或是德國香腸捲餅,而為了讓客人能夠保持新鮮感,應該要允許每一種搭配組合。
- 每種搭配組合都想歡迎大家嚐嚐,可是實在不知道該如何表達在菜單中,有太多組合了。
仔細觀察菜單的蔬菜蛋餅的定價,會發現還有第三道 Force:
3. 在這麼多搭配組合之下,錢有點難算啊,得制定好定價的規則。香Q蛋餅如果搭配的是「蔬菜」,那價格就會是 40,搭配其他主食的話則不額外收費。在不同的搭配之下,定價的方式可能會有所不同。
以軟體設計的角度來描述這三道 Forces 的話,可以寫出以下:
- 結構變動性 (Structural Variation):單品類別中存在著兩個不同抽象層次的獨立變化——是某種味道落在了某種口感上。
- 定價的行為變動性 (Pricing Behavioral Variation):根據不同的單品組合,會有不同的定價行為。
- 程式維護性的要求 (Code Maintainability’s Requirement) :組合爆炸 (Combinatorial Explosion)!組合的數量太多,如果用繼承來表示每一種組合的話,則會有太多的子類別要維護。
根據 Force 2,我們確信了「價格」不會只是屬性,因為定價會根據組合的不同而有所變動,而在這麼多種組合之下,未來肯定也會有不同的定價策略。因此,我們該將定價視為是一個行為,而每個單品都有「定價」這個動作 (Operation) 。
此時早餐店菜單的類別圖就會變成以下如此這般複雜:
現在只考慮了「香Q蛋餅」和「玉米、蔬菜、爆漿乳酪起士、火腿和薯餅」兩部分的搭配而已,就已經產出了如此多項的單品,已經組合爆炸了,十分難以維護啊。
因此,為了不要在菜單上陳列出組合爆炸的單品清單,這間早餐店就拿出了 Gang Of Four 23 個設計模式(謎之聲:不要瞎掰好嗎)反覆對照 Context 和 Problem,左思右想。
最後看到了橋接模式 Bridge Pattern 及其欲解決的 Problem:「你的類別中浮現出了兩種不同抽象層次,『高階』和『低階』兩個部分嗎!?而你想要解耦它們以致於彼此能獨立改變並且允許各種組合嗎!?」
Eureka!就是他啊!好!就套橋接模式吧!
橋接模式 Bridge Pattern
橋接模式的 Form 如下圖:
主廚看著這個解決方案,嘗試套用到自己的 Context 中,做出了以下的思路:「Abstraction 為高階的部分而 Implementor 為低階的部分是嗎?嗯⋯⋯那正對到了我所謂的『主要味道』和『口感』兩部份呢。」
「我就將這兩部分命名為『主食』和『麵包』吧!」
於是便有了以下的 Redesign:
在這個小品文章中,雖然菜單的設計元素不一定能完整地類比到軟體設計的元素之上,但是從菜單的設計中,我們可以嘗試反推他想要解決的問題 (Problem)、他套用的解決方案 (Solution),以及套用的後果 (Resulting Context)。
在套用完 Bridge Pattern 之後的 Resulting Context 就是:
- 主食和麵包可以獨立變化,並且允許 Client 自由搭配兩者。
- 在自由搭配之下,定價的規則帶來的額外的複雜度。
嗯,是啊,這也難怪當我點了爆漿乳酪起士香Q蛋餅時,會對於他的 $90 價格感到十分驚訝了 XD。
(我知道爆漿乳酪起士加其他麵包的話,也許值這麼高的價格,但配蛋餅的話實在不該收 $90 啊!!)
在這個 Resulting Context 下,又面臨了新的 Force 了⋯
水球軟體學院
想聽更多的軟體設計話題或是參加每週舉辦的線上的軟體設計派對嗎?
來吧~!加入我們的臉書社團 😃:https://www.facebook.com/groups/wateballsa.tw