Home Visitor Pattern in iOS (Swift)
Post
Cancel

Visitor Pattern in iOS (Swift)

Visitor Pattern in Swift (Share Object to XXX Example)

Analysis of the practical application scenarios of the Visitor Pattern (sharing items like products, songs, articles… to Facebook, Line, Linkedin, etc.)

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

Photo by Daniel McCullough

Introduction

From knowing about the existence of “Design Patterns” to now, it has been over 10 years, and I still can’t confidently say that I have mastered them completely. I have always been somewhat confused, and I have gone through all the patterns several times from start to finish, but if I don’t internalize them and apply them in practice, I quickly forget.

I am truly useless.

Internal Strength and Techniques

I once saw a very good analogy: the techniques part, such as PHP, Laravel, iOS, Swift, SwiftUI, etc., are relatively easy to switch between for learning, but the internal strength part, such as algorithms, data structures, design patterns, etc., are considered internal strength. There is a complementary effect between internal strength and techniques. Techniques are easy to learn, but internal strength is difficult to cultivate. Someone with excellent techniques may not have excellent internal strength, while someone with excellent internal strength can quickly learn techniques. Therefore, rather than saying they complement each other, it is better to say that internal strength is the foundation, and techniques complement it to achieve great success.

Find Your Suitable Learning Method

Based on my previous learning experiences, I believe that the learning method of Design Patterns that suits me best is to focus on mastering a few patterns first, internalize and flexibly apply them, develop a sense of judgment to determine which scenarios are suitable and which are not, and then gradually accumulate new patterns until mastering all of them. I think the best way is to find practical scenarios to learn from applications.

Learning Resources

I recommend two free learning resources:

Visitor — Behavioral Patterns

The first chapter documents the Visitor Pattern, which is one of the gold mines I dug up during my year at StreetVoice, where Visitor was widely used to solve architectural problems in the StreetVoice App. I also grasped the essence of Visitor during this experience, so let’s start with it in the first chapter!

What is Visitor

First, please understand what Visitor is? What problems does it solve? What is its structure?

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

The image is from refactoringguru.

The detailed content is not repeated here. Please refer directly to refactoringguru’s explanation of Visitor first.

Practical iOS Scenario - Sharing Feature

Assuming today we have the following models: UserModel, SongModel, PlaylistModel. Now we need to implement a sharing feature that can share to: Facebook, Line, Instagram, these three platforms. The sharing message to be displayed for each model is different, and each platform requires different data:

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

Especially when sharing a Playlist on Instagram, multiple images are required, which is different from the source required for other sharing platforms.

Define Models

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")

Doing Nothing Approach

Do not translate the content as it is already in English.

We have extracted a CanShare Protocol, any Model that follows this protocol can support sharing; the sharing part is also abstracted into ShareManagerProtocol. Implementing the protocol content for new sharing will not affect other ShareManagers.

However, getShareImageURLStrings is still strange. Additionally, assuming that the data for the Model requirements of a newly added sharing platform are vastly different, such as WeChat sharing requiring playback counts, creation dates, etc., and only it needs them, things will start to get messy.

Visitor

Solution using the Visitor Pattern.

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 \(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 just heard, \(model.user.name)'s \(model.name), played by him.](\(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 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 \(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 just heard, \(model.user.name)'s \(model.name), played by him.](https://zhgchg.li/user/\(model.user.id)/song/\(model.id)")
    }
    
    func visit(model: PlaylistModel) {
        // call Line share sdk...
        print("Share to Line...")
        print("[Hi 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)

Let’s see what we did line by line:

  • First, we created a Shareable Protocol, which is just for us to manage models that support sharing with a unified interface for visitors (undefined is also acceptable).
  • UserModel/SongModel/PlaylistModel implement Shareable func accept(visitor: SharePolicy), so if we add a new model that supports sharing, it only needs 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 we do that, we will repeat the issues from the previous version.
  • Implement SharePolicy for each Share method, combining the required resources based on the source.
  • Suppose today we have a new WeChat sharing feature that requires special data (play count, creation date). It won’t affect the existing code because it can retrieve the information it needs from concrete models.

Achieving the goal of low coupling and high cohesion in software development.

The above is the classic Visitor Double Dispatch implementation. However, we rarely encounter this situation in our daily development. In general, we may only have one visitor, but I think it is also suitable to use this pattern for composition. For example, if we have a SaveToCoreData requirement today, we can directly define accept(visitor: SaveToCoreDataVisitor) without declaring a Policy Protocol, which is also a good architectural approach.

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 humans to read, so avoid over-designing.
  • Consistency is crucial. The same context in the same codebase should use the same architectural approach.
  • If the scope is controllable or no other situations are likely to occur, continuing to break it down further can be considered over-designing.
  • Use existing solutions more and invent less. Design patterns have been around in software design for decades, and they consider scenarios more comprehensively than creating a new architecture.
  • If you can’t understand a design pattern, you can learn it. However, if it’s a self-created architecture, it’s harder to convince others to learn because it may only be applicable to that specific case and not a common practice.
  • Code duplication doesn’t always mean it’s bad. Pursuing encapsulation blindly can lead to over-designing. Again, referring back to the previous points, code readability, low coupling, and high cohesion are indicators of good code.
  • Don’t tamper with patterns. There is a reason behind their design, and random modifications may cause issues in certain scenarios.
  • Once you start taking detours, you’ll only go further astray, and the code will get messier.

inspired by @saiday

References

Further Reading

Feel free to contact me for any questions or feedback.

===

本文中文版本

===

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