Концептуальный пример
Этот пример показывает структуру паттерна Посетитель , а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире Swift.
Example.swift: Пример структуры паттерна
import XCTest
/// Интерфейс Компонента объявляет метод принятия, который в качестве аргумента
/// может получать любой объект, реализующий интерфейс посетителя.
protocol Component {
func accept(_ visitor: Visitor)
}
/// Каждый Конкретный Компонент должен реализовать метод принятия таким образом,
/// чтобы он вызывал метод посетителя, соотвествующий классу компонента.
class ConcreteComponentA: Component {
/// Обратите внимание, мы вызываем visitConcreteComponentA, что
/// соответствует названию текущего класса. Таким образом мы позволяем
/// посетителю узнать, с каким классом компонента он работает.
func accept(_ visitor: Visitor) {
visitor.visitConcreteComponentA(element: self)
}
/// Конкретные Компоненты могут иметь особые методы, не объявленные в их
/// базовом классе или интерфейсе. Посетитель всё же может использовать эти
/// методы, поскольку он знает о конкретном классе компонента.
func exclusiveMethodOfConcreteComponentA() -> String {
return "A"
}
}
class ConcreteComponentB: Component {
/// То же самое здесь: visitConcreteComponentB => ConcreteComponentB
func accept(_ visitor: Visitor) {
visitor.visitConcreteComponentB(element: self)
}
func specialMethodOfConcreteComponentB() -> String {
return "B"
}
}
/// Интерфейс Посетителя объявляет набор методов посещения, соответствующих
/// классам компонентов. Сигнатура метода посещения позволяет посетителю
/// определить конкретный класс компонента, с которым он имеет дело.
protocol Visitor {
func visitConcreteComponentA(element: ConcreteComponentA)
func visitConcreteComponentB(element: ConcreteComponentB)
}
/// Конкретные Посетители реализуют несколько версий одного и того же алгоритма,
/// которые могут работать со всеми классами конкретных компонентов.
///
/// Максимальную выгоду от паттерна Посетитель вы почувствуете, используя его со
/// сложной структурой объектов, такой как дерево Компоновщика. В этом случае
/// было бы полезно хранить некоторое промежуточное состояние алгоритма при
/// выполнении методов посетителя над различными объектами структуры.
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")
}
}
/// Клиентский код может выполнять операции посетителя над любым набором
/// элементов, не выясняя их конкретных классов. Операция принятия направляет
/// вызов к соответствующей операции в объекте посетителя.
class Client {
// ...
static func clientCode(components: [Component], visitor: Visitor) {
// ...
components.forEach({ $0.accept(visitor) })
// ...
}
// ...
}
/// Давайте посмотрим как всё это будет работать.
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: Результат выполнения
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
Пример из реальной жизни
Example.swift: Пример из реальной жизни
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: Результат выполнения
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