/ 设计模式 / 访问者模式 / Swift Swift 访问者模式讲解和代码示例 访问者是一种行为设计模式, 允许你在不修改已有代码的情况下向已有类层次结构中增加新的行为。 阅读我们的文章访问者和双分派以了解为什么不能通过方法重载来简单地替换访问者。 进一步了解访问者模式 导航 简介 概念示例 Example Output 真实世界示例 Example Output 复杂度: 流行度: 使用示例: 访问者不是常用的设计模式, 因为它不仅复杂, 应用范围也比较狭窄。 以下示例可在 Swift Playgrounds 上使用。 感谢 Alejandro Mohamad 创建了Playground版本。 概念示例 真实世界示例 概念示例 本例说明了访问者设计模式的结构并重点回答了下面的问题: 它由哪些类组成? 这些类扮演了哪些角色? 模式中的各个元素会以何种方式相互关联? 了解该模式的结构后, 你可以更容易地理解下面基于真实世界的 Swift 应用案例。 Example.swift: 概念示例 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: 执行结果 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 概念示例 真实世界示例