The visitor design pattern

There are some applications out there where during design time not all possible use cases are known. There might be new application features coming out from time to time, and in order to implement them, some refactoring has to be done. What the visitor design pattern helps us achieve is this:

Note

Add new operations to existing object structures without modifying them.

This helps us to design our structures separately and then use the visitor design pattern to add functionality on top.

Another case where the visitor design pattern could be useful is if we are building a big object structure with many different types of nodes that support different operations. Instead of creating a base node that has all the operations and only a few of them are implemented by the concrete nodes or use type casting, we could create visitors that will add the functionality we need where we need it.

Class diagram

Initially, when a developer sees the visitor design pattern, it seems that it can be easily replaced using polymorphism and can rely on the dynamic types of the classes. However, what if we have a huge type hierarchy? In such a case, every single change will have to change an interface as well, which will lead to changing a whole bunch of classes, and so on.

For our class diagram and example, let's imagine that we are writing a text editor and we have documents. We want to be able to save each document in at least two data formats, but new ones could come. The next figure shows the class diagram for our application that uses the visitor design pattern:

Class diagram

As you can see in the preceding diagram, we have two seemingly disconnected hierarchies. The one to the left represents our document—each document is simply a list of different elements. All of them subclass the Element abstract class, which has a visit method that accepts a Visitor. To the right, we have the visitor hierarchy—each of our accept will mix in the Visitor trait, which contains the visit methods with overrides for each of our document elements.

The way the visitor pattern will work is that it will create an instance of Visitor depending on what needs to be done, and then pass it to the Document accept method. This way we can add extra functionality really easily (different formats in our case). And the extra functionality will not involve any changes to the model.

Code example

Let's take a step-by-step look at the code that implements the visitor design pattern for the previous example. First of all, we have our model of the document and all the elements that can build it:

abstract class Element(val text: String) {
  def accept(visitor: Visitor)
}

class Title(text: String) extends Element(text) {
  override def accept(visitor: Visitor): Unit = {
    visitor.visit(this)
  }
}

class Text(text: String) extends Element(text) {
  override def accept(visitor: Visitor): Unit = {
    visitor.visit(this)
  }
}

class Hyperlink(text: String, val url: String) extends Element(text) {
  override def accept(visitor: Visitor): Unit = {
    visitor.visit(this)
  }
}

class Document(parts: List[Element]) {
  
  def accept(visitor: Visitor): Unit = {
    parts.foreach(p => p.accept(visitor))
  }
}

There is nothing special about the preceding code. Just a simple subclassing for the different document elements and a composition for the Document class and the elements it contains. The important method here is accept. It takes a visitor and since the trait type is given, we can pass different visitor implementations. In all the cases, it calls the visit method of the visitor with the current instance passed as a parameter.

Let's now have a look on the other side—the Visitor trait and its implementations. The Visitor trait looks as simple as this:

trait Visitor {
  def visit(title: Title)
  def visit(text: Text)
  def visit(hyperlink: Hyperlink)
}

In this case, it has overloads of the visit method with different concrete element types. In the preceding code, the visitors and elements allow us to use double dispatch in order to determine which calls will be made.

Let's now have a look at the concrete Visitor implementations. The first one is the HtmlExporterVisitor:

class HtmlExporterVisitor extends Visitor {
  val line = System.getProperty("line.separator")
  val builder = new StringBuilder
  
  def getHtml(): String = builder.toString
  
  override def visit(title: Title): Unit = {
    builder.append(s"<h1>${title.text}</h1>").append(line)
  }

  override def visit(text: Text): Unit = {
    builder.append(s"<p>${text.text}</p>").append(line)
  }

  override def visit(hyperlink: Hyperlink): Unit = {
    builder.append(s"""<a href="${hyperlink.url}">${hyperlink.text}</a>""").append(line)
  }
}

It simply provides different implementations depending on what type of Element it gets. There are no conditional statements, just overloads.

If we want to save the document we have in plain text, we can use the PlainTextExporterVisitor:

class PlainTextExporterVisitor extends Visitor {
  val line = System.getProperty("line.separator")
  val builder = new StringBuilder
  
  def getText(): String = builder.toString
  
  override def visit(title: Title): Unit = {
    builder.append(title.text).append(line)
  }

  override def visit(text: Text): Unit = {
    builder.append(text.text).append(line)
  }

  override def visit(hyperlink: Hyperlink): Unit = {
    builder.append(s"${hyperlink.text} (${hyperlink.url})").append(line)
  }
}

After having the visitors and the document structure, wiring everything up is pretty straightforward:

object VisitorExample {
  def main(args: Array[String]): Unit = {
    val document = new Document(
      List(
        new Title("The Visitor Pattern Example"),
        new Text("The visitor pattern helps us add extra functionality without changing the classes."),
        new Hyperlink("Go check it online!", "https://www.google.com/"),
        new Text("Thanks!")
      )
    )
    val htmlExporter = new HtmlExporterVisitor
    val plainTextExporter = new PlainTextExporterVisitor
    
    System.out.println(s"Export to html:")
    document.accept(htmlExporter)
    System.out.println(htmlExporter.getHtml())

    System.out.println(s"Export to plain:")
    document.accept(plainTextExporter)
    System.out.println(plainTextExporter.getText())
  }
}

The preceding example shows how to use both the visitors we implemented. The output of our program is shown in the following screenshot:

Code example

As you can see, using the visitor is simple. Adding new visitors and new formats in our case is even easier. We just need to create a class that implements all the visitor methods and use it.

The visitor design pattern the Scala way

As with many other design patterns we saw earlier, the visitor design pattern could be represented in a way that is less verbose and closer to Scala. The way things can be done in order to implement a visitor in Scala is the same as the strategy design pattern—pass functions to the accept method. Moreover, we can also use pattern matching instead of having multiple different visit methods in the Visitor trait.

In this subsection, we will show both the improvement steps. Let's start with the latter.

First of all, we need to make the model classes case classes in order to be able to use them in pattern matching:

abstract class Element(text: String) {
  def accept(visitor: Visitor)
}

case class Title(text: String) extends Element(text) {
  override def accept(visitor: Visitor): Unit = {
    visitor.visit(this)
  }
}

case class Text(text: String) extends Element(text) {
  override def accept(visitor: Visitor): Unit = {
    visitor.visit(this)
  }
}

case class Hyperlink(text: String, val url: String) extends Element(text) {
  override def accept(visitor: Visitor): Unit = {
    visitor.visit(this)
  }
}

class Document(parts: List[Element]) {

  def accept(visitor: Visitor): Unit = {
    parts.foreach(p => p.accept(visitor))
  }
}

Then, we change our Visitor trait to the following:

trait Visitor {
  def visit(element: Element)
}

Since we will be using pattern matching, we will only need one method to implement it. Finally, we can have our visitor implementations as follows:

class HtmlExporterVisitor extends Visitor {
  val line = System.getProperty("line.separator")
  val builder = new StringBuilder

  def getHtml(): String = builder.toString

  override def visit(element: Element): Unit = {
    element match {
      case Title(text) => builder.append(s"<h1>${text}</h1>").append(line)
      case Text(text) => builder.append(s"<p>${text}</p>").append(line)
      case Hyperlink(text, url) => builder.append(s"""<a href="${url}">${text}</a>""").append(line)
    }
  }
}

class PlainTextExporterVisitor extends Visitor {
  val line = System.getProperty("line.separator")
  val builder = new StringBuilder

  def getText(): String = builder.toString

  override def visit(element: Element): Unit = {
    element match {
      case Title(text) =>
        builder.append(text).append(line)
      case Text(text) =>
        builder.append(text).append(line)
      case Hyperlink(text, url) =>
        builder.append(s"${text} (${url})").append(line)
    }
  }
}

The pattern matching is similar to the instanceOf checks in Java; however, it is a powerful feature of Scala and is quite commonly used. Our example then doesn't need to change at all and the output will be the same as before.

Next, we will show how we can pass functions instead of visitor objects. The fact that we will be passing functions means that now we can change our model to the following:

abstract class Element(text: String) {
  def accept(visitor: Element => Unit): Unit = {
    visitor(this)
  }
}

case class Title(text: String) extends Element(text)
case class Text(text: String) extends Element(text)
case class Hyperlink(text: String, val url: String) extends Element(text)

class Document(parts: List[Element]) {
  def accept(visitor: Element => Unit): Unit = {
    parts.foreach(p => p.accept(visitor))
  }
}

We moved the accept method implementation to the base Element class (which can also be represented as a trait) and inside this, we simply called the function passed as parameter. Since we will be passing functions, we can get rid of the Visitor trait and its implementations. Everything we have now is the example, which looks like the following:

object VisitorExample {
  val line = System.getProperty("line.separator")
  
  def htmlExporterVisitor(builder: StringBuilder): Element => Unit = element => element match {
    case Title(text) => builder.append(s"<h1>${text}</h1>").append(line)
    case Text(text) => builder.append(s"<p>${text}</p>").append(line)
    case Hyperlink(text, url) => builder.append(s"""<a href="${url}">${text}</a>""").append(line)
  }
  
  def plainTextExporterVisitor(builder: StringBuilder): Element => Unit = element => element match {
    case Title(text) => builder.append(text).append(line)
    case Text(text) => builder.append(text).append(line)
    case Hyperlink(text, url) => builder.append(s"${text} (${url})").append(line)
  }
  
  def main(args: Array[String]): Unit = {
    val document = new Document(
      List(
        Title("The Visitor Pattern Example"),
        Text("The visitor pattern helps us add extra functionality without changing the classes."),
        Hyperlink("Go check it online!", "https://www.google.com/"),
        Text("Thanks!")
      )
    )

    val html = new StringBuilder
    System.out.println(s"Export to html:")
    document.accept(htmlExporterVisitor(html))
    System.out.println(html.toString())

    val plain = new StringBuilder
    System.out.println(s"Export to plain:")
    document.accept(plainTextExporterVisitor(plain))
    System.out.println(plain.toString())
  }
}

We have moved the visitor functionality inside the functions that are part of the VisitorExample object. In the initial examples, we had a StringBuilder as part of the visitor classes. We have used curried functions in order to be able to pass one here. Passing these functions to the Document structure is then straightforward. Again, the result here will be identical to the previous versions of the example. However, we can see how much code and boilerplate classes we have saved.

What is it good for?

The visitor design pattern is really good for applications that have large object hierarchies, where adding a new functionality will involve a lot of refactoring. Whenever we need to be able to do multiple different things with an object hierarchy and when changing the object classes could be problematic, the visitor design pattern is a useful alternative.

What is it not so good for?

As you saw in the initial version of our example, the visitor design pattern could be bulky and include quite a lot of boilerplate code. Moreover, if some component is not designed to support the pattern, we cannot really use it if we are not allowed to change the original code.

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

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