Chapter 8. Behavioral Patterns – Visitor, Interpreter, and Memento

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:

  • The visitor pattern
  • The interpreter pattern
  • The memento pattern

The visitor pattern

In this section, we will talk about the visitor pattern, which allows us to separate data and their associated treatments.

Roles

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:

  • We need to add functionalities to a group of classes without weighing down these classes
  • A group of classes have a fixed structure and we need to add some functionalities to them without modifying their interface

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.

Design

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:

Design

Participants

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.

Collaboration

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.

Illustration

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.

Implementation

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) }
}

Note

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:

Implementation
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset