小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,閱讀代碼中有任何問題,歡迎通過各種方式“騷擾”樓主。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章