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