![Odwiedzający](/images/patterns/cards/visitor-mini.png?id=854a35a62963bec1d75eab996918989b)
Odwiedzający w języku Swift
Odwiedzający to behawioralny wzorzec projektowy pozwalający dodawać nowe zachowanie istniejącej hierarchii klas bez zmiany kodu jej klas.
O tym, dlaczego Odwiedzającego nie można po prostu zastąpić przeciążaniem metod, przeczytasz w naszym artykule Odwiedzający i podwójna dyspozycja.
Złożoność:
Popularność:
Przykłady użycia: Odwiedzający nie jest zbyt powszechnie stosowany, gdyż jest skomplikowany i ma wąski zakres stosowania.
Następujące przykłady są dostępne na
Swift Playgrounds.
Gratulacje dla
Alejandro Mohamad za stworzenie wersji Playground.
Przykład koncepcyjny
Poniższy przykład ilustruje strukturę wzorca Odwiedzający ze szczególnym naciskiem na następujące zagadnienia:
- Z jakich składa się klas?
- Jakie role pełnią te klasy?
- W jaki sposób elementy wzorca są ze sobą powiązane?
Poznawszy strukturę wzorca będzie ci łatwiej zrozumieć poniższy przykład, oparty na prawdziwym przypadku użycia Swift.
Example.swift: Przykład koncepcyjny
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: Wynik działania
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
Przykład z prawdziwego życia
Example.swift: Przykład z prawdziwego życia
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: Wynik działania
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