In this chapter, we will complete our discovery trip of the 23 Gang of Four patterns. Now, let's have a look at the three last design patterns of the behavioral patterns category. They are as follows:
In this section, we will talk about the visitor pattern, which allows us to separate data and their associated treatments.
The visitor pattern allows us to externalize and centralize the actions that must be executed on object; these objects cannot have any links between them.
These actions will not be implemented in the class of the objects but in external classes.
So, this allows us to add any action in an external class, even a concrete visitor that implements IVisitor.
This pattern can be used when:
The visitor pattern must be applied and used when you need to perform operations on objects of a collection that do not share a common base class or conform to a common protocol.
The following diagram shows us how objects and treatments are separated. Treatments are implemented in the ConcreteVisitor
classes. The objects are implemented in the ConcreteElement
classes, as shown in the following figure:
The following are the visitor pattern participants:
Visitor
: This interface introduces the signature of the methods that realize a functionality in a group of classes. There is one method per class that receives an instance of this class as an argument.ConcreteVisitors
: This implements methods that realize the functionality that correspond to the classes. This functionality is distributed in different elements.Element
: This is an abstract class of the concrete elements class. It introduces the accept(visitor)
method.ConcreteElements
: This implements the accept()
method, which consists of calling the visitor through the method that corresponds to the class.A client that uses a visitor needs to create an instance of a visitor in the class of its choice and then pass it as an argument to the accept method of a group of elements.
The element then calls the the visitor method that corresponds to its class. A reference to itself is sent back to the visitor that allows it to access its internal structure.
We are a car seller having three brands: DS, Renault, and Citroen, and each of them has a price.
We want to be able to modify the price without modifying our car concrete classes. For this, we will introduce our visitor pattern.
For this last chapter, we will use Playground. Now, open the VisitorPattern.playground
file and let's have a look at how this works.
Here, we will use a technique called Double Dispatch that will allow us to perform the appropriate actions depending on the type of the object . This technique also help us to avoid making some type casting to perform the appropriated operation. (see the following URL to get more information: https://en.wikipedia.org/wiki/Double_dispatch if you need more informations about this technique)
First, we define our visitor protocol. The visitor has three visit methods having a ConcreteElement
as an argument to accept each car type, as shown:
protocol CarVisitor { func visit(car: DSCar) func visit(car: RenaultCar) func visit(car: CitroenCar) }
Then, we define our Car
protocol. A car can accept a concrete CarVisitor
object:
protocol Car { func accept(visitor: CarVisitor) }
We can easily implement our three concrete cars. Each of them has a default price and also the accept
method having a concrete Visitor
object as an argument:
class DSCar: Car { var price = 29000.0 func accept(visitor: CarVisitor) { visitor.visit(self) } } class RenaultCar: Car { var price = 17000.0 func accept(visitor: CarVisitor) { visitor.visit(self) } } class CitroenCar: Car { var price = 19000.0 func accept(visitor: CarVisitor) { visitor.visit(self) } }
The accept
method defined by the Car
protocol and implemented by the classes is the key to the double dispatch technique. By sending self
as argument to the visitor.visit
method, where visitor is our concrete visitor implementation of CarVisitor
, Swift will choose the version of the visit
method with the most specific type.
Lastly, we must implement our concrete visitor, our visitor is in charge of modifying the price of the Element
class. The Element
class modification depends on the type of object passed in the argument.
The DS car will see its price modified by 20 percent and the price of Renault and Citroen cars modified by 10 percent:
class PriceVisitor: CarVisitor { var price = 0.0 func visit(car: DSCar) { price = car.price * 0.8 } func visit(car: RenaultCar) { price = car.price * 0.9 } func visit(car: CitroenCar) { price = car.price * 0.9 } }
The client will be simulated with the following code. We will first instantiate our three car objects and add them in a Car
array. Then we will define a new variable price
, which is an array containing our three new prices.
For this, we will use the map
function that is an extension of the array type. It allows us to execute a treatment on each element of the array. Here we can (for each element) instantiate a PriceVisitor
object and pass it in the accept
method of the current car
object.
Then we return the new visitor.price
, which is the new price of the current car object.
Like I said in the Roles section of this pattern, the visitor pattern is used when an array manages a heterogeneous collection of objects that does not share a common base class or conforms to a common protocol. By applying the pattern, all three Cars
classes can share and conform to the same protocol and allow us to manage the following array:
let cars: [Car] = [DSCar(), RenaultCar(), CitroenCar()]
Then, we can calculate new prices by applying the appropriate visitor calculation:
let prices = cars.map { (car: Car) -> Double in let visitor = PriceVisitor() car.accept(visitor) return visitor.price }
To show the result, check the right part of the following screenshot. 23200, 15300 and 17100 are the new prices of our cars: