Visitor Pattern in TableView
使用 Visitor Pattern 增加 TableView 的阅读和扩充性

Photo by Alex wong
前言
接上篇「 Visitor Pattern in Swift 」介绍 Visitor 模式及一个简单的实务应用场景,此篇将介绍另一个在 iOS 需求开发上的实际应用。
需求场景
要开发一个动态墙功能,有多种不同类型的区块需要动态组合显示。
以 StreetVoice 的动态墙为例:

如上图所示,动态墙是由多种不同类型的区块动态组合而成:
-
Type A: 活动动态
-
Type B: 追踪推荐
-
Type C: 新歌动态
-
Type D: 新专辑动态
-
Type E: 新追纵动态
-
Type …. 更多
类型可预期会在未来随著功能迭代越来越多。
问题
在没有任何架构设计的情况下 Code 可能会长这样:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let row = datas[indexPath.row]
switch row.type {
case .invitation:
let cell = tableView.dequeueReusableCell(withIdentifier: "invitation", for: indexPath) as! InvitationCell
// config cell with viewObject/viewModel...
return cell
case .newSong:
let cell = tableView.dequeueReusableCell(withIdentifier: "newSong", for: indexPath) as! NewSongCell
// config cell with viewObject/viewModel...
return cell
case .newEvent:
let cell = tableView.dequeueReusableCell(withIdentifier: "newEvent", for: indexPath) as! NewEventCell
// config cell with viewObject/viewModel...
return cell
case .newText:
let cell = tableView.dequeueReusableCell(withIdentifier: "newText", for: indexPath) as! NewTextCell
// config cell with viewObject/viewModel...
return cell
case .newPhotos:
let cell = tableView.dequeueReusableCell(withIdentifier: "newPhotos", for: indexPath) as! NewPhotosCell
// config cell with viewObject/viewModel...
return cell
}
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let row = datas[indexPath.row]
switch row.type {
case .invitation:
if row.isEmpty {
return 100
} else {
return 300
}
case .newSong:
return 100
case .newEvent:
return 200
case .newText:
return UITableView.automaticDimension
case .newPhotos:
return UITableView.automaticDimension
}
}
-
难以测试:什么 Type 有什么对应的逻辑输出难以测试
-
难以扩充维护:需要新增新 Type 时,都要更动此 ViewController;cellForRow、heightForRow、willDisplay…四散在各个 Function 内,难保忘记改,或改错
-
难以阅读:全部逻辑都在 View 身上
Visitor Pattern 解决方案
Why?
整理了一下物件关系,如下图所示:

我们有许多种类型的 DataSource (ViewObject) 需要与多种类型的操作器做交互,是一个很典型的 Visitor Double Dispatch 。
How?
为简化 Demo Code 以下改用 PlainTextFeedViewObject 纯文字动态、 MemoriesFeedViewObject 每日回忆、 MediaFeedViewObject 图片动态,呈现设计。
套用 Visitor Pattern 的架构图如下:

首先定义出 Visitor 介面,此介面用途是抽象宣告出操作器能接受的 DataSource 类型:
protocol FeedVisitor {
associatedtype T
func visit(_ viewObject: PlainTextFeedViewObject) -> T?
func visit(_ viewObject: MediaFeedViewObject) -> T?
func visit(_ viewObject: MemoriesFeedViewObject) -> T?
//...
}
各操作器实现 FeedVisitor 介面:
struct FeedCellVisitor: FeedVisitor {
typealias T = UITableViewCell.Type
func visit(_ viewObject: MediaFeedViewObject) -> T? {
return MediaFeedTableViewCell.self
}
func visit(_ viewObject: MemoriesFeedViewObject) -> T? {
return MemoriesFeedTableViewCell.self
}
func visit(_ viewObject: PlainTextFeedViewObject) -> T? {
return PlainTextFeedTableViewCell.self
}
}
实现 ViewObject <-> UITableViewCell 对应。
struct FeedCellHeightVisitor: FeedVisitor {
typealias T = CGFloat
func visit(_ viewObject: MediaFeedViewObject) -> T? {
return 30
}
func visit(_ viewObject: MemoriesFeedViewObject) -> T? {
return 10
}
func visit(_ viewObject: PlainTextFeedViewObject) -> T? {
return 10
}
}
实现 ViewObject <-> UITableViewCell Height 对应。
struct FeedCellConfiguratorVisitor: FeedVisitor {
private let cell: UITableViewCell
init(cell: UITableViewCell) {
self.cell = cell
}
func visit(_ viewObject: MediaFeedViewObject) -> Any? {
guard let cell = cell as? MediaFeedTableViewCell else { return nil }
// cell.config(viewObject)
return nil
}
func visit(_ viewObject: MemoriesFeedViewObject) -> Any? {
guard let cell = cell as? MediaFeedTableViewCell else { return nil }
// cell.config(viewObject)
return nil
}
func visit(_ viewObject: PlainTextFeedViewObject) -> Any? {
guard let cell = cell as? MediaFeedTableViewCell else { return nil }
// cell.config(viewObject)
return nil
}
}
实现 ViewObject <-> Cell 如何 Config 对应。
当需要支援新的 DataSource (ViewObject) 时,只需在 FeedVisitor 介面上多加一个开口,并在各操作器中实现对应的逻辑。
DataSource (ViewObject) 与操作器的绑定:
protocol FeedViewObject {
@discardableResult func accept<V: FeedVisitor>(visitor: V) -> V.T?
}
ViewObject 实现绑定的介面:
struct PlainTextFeedViewObject: FeedViewObject {
func accept<V>(visitor: V) -> V.T? where V : FeedVisitor {
return visitor.visit(self)
}
}
struct MemoriesFeedViewObject: FeedViewObject {
func accept<V>(visitor: V) -> V.T? where V : FeedVisitor {
return visitor.visit(self)
}
}
UITableView 中的实现:
final class ViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
private let cellVisitor = FeedCellVisitor()
private var viewObjects: [FeedViewObject] = [] {
didSet {
viewObjects.forEach { viewObject in
let cellName = viewObject.accept(visitor: cellVisitor)
tableView.register(cellName, forCellReuseIdentifier: String(describing: cellName))
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
viewObjects = [
MemoriesFeedViewObject(),
MediaFeedViewObject(),
PlainTextFeedViewObject(),
MediaFeedViewObject(),
PlainTextFeedViewObject(),
MediaFeedViewObject(),
PlainTextFeedViewObject()
]
// Do any additional setup after loading the view.
}
}
extension ViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewObjects.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let viewObject = viewObjects[indexPath.row]
let cellName = viewObject.accept(visitor: cellVisitor)
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: cellName), for: indexPath)
let cellConfiguratorVisitor = FeedCellConfiguratorVisitor(cell: cell)
viewObject.accept(visitor: cellConfiguratorVisitor)
return cell
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let viewObject = viewObjects[indexPath.row]
let cellHeightVisitor = FeedCellHeightVisitor()
let cellHeight = viewObject.accept(visitor: cellHeightVisitor) ?? UITableView.automaticDimension
return cellHeight
}
}
结果
-
测试:符合单一职责原则,可针对每个操作器的每个资料单点进行测试
-
扩充维护:当需要支援新的 DataSource (ViewObject) 时只需在 Visitor 协议扩充一个开口,并在个别操作器 Visitor 上进行实现、需要抽离新操作器时,也只要 New 新的 Class 实现即可。
-
阅读:只需浏览各操作器物件即可知道整个页面各个 View 的组成逻辑
完整专案
Murmur…
2022/07 思维低谷期中撰写的文章,内容如有描述不周、错误敬请海纳!



留言 · Comments