Home Visitor Pattern in iOS (Swift)
Post
Cancel

Visitor Pattern in iOS (Swift)

Visitor Pattern in Swift

Analysis of the practical application scenarios of the Design Pattern Visitor

Photo by [Daniel McCullough](https://unsplash.com/@d_mccullough?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText){:target="_blank"}

Photo by Daniel McCullough

Preface

It has been more than 10 years since I first learned about “Design Patterns,” and I still can’t confidently say I have fully mastered them. I’ve always had a vague understanding, and I’ve gone through all the patterns several times, but without internalizing them or applying them in practice, I quickly forget.

I am really useless.

Internal Skills and Techniques

I once saw a great analogy: techniques such as PHP, Laravel, iOS, Swift, SwiftUI, etc., are relatively easy to switch between and learn. However, internal skills like algorithms, data structures, design patterns, etc., are much harder to master. Internal skills and techniques complement each other; techniques are easy to learn, but internal skills are hard to practice. Someone proficient in techniques may not necessarily have strong internal skills, but someone with strong internal skills can quickly learn techniques. Therefore, internal skills are the foundation, and combining them with techniques makes one invincible.

Finding a Suitable Learning Method

Based on my previous learning experiences, I believe the best way for me to learn Design Patterns is to master a few patterns first, internalize and flexibly use them, and develop a sense of judgment for suitable and unsuitable scenarios. Then, gradually accumulate new patterns until I master them all. The best way is to find practical scenarios and learn from application.

Learning Resources

I recommend two free learning resources:

Visitor — Behavioral Patterns

The first chapter records the Visitor Pattern, which is one of the gold mines I discovered during my year at StreetVoice. The StreetVoice App has many places where the Visitor Pattern is used to solve architectural problems. During this experience, I grasped the essence of the Visitor Pattern. So, let’s start with it in the first chapter!

What is Visitor

First, understand what Visitor is, what problem it aims to solve, and what its structure is.

Image from [refactoringguru](https://refactoringguru.cn/design-patterns/visitor){:target="_blank"}

Image from refactoringguru

For detailed information, please refer directly to refactoringguru’s explanation of Visitor.

iOS Practical Scenario (1)

Suppose today we have the following Models: UserModel, SongModel, PlaylistModel. Now we need to implement a sharing feature that can share to: Facebook, Line, Instagram. Each Model needs to present different sharing information, and each platform requires different data:

The combination scenario is as shown in the figure above. The first table shows the customized content of each Model, and the second table shows the data required by each sharing platform.

Especially Instagram requires multiple images when sharing a Playlist, which is different from the sources required by other shares.

Define Model

First, define the properties of each Model:

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
// Model
struct UserModel {
    let id: String
    let name: String
    let profileImageURLString: String
}

struct SongModel {
    let id: String
    let name: String
    let user: UserModel
    let coverImageURLString: String
}

struct PlaylistModel {
    let id: String
    let name: String
    let user: UserModel
    let songs: [SongModel]
    let coverImageURLString: String
}

// Data

let user = UserModel(id: "1", name: "Avicii", profileImageURLString: "https://zhgchg.li/profile/1.png")

let song = SongModel(id: "1",
                     name: "Wake me up",
                     user: user,
                     coverImageURLString: "https://zhgchg.li/cover/1.png")

let playlist = PlaylistModel(id: "1",
                            name: "Avicii Tribute Concert",
                            user: user,
                            songs: [
                                song,
                                SongModel(id: "2", name: "Waiting for love", user: UserModel(id: "1", name: "Avicii", profileImageURLString: "https://zhgchg.li/profile/1.png"), coverImageURLString: "https://zhgchg.li/cover/3.png"),
                                SongModel(id: "3", name: "Lonely Together", user: UserModel(id: "1", name: "Avicii", profileImageURLString: "https://zhgchg.li/profile/1.png"), coverImageURLString: "https://zhgchg.li/cover/1.png"),
                                SongModel(id: "4", name: "Heaven", user: UserModel(id: "1", name: "Avicii", profileImageURLString: "https://zhgchg.li/profile/1.png"), coverImageURLString: "https://zhgchg.li/cover/4.png"),
                                SongModel(id: "5", name: "S.O.S", user: UserModel(id: "1", name: "Avicii", profileImageURLString: "https://zhgchg.li/profile/1.png"), coverImageURLString: "https://zhgchg.li/cover/5.png")],
                            coverImageURLString: "https://zhgchg.li/playlist/1.png")

The approach without any consideration

Consider the structure completely, and start with the dirtiest approach without thinking about anything.

Stephen Chow — The God of Cookery

Stephen Chow — The God of Cookery

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
class ShareManager {
    private let title: String
    private let urlString: String
    private let imageURLStrings: [String]

    init(user: UserModel) {
        self.title = "Hi, sharing a great artist with you \(user.name)."
        self.urlString = "https://zhgchg.li/user/\(user.id)"
        self.imageURLStrings = [user.profileImageURLString]
    }

    init(song: SongModel) {
        self.title = "Hi, sharing a great song I just heard, \(song.user.name)'s \(song.name)."
        self.urlString = "https://zhgchg.li/user/\(song.user.id)/song/\(song.id)"
        self.imageURLStrings = [song.coverImageURLString]
    }

    init(playlist: PlaylistModel) {
        self.title = "Hi, I can't stop listening to this playlist \(playlist.name)."
        self.urlString = "https://zhgchg.li/user/\(playlist.user.id)/playlist/\(playlist.id)"
        self.imageURLStrings = playlist.songs.map({ $0.coverImageURLString })
    }

    func shareToFacebook() {
        // call Facebook share sdk...
        print("Share to Facebook...")
        print("[![\(self.title)](\(String(describing: self.imageURLStrings.first))](\(self.urlString))")
    }

    func shareToInstagram() {
        // call Instagram share sdk...
        print("Share to Instagram...")
        print(self.imageURLStrings.joined(separator: ","))
    }

    func shareToLine() {
        // call Line share sdk...
        print("Share to Line...")
        print("[\(self.title)](\(self.urlString))")
    }
}

Nothing much to say, it’s a 0-structure mess all mixed together. If you want to add a new sharing platform, change the sharing information of a platform, or add a shareable model, you have to modify ShareManager. Additionally, the design of imageURLStrings is considered because Instagram needs a set of image data when sharing a playlist, so it is declared as an array. This is a bit of a backward design, where the structure is designed according to the requirements, and other types that do not need a set of images are also polluted.

Optimize a bit

Separate the logic a bit.

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
protocol Shareable {
    func getShareText() -> String
    func getShareURLString() -> String
    func getShareImageURLStrings() -> [String]
}

extension UserModel: Shareable {
    func getShareText() -> String {
        return "Hi, sharing a great artist with you \(self.name)."
    }

    func getShareURLString() -> String {
        return "https://zhgchg.li/user/\(self.id)"
    }

    func getShareImageURLStrings() -> [String] {
        return [self.profileImageURLString]
    }
}

extension SongModel: Shareable {
    func getShareText() -> String {
        return "Hi, sharing a great song I just heard, \(self.user.name)'s \(self.name)."
    }

    func getShareURLString() -> String {
        return "https://zhgchg.li/user/\(self.user.id)/song/\(self.id)"
    }

    func getShareImageURLStrings() -> [String] {
        return [self.coverImageURLString]
    }
}

extension PlaylistModel: Shareable {
    func getShareText() -> String {
        return "Hi, I can't stop listening to this playlist \(self.name)."
    }

    func getShareURLString() -> String {
        return "https://zhgchg.li/user/\(self.user.id)/playlist/\(self.id)"
    }

    func getShareImageURLStrings() -> [String] {
        return [self.coverImageURLString]
    }
}

protocol ShareManagerProtocol {
    var model: Shareable { get }
    init(model: Shareable)
    func share()
}

class FacebookShare: ShareManagerProtocol {
    let model: Shareable

    required init(model: Shareable) {
        self.model = model
    }

    func share() {
        // call Facebook share sdk...
        print("Share to Facebook...")
        print("[![\(model.getShareText())](\(String(describing: model.getShareImageURLStrings().first))](\(model.getShareURLString())")
    }
}

class InstagramShare: ShareManagerProtocol {
    let model: Shareable

    required init(model: Shareable) {
        self.model = model
    }

    func share() {
        // call Instagram share sdk...
        print("Share to Instagram...")
        print(model.getShareImageURLStrings().joined(separator: ","))
    }
}

class LineShare: ShareManagerProtocol {
    let model: Shareable

    required init(model: Shareable) {
        self.model = model
    }

    func share() {
        // call Line share sdk...
        print("Share to Line...")
        print("[\(model.getShareText())](\(model.getShareURLString())")
    }
}

We extracted a CanShare Protocol, and any Model that follows this protocol can support sharing; the sharing part is also extracted into ShareManagerProtocol. If there is a new sharing requirement, just implement the protocol content, and modifying or deleting it will not affect other ShareManagers.

But getShareImageURLStrings is still strange. Additionally, suppose the newly added sharing platform requires vastly different Model data, such as WeChat sharing needing play count, creation date, etc., only it needs this information. At this point, it will start to become confusing.

Visitor

Using the Visitor Pattern solution.

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// Visitor Version
protocol Shareable {
    func accept(visitor: SharePolicy)
}

extension UserModel: Shareable {
    func accept(visitor: SharePolicy) {
        visitor.visit(model: self)
    }
}

extension SongModel: Shareable {
    func accept(visitor: SharePolicy) {
        visitor.visit(model: self)
    }
}

extension PlaylistModel: Shareable {
    func accept(visitor: SharePolicy) {
        visitor.visit(model: self)
    }
}

protocol SharePolicy {
    func visit(model: UserModel)
    func visit(model: SongModel)
    func visit(model: PlaylistModel)
}

class ShareToFacebookVisitor: SharePolicy {
    func visit(model: UserModel) {
        // call Facebook share sdk...
        print("Share to Facebook...")
        print("[![Hi, sharing a great artist with you \(model.name).](\(model.profileImageURLString)](https://zhgchg.li/user/\(model.id)")
    }
    
    func visit(model: SongModel) {
        // call Facebook share sdk...
        print("Share to Facebook...")
        print("[![Hi, sharing a great song I just heard, \(model.user.name)'s \(model.name), it was played.](\(model.coverImageURLString))](https://zhgchg.li/user/\(model.user.id)/song/\(model.id)")
    }
    
    func visit(model: PlaylistModel) {
        // call Facebook share sdk...
        print("Share to Facebook...")
        print("[![Hi, I can't stop listening to this playlist \(model.name).](\(model.coverImageURLString))](https://zhgchg.li/user/\(model.user.id)/playlist/\(model.id)")
    }
}

class ShareToLineVisitor: SharePolicy {
    func visit(model: UserModel) {
        // call Line share sdk...
        print("Share to Line...")
        print("[Hi, sharing a great artist with you \(model.name).](https://zhgchg.li/user/\(model.id)")
    }
    
    func visit(model: SongModel) {
        // call Line share sdk...
        print("Share to Line...")
        print("[Hi, sharing a great song I just heard, \(model.user.name)'s \(model.name), it was played.](https://zhgchg.li/user/\(model.user.id)/song/\(model.id)")
    }
    
    func visit(model: PlaylistModel) {
        // call Line share sdk...
        print("Share to Line...")
        print("[Hi, I can't stop listening to this playlist \(model.name).](https://zhgchg.li/user/\(model.user.id)/playlist/\(model.id)")
    }
}

class ShareToInstagramVisitor: SharePolicy {
    func visit(model: UserModel) {
        // call Instagram share sdk...
        print("Share to Instagram...")
        print(model.profileImageURLString)
    }
    
    func visit(model: SongModel) {
        // call Instagram share sdk...
        print("Share to Instagram...")
        print(model.coverImageURLString)
    }
    
    func visit(model: PlaylistModel) {
        // call Instagram share sdk...
        print("Share to Instagram...")
        print(model.songs.map({ $0.coverImageURLString }).joined(separator: ","))
    }
}

// Use case
let shareToInstagramVisitor = ShareToInstagramVisitor()
user.accept(visitor: shareToInstagramVisitor)
playlist.accept(visitor: shareToInstagramVisitor)

We will look at what was done line by line:

  • First, we created a Shareable Protocol, which is just for convenience in managing Models that support sharing. Visitors have a unified interface (it is okay not to define it).
  • UserModel/SongModel/PlaylistModel implement Shareable func accept(visitor: SharePolicy), and if new Models that support sharing are added later, they only need to implement the protocol.
  • Define SharePolicy to list the supported Models (must be concrete type) You might wonder why not define it as visit(model: Shareable). If so, it would repeat the problem of the previous version.
  • Each Share method implements SharePolicy, combining the required resources according to the source.
  • Suppose there is an additional WeChat share today, which requires special data (play count, creation date). It will not affect the existing code because it can get the information it needs from the concrete model.

Achieve the goal of low coupling and high cohesion in program development.

The above is a classic implementation of Visitor Double Dispatch, but we rarely encounter such situations in daily development. Generally, there may only be one Visitor, but I think this pattern is also very suitable for combination. For example, if there is a requirement for SaveToCoreData today, we can directly define accept(visitor: SaveToCoreDataVisitor) without declaring the Policy Protocol, which is also a very good usage architecture.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protocol Saveable {
  func accept(visitor: SaveToCoreDataVisitor)
}

class SaveToCoreDataVisitor {
    func visit(model: UserModel) {
        // map UserModel to coredata
    }
    
    func visit(model: SongModel) {
        // map SongModel to coredata
    }
    
    func visit(model: PlaylistModel) {
        // map PlaylistModel to coredata
    }
}

Other applications: Save, Like, tableview/collectionview cellforrow…

Principles

Finally, let’s talk about some common principles:

  • Code is for people to read, do not over-design.
  • Consistency is important; the same context and codebase should use the same architectural method.
  • If the scope is controllable or other situations are unlikely to occur, further splitting can be considered over-design.
  • Apply more, invent less; Design Patterns have been in the software design field for decades, and the scenarios they consider are certainly more comprehensive than creating a new architecture.
  • If you don’t understand Design Patterns, you can learn them, but if it’s a self-created architecture, it’s harder to convince others to learn because it might only be applicable to this case. It is not a common sense.
  • Code repetition does not mean bad. If you blindly pursue encapsulation, it may lead to over-design. As mentioned earlier, code is for people to read, so as long as it is readable and has low coupling and high cohesion, it is good code.
  • Do not modify patterns arbitrarily. They are designed for a reason, and random modifications may cause problems in certain scenarios.
  • Once you start taking detours, you will go further and further, and the code will become messier.

inspired by @saiday

Further Reading

Next Chapter

If you have any questions or comments, feel free to contact me.

===

本文中文版本

===

This article was first published in Traditional Chinese on Medium ➡️ View Here


This post is licensed under CC BY 4.0 by the author.

Building a Fully Automated WFH Employee Health Reporting System with Slack

Leading Snowflakes Reading Notes