徒弟小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,阅读代码中有任何问题,欢迎通过各种方式“骚扰”楼主。
网友评论