徒弟小M接到一個私活,給朋友的川菜館做個訂餐APP,在開發點菜菜單時,遇到了困難。
一開始他是這麼做的,將菜單項放入一個數組作爲TableView的數據源:
["宮保雞丁", "乾燒魚", "回鍋肉", "麻婆豆腐", "家常豆腐", "黃燜鴨", "夫妻肺片", "鹽水鴨", "鍋巴肉片"]
可給朋友一看,朋友說不行,原來朋友不光做中晚餐,還兼做早餐,提供的是一些四川小吃,希望與主菜分開顯示,方便用戶選擇,於是菜單變成了這樣:
“相當於兩個菜單組合”小M很自然想到,用二維數組將兩個菜單組織到一起:
[["宮保雞丁", "乾燒魚", "回鍋肉", "麻婆豆腐", "家常豆腐", "黃燜鴨", "夫妻肺片", "鹽水鴨", "鍋巴肉片"], // 主菜
["擔擔麪", "川北涼粉", "麻辣小面", "酸辣面", "酸辣粉"]] // 早餐
爲了使兩個菜單組能分別展開/收起,小M開闢了兩個數組,用來表示菜單組“展開/收起”和組名:
var groupExpandFlag:Array<Bool> = [true, true]
var groupName:Array<String> = ["主菜", "早餐"]
顯示 cell 的代碼有點兒彆扭,不過還在小M控制範圍內,只是需要小心處理數組的下標:
朋友對新菜單表示滿意,正在小M暗自慶幸時,朋友一拍腦袋,說到:“哎呀,忘了加酒水單了,這可是賺錢的大頭啊,你可得幫我加上!”
小M看了一眼cellForRowAt
中已如亂麻的if-else
,一時不知該從何下手了。
用組合模式進行簡化
爲什麼用二維數組加個菜單組這麼麻煩呢?我們注意到 cellForRowAt
中的代碼主要是爲了區分第一組/第二組,判斷依據是(居然是)indexPath.row
,由於菜單組會展開/收起,indexPath.row
對應的菜單項也在變化,每增加一組,偏移的計算就要更新一次;
而 tableView 實際上不關心要顯示的是菜單組還是菜單項,只要能正確獲得菜單項目和每項的數據就可以了,於是矛盾就在於:
對每個菜單項來說,必須區分是菜單組還是菜單項,才能正確處理數據;而對調用者來說,它們是一個整體,都是同一個菜單,像菜單這樣明顯有“整體/部分”關係的數據集合,就需要組合模式來幫忙了。
爲對組合模式的作用有直觀的瞭解,我們先來看實現後達到的效果。
組合的訪問者
作爲菜單的調用者,tableView的代碼如下:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.menu.count() - 1 // 根菜單不需要顯示
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellID)
let menuItem = self.menu.itemAt(index: indexPath.row + 1)
var indent = " "
if ((menuItem?.isGroup)!) {
indent = ""
}
cell?.textLabel?.text = "\(indent)\(menuItem?.name ?? "")"
return cell!
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
var menuItem = self.menu.itemAt(index: indexPath.row + 1)
menuItem!.isExpand = !(menuItem!.isExpand)
tableView.reloadData()
}
除了顯示所需的代碼外,沒有任何多餘的代碼,從 tableView 看來,根菜單、組菜單、菜單項之間,沒有任何區別,比如在處理展開菜單時,didSelectRowAt
對所有 MenuItem 都處理了 isExpand ,並沒有具體區分組菜單還是菜單項,isExpand 對兩者 count 的不同影響,由 MenuItem 自行處理,菜單項實際上沒有對 isExpand 做任何處理(但依然實現了 isExpand,從而避免調用者做判斷)。
組合的構造者
因爲組合模式是一種結構模式,該模式主要處理的是對象的結構和它們的組合方式,而生成組合對象是一種行爲,需要額外的訪問者,下面代碼片段展示了主菜的構造過程:
let mainCoursesMenu = MenuItem()
mainCoursesMenu.name = "主菜"
for name in ["宮保雞丁", "乾燒魚", "回鍋肉", "麻婆豆腐", "家常豆腐", "黃燜鴨", "夫妻肺片", "鹽水鴨", "鍋巴肉片"] {
let menuItem = MenuItem()
menuItem.name = name
mainCoursesMenu.add(item:menuItem)
}
self.menu.add(item:mainCoursesMenu)
組合對象的實現
用 MenuComponent
協議表示組菜單、菜單項,統一它們的操作
protocol MenuComponent {
var name:String { get }
var child:Array<MenuComponent> { get }
var isExpand:Bool { get set }
var isGroup:Bool { get }
func add(item:MenuComponent)
func itemAt(index:Int) -> MenuComponent?
func count() -> Int
}
用 MenuItem
實現,這裏以 count 方法爲代表:
func count() -> Int {
var count = 1 //自己爲第一項
if (self.isExpand) {
for item in self.child {
count += item.count()
}
}
return count
}
這裏可以看出,主要是利用了遞歸對組合對象進行了遍歷。
完整代碼請參閱SichuanFood,閱讀代碼中有任何問題,歡迎通過各種方式“騷擾”樓主。