We'll now put those protocols to work. Let's imagine you're building an app that helps open source maintainers. New contributors are rare, and it's important to thank them in a timely manner.
Let's implement a simple program that will determine who to send a thank you note to, 3 days after their contribution, out of all the contributions to the repository:
struct Contribution {
let date: Date
let author: String
let email: String
let details: String
}
Contribution is a simple object of itself, which holds information about the contributor and their contribution.
Now we need to make those Contribution objects Visitable so we can use Visitors on them:
extension Contribution: Visitable {}
As Contribution is Visitable, [Contribution] will also be so, thanks to the protocol extension on Array.
Let's make a logger Visitor. This demonstrates the power of the visitor pattern—being able to swap the algorithm without touching the underlying objects. From a single dataset (the list of contributions), it is possible to design multiple algorithms, as follows:
class LoggerVisitor: Visitor {
func visit<T>(element: T) where T : Visitable {
guard let contribution = element as? Contribution else { return }
print("\(contribution.author) / \(contribution.email)")
}
}
let visitor = LoggerVisitor()
[
Contribution(author: "Contributor", email: "my@email.com", date: Date(), details: ""),
Contribution(author: "Contributor 2", email: "my-other@email.com", date: Date(), details: "")
].accept(visitor: visitor)
// output
Contributor / my@email.com
Contributor 2 / my-other@email.com
Now that we know the protocol-based logic works properly, we can implement a proper Visitor that will collect the contributor's contact details in order to send them a nice thank-you note:
let threeDaysAgo = Calendar.current.date(byAdding: .day, value: -3, to: Date())!
let fourDaysAgo = Calendar.current.date(byAdding: .day, value: -4, to: Date())!
class ThankYouVisitor: Visitor {
var contributions = [Contribution]()
func visit<T>(element: T) where T : Visitable {
guard let contribution = element as? Contribution else { return }
// Check that the contribution was done between 3 and 4 days ago
if contribution.date <= threeDaysAgo && contribution.date > fourDaysAgo {
contributions.append(contribution)
}
}
}
// Create an instance of the visitor
let thanksVisitor = ThankYouVisitor()
// Visit this contribution
Contribution(author: "John Duff",
email: "john@cosmic.tortillas",
date: threeDaysAgo,
details: "...")
.accept(visitor: thanksVisitor)
// We have one as the date is within range
assert(thanksVisitor.contributions.length === 1)
// Visit another contribution
Contribution(author: "Harry Cover",
email: "harry@cosmic.tortillas",
date: fourDaysAgo,
details: "...")
.accept(visitor: thanksVisitor)
// We have one as the second one is out of range
assert(thanksVisitor.contributions.length === 1)
let allContributions: [Contribution] = ... // get all contributions
allContributions.accept(visitor: thanksVisitor) // visit all contributions
// Send thanks!
thanksVisitor.contributions.forEach {
sendThanks($0)
}
In the preceding examples, we successfully implemented the visitor pattern in order to extract meaningful information from an existing data structure. You'll have noted that you can use and implement many Visitor objects for a particular dataset, as long as the data set is Visitable.