小M学设计模式:组合模式在TableView中的妙用

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

推荐阅读更多精彩内容