Greetings, traveler!
Welcome to the next part of our Design Pattern series. Today, we delve into the Visitor pattern. This behavioral design pattern allows you to enhance the functionality of an object hierarchy without altering the existing code, a feature that can be particularly beneficial in specific scenarios.
Imagine you’re working on a project with several classes, each performing a specific set of tasks. You want to add new functionality with a similar algorithm to these classes, but you’re hesitant to modify the existing code due to the potential risks. Moreover, you want to avoid burdening the classes with additional responsibilities. In such a situation, the Visitor pattern comes to the rescue.
The Visitor will implement the new functionality for each class. To do this, it will use several functions whose names will be identical but differ in their incoming parameter, which we will use to specify the specific class the Visitor will work with. Thanks to this, the Visitor will also be able to access the functions and properties of each class, even if they do not belong to a protocol or parent class.
Example of usage
Let’s take a look at an example to understand this better. We have two iPhone models: iPhone 15 and iPhone 8. For biometric authentication, we need to use the built-in features of each device. However, the iPhone 15 uses Face ID, and the iPhone 8 uses Touch ID.
A Visitor will be assigned to handle the authentication process for both devices. It will have access to the internal components of each phone to use the necessary features for authentication.
Let’s start writing the code to implement this process. First, let’s create a protocol for our iPhones. This protocol will have only one function, which should implement the authentication process. The incoming parameter of this function will be the Visitor protocol.
protocol iPhoneProtocol {
func accept(_ visitor: iPhoneVisitorProtocol)
}
Since we mentioned the Visitor protocol, let’s create it. The Visitor interface has a function for each of the iPhone implementations.
protocol iPhoneVisitorProtocol {
func visit(iPhone: iPhone15)
func visit(iPhone: iPhone8)
}
Now, let’s create implementations of our iPhones. Each iPhone implementation uses only the visitor function relevant to that particular implementation.
final class iPhone15: iPhoneProtocol {
func accept(_ visitor: iPhoneVisitorProtocol) {
visitor.visit(iPhone: self)
}
func faceID() -> String {
"Face ID"
}
}
final class iPhone8: iPhoneProtocol {
func accept(_ visitor: iPhoneVisitorProtocol) {
visitor.visit(iPhone: self)
}
func touchID() -> String {
"Touch ID"
}
}
Finally, let’s create an implementation of our Visitor. Because it knows a concrete iPhone implementation, it can access all its public variables and methods.
final class AuthVisitor: iPhoneVisitorProtocol {
func visit(iPhone: iPhone15) {
print("Authenticate via \(iPhone.faceID())")
}
func visit(iPhone: iPhone8) {
print("Authenticate via \(iPhone.touchID())")
}
}
Now, we can write such code.
let visitor = AuthVisitor()
let model1 = iPhone8()
let model2 = iPhone15()
model2.accept(visitor)
Conclusion
Yes, the Visitor pattern increases the number of interacting objects. However, this can be an acceptable trade-off if it leads to more straightforward code maintenance and reduces the risk of errors after applying significant changes. Additionally, the Visitor only works with specific objects and not protocols, which might seem like a violation of the consistency of object interactions in your project. All this reminds us that using Design Patterns should be a thoughtful decision, as each pattern can be a powerful tool for developers. Still, if used carelessly, they can complicate the code without providing benefits.
This concludes our discussion of the Visitor Design Pattern and also the series of articles on Design Patterns. Of course, there are many other patterns that we have not discussed, but we have covered all the basic ones. I have tried to present their essence concisely and clearly so as not to overwhelm the reader with too much information. I hope that these articles have been interesting and valuable. And I look forward to seeing you in the following ones. See you soon!
Check out other posts in the Design Patterns series:
- Themplate Method Design Pattern in Swift
- Strategy Design Pattern in Swift
- State Design Pattern in Swift
- Observer Design Pattern in Swift
- Memento Design Pattern in Swift
- Mediator Design Pattern in Swift
- Iterator Design Pattern in Swift
- Command Design Pattern in Swift
- Chain of Responsibility Design Pattern in Swift
- Proxy Design Pattern in Swift
- FlyWeight Design Pattern in Swift
- Facade Design Pattern in Swift
- Decorator Design Pattern in Swift
- Composite Design Pattern in Swift
- Bridge Design Pattern in Swift
- Adapter Design Pattern in Swift
- Singleton Design Pattern in Swift
- Prototype Design Pattern in Swift
- Builder Design Pattern in Swift
- Abstract Factory Design Pattern in Swift
- Factory Method Design Pattern in Swift
- Design Patterns: Basics