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:
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.
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:
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.
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:
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.
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.
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.
3.145.202.61