Visitor Pattern|提升 TableView 可扩充性与可维护性设计范例
针对多类型动态墙 TableView,运用 Visitor Pattern 解决难测试、难扩充与难阅读问题,实现单一职责分离,轻松新增资料来源与操作器,提升程式码品质与维护效率。
Click here to view the English version of this article.
點擊這裡查看本文章正體中文版本。
基于 SEO 考量,本文标题与描述经 AI 调整,原始版本请参考内文。
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 可能会长这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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 类型:
1
2
3
4
5
6
7
protocol FeedVisitor {
associatedtype T
func visit(_ viewObject: PlainTextFeedViewObject) -> T?
func visit(_ viewObject: MediaFeedViewObject) -> T?
func visit(_ viewObject: MemoriesFeedViewObject) -> T?
//...
}
各操作器实现 FeedVisitor
介面:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 对应。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 对应。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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) 与操作器的绑定:
1
2
3
protocol FeedViewObject {
@discardableResult func accept<V: FeedVisitor>(visitor: V) -> V.T?
}
ViewObject 实现绑定的介面:
1
2
3
4
5
6
7
8
9
10
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 中的实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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 思维低谷期中撰写的文章,内容如有描述不周、错误敬请海纳!
延伸阅读
Design Patterns 的实战应用纪录 — In WKWebView with Builder, Strategy & Chain of Responsibility Pattern
有任何问题及指教欢迎 与我联络 。
本文首次发表于 Medium (点击查看原始版本),由 ZMediumToMarkdown 提供自动转换与同步技术。