
Visitor in Swift
Visitor is a behavioral design pattern that allows adding new behaviors to existing class hierarchy without altering any existing code.
Read why Visitors can’t be simply replaced with method overloading in our article Visitor and Double Dispatch.
Complexity:
Popularity:
Usage examples: Visitor isn’t a very common pattern because of its complexity and narrow applicability.
The following examples are available on
Swift Playgrounds.
Kudos to
Alejandro Mohamad for creating the Playground version.
Conceptual Example
This example illustrates the structure of the Visitor design pattern and focuses on the following questions:
- What classes does it consist of?
- What roles do these classes play?
- In what way the elements of the pattern are related?
After learning about the pattern’s structure it’ll be easier for you to grasp the following example, based on a real-world Swift use case.
Example.swift: Conceptual example
import XCTest
/// The Component interface declares an `accept` method that should take the
/// base visitor interface as an argument.
protocol Component {
func accept(_ visitor: Visitor)
}
/// Each Concrete Component must implement the `accept` method in such a way
/// that it calls the visitor's method corresponding to the component's class.
class ConcreteComponentA: Component {
/// Note that we're calling `visitConcreteComponentA`, which matches the
/// current class name. This way we let the visitor know the class of the
/// component it works with.
func accept(_ visitor: Visitor) {
visitor.visitConcreteComponentA(element: self)
}
/// Concrete Components may have special methods that don't exist in their
/// base class or interface. The Visitor is still able to use these methods
/// since it's aware of the component's concrete class.
func exclusiveMethodOfConcreteComponentA() -> String {
return "A"
}
}
class ConcreteComponentB: Component {
/// Same here: visitConcreteComponentB => ConcreteComponentB
func accept(_ visitor: Visitor) {
visitor.visitConcreteComponentB(element: self)
}
func specialMethodOfConcreteComponentB() -> String {
return "B"
}
}
/// The Visitor Interface declares a set of visiting methods that correspond to
/// component classes. The signature of a visiting method allows the visitor to
/// identify the exact class of the component that it's dealing with.
protocol Visitor {
func visitConcreteComponentA(element: ConcreteComponentA)
func visitConcreteComponentB(element: ConcreteComponentB)
}
/// Concrete Visitors implement several versions of the same algorithm, which
/// can work with all concrete component classes.
///
/// You can experience the biggest benefit of the Visitor pattern when using it
/// with a complex object structure, such as a Composite tree. In this case, it
/// might be helpful to store some intermediate state of the algorithm while
/// executing visitor's methods over various objects of the structure.
class ConcreteVisitor1: Visitor {
func visitConcreteComponentA(element: ConcreteComponentA) {
print(element.exclusiveMethodOfConcreteComponentA() + " + ConcreteVisitor1\n")
}
func visitConcreteComponentB(element: ConcreteComponentB) {
print(element.specialMethodOfConcreteComponentB() + " + ConcreteVisitor1\n")
}
}
class ConcreteVisitor2: Visitor {
func visitConcreteComponentA(element: ConcreteComponentA) {
print(element.exclusiveMethodOfConcreteComponentA() + " + ConcreteVisitor2\n")
}
func visitConcreteComponentB(element: ConcreteComponentB) {
print(element.specialMethodOfConcreteComponentB() + " + ConcreteVisitor2\n")
}
}
/// The client code can run visitor operations over any set of elements without
/// figuring out their concrete classes. The accept operation directs a call to
/// the appropriate operation in the visitor object.
class Client {
// ...
static func clientCode(components: [Component], visitor: Visitor) {
// ...
components.forEach({ $0.accept(visitor) })
// ...
}
// ...
}
/// Let's see how it all works together.
class VisitorConceptual: XCTestCase {
func test() {
let components: [Component] = [ConcreteComponentA(), ConcreteComponentB()]
print("The client code works with all visitors via the base Visitor interface:\n")
let visitor1 = ConcreteVisitor1()
Client.clientCode(components: components, visitor: visitor1)
print("\nIt allows the same client code to work with different types of visitors:\n")
let visitor2 = ConcreteVisitor2()
Client.clientCode(components: components, visitor: visitor2)
}
}
Output.txt: Execution result
The client code works with all visitors via the base Visitor interface:
A + ConcreteVisitor1
B + ConcreteVisitor1
It allows the same client code to work with different types of visitors:
A + ConcreteVisitor2
B + ConcreteVisitor2
Real World Example
Example.swift: Real world example
import Foundation
import XCTest
protocol Notification: CustomStringConvertible {
func accept(visitor: NotificationPolicy) -> Bool
}
struct Email {
let emailOfSender: String
var description: String { return "Email" }
}
struct SMS {
let phoneNumberOfSender: String
var description: String { return "SMS" }
}
struct Push {
let usernameOfSender: String
var description: String { return "Push" }
}
extension Email: Notification {
func accept(visitor: NotificationPolicy) -> Bool {
return visitor.isTurnedOn(for: self)
}
}
extension SMS: Notification {
func accept(visitor: NotificationPolicy) -> Bool {
return visitor.isTurnedOn(for: self)
}
}
extension Push: Notification {
func accept(visitor: NotificationPolicy) -> Bool {
return visitor.isTurnedOn(for: self)
}
}
protocol NotificationPolicy: CustomStringConvertible {
func isTurnedOn(for email: Email) -> Bool
func isTurnedOn(for sms: SMS) -> Bool
func isTurnedOn(for push: Push) -> Bool
}
class NightPolicyVisitor: NotificationPolicy {
func isTurnedOn(for email: Email) -> Bool {
return false
}
func isTurnedOn(for sms: SMS) -> Bool {
return true
}
func isTurnedOn(for push: Push) -> Bool {
return false
}
var description: String { return "Night Policy Visitor" }
}
class DefaultPolicyVisitor: NotificationPolicy {
func isTurnedOn(for email: Email) -> Bool {
return true
}
func isTurnedOn(for sms: SMS) -> Bool {
return true
}
func isTurnedOn(for push: Push) -> Bool {
return true
}
var description: String { return "Default Policy Visitor" }
}
class BlackListVisitor: NotificationPolicy {
private var bannedEmails = [String]()
private var bannedPhones = [String]()
private var bannedUsernames = [String]()
init(emails: [String], phones: [String], usernames: [String]) {
self.bannedEmails = emails
self.bannedPhones = phones
self.bannedUsernames = usernames
}
func isTurnedOn(for email: Email) -> Bool {
return bannedEmails.contains(email.emailOfSender)
}
func isTurnedOn(for sms: SMS) -> Bool {
return bannedPhones.contains(sms.phoneNumberOfSender)
}
func isTurnedOn(for push: Push) -> Bool {
return bannedUsernames.contains(push.usernameOfSender)
}
var description: String { return "Black List Visitor" }
}
class VisitorRealWorld: XCTestCase {
func testVisitorRealWorld() {
let email = Email(emailOfSender: "some@email.com")
let sms = SMS(phoneNumberOfSender: "+3806700000")
let push = Push(usernameOfSender: "Spammer")
let notifications: [Notification] = [email, sms, push]
clientCode(handle: notifications, with: DefaultPolicyVisitor())
clientCode(handle: notifications, with: NightPolicyVisitor())
}
}
extension VisitorRealWorld {
/// Client code traverses notifications with visitors and checks whether a
/// notification is in a blacklist and should be shown in accordance with a
/// current SilencePolicy
func clientCode(handle notifications: [Notification], with policy: NotificationPolicy) {
let blackList = createBlackList()
print("\nClient: Using \(policy.description) and \(blackList.description)")
notifications.forEach { item in
guard !item.accept(visitor: blackList) else {
print("\tWARNING: " + item.description + " is in a black list")
return
}
if item.accept(visitor: policy) {
print("\t" + item.description + " notification will be shown")
} else {
print("\t" + item.description + " notification will be silenced")
}
}
}
private func createBlackList() -> BlackListVisitor {
return BlackListVisitor(emails: ["banned@email.com"],
phones: ["000000000", "1234325232"],
usernames: ["Spammer"])
}
}
Output.txt: Execution result
Client: Using Default Policy Visitor and Black List Visitor
Email notification will be shown
SMS notification will be shown
WARNING: Push is in a black list
Client: Using Night Policy Visitor and Black List Visitor
Email notification will be silenced
SMS notification will be shown
WARNING: Push is in a black list