(Coursenotes for CSC 305 Individual Software Design and Development)

Composite and Visitor design patterns

Design patterns

Design patterns are general, re-usable solutions to commonly occurring problems within a given context in software design. They offer templates to solve problems that can be used in multiple contexts.

In 1994, a group of four authors wrote what was to become a famous book about Design Patterns (titled Design Patterns). The book describes three broad classes of Design Patterns:

We are not going to talk about all the 23 patterns described in the book, but we’ll talk about some of them, the first of which is the Composite pattern.

Composite design pattern

The Composite pattern is billed as a “Structural” pattern, because it involves a specific organisational structure for your objects. The pattern is useful when your data naturally organises into a tree-like structure, and you need to provide functionality for the entire structure (the “Composite”) and the individual objects.

For example, a composite object structure might look like this:

flowchart TD
  c1[Composite] -- has a --> Leaf1
  c1 -- has a --> c2[Composite]
  c2 -- has a --> Leaf2
  c2 -- has a --> Leaf3

Here are some key things to note:

Ok, that was super abstract. Let’s consider a couple of concrete examples.

File system example

For example, suppose you need to “read” all the files in a computer. You have a root folder (the root of your directory tree). That root may have many children (files or folders inside of it). Some of those children may in turn have further children.

The Composite pattern involves you treating the entire structure as a tree (much like your file system does). Then each “node” of the tree might have a read operation. For FileNodes, the read operation simply prints out the contents of the file. For FolderNodes, the read operation involves further traversing its children and reading them. This recursively continues until there are no more files to be read.

In the diagram below, reading FolderNodes involves reading all the nodes contained within the folder, which may be FileNodes, or they may themselves be FolderNodes that contain further children.

flowchart TD

home["fa:fa-folder-open Home"] --> music[fa:fa-folder-open Music]
home --> movies["fa:fa-folder-open Movies"]
home --> csc305[fa:fa-folder-open CSC 305 Assignments]
csc305 --> lab1[fa:fa-file Lab 1]
csc305 --> lab2[fa:fa-file Lab 2]
movies --> animated[fa:fa-folder-open Animated] 
animated --> httyd[fa:fa-file How To Train Your Dragon]
music --> beatles[fa:fa-folder-open The Beatles]
beatles --> hcs[fa:fa-file Here Comes the Sun]
beatles --> lib[fa:fa-file Let It Be]

HTML elements example

As another exmaple, consider a super simple HTML page:

<html>
  <head>
    <title>Composite design pattern</title>
  </head>
  <body>
    <div>
      Here's the text on the page.
    </div>
  </body>
</html>

To represent the page above in objects, we would have the following:

Here’s the structure:

flowchart TD
  html -- has a --> head
  html -- has a --> body

  head -- has a --> title
  body -- has a --> div

This structure is mostly invisible to “clients” using this code. The HTMLElement is an interface in the standard Web API that ships with browser-based JavaScript. Any JavaScript code that needs to manipulate a webpage dynamically (e.g., changing the background colour of a given element) can do so using methods from this interface. The Composite structure ensures that the behaviours are appropriately carried out for different types of elements. That is, the client is not concerned with differences between changing the background-color of a “leaf” element (that no inner elements) vs. a “composite” element (like a <div> that contains many inner elements).

Other examples include:

Benefits and drawbacks of Composite design

Benefits of this pattern

Drawbacks

It might be difficult to provide a common interface for classes whose functionality differs too much. In certain scenarios, you’d need to overgeneralize the component interface, making it harder to comprehend. (source)

Personally, I think the above would be an indication that you shouldn’t be using the Composite design pattern in the first place.

Visitor design pattern

The Visitor design pattern is a “behavioural” pattern. It makes sense when the you need perform some task on all objects in a complex structure (like a graph or a tree). The underlying classes get “visited” by some code which executes on each object in the structure.

At this point, you may wonder about the difference between the Visitor pattern and the Composite pattern. It’s true, they’re similar in focus and intent. Let’s consider an example.

Example use-case

(Example from Refactoring Guru)

Suppose you’re working on an app that maintains a large graph of geographical information. Each node represents an complex entity in the graph, like a city, sightseeing area, industry, shopping mall, etc. Depending on its type, each node has various details that make up its internal state, but everything is a “node” in the graph.

You’re asked to export the entire graph to some format, like XML. This is a pretty common ask: you often want to transmit data in some language-agnostic format so that different subsystems can operate on the same data.

Each different type of node in the graph will need to write out its salient details, meaning that the “export” operation looks different for each node. Moreover, the export of one node (like a SightseeingArea) might lead to the export of its other component nodes (like a Museum or a Landmark). So, like we did with Composite design, we could make use of polymorphism and recursion to implement an “export to XML” function for each type of node.

What are some drawbacks of adding the XML export behaviour to the existing graph nodes?

The Visitor pattern helps us extend our graph to give it XML export behaviour, without modifying it. It lets us adhere to the Open/closed principle (the “O” in the SOLID principles of software design).

I like to think about the Visitor design pattern as the Composite pattern, but from the outside of the object structure instead of from the inside.

A nice added benefit of not coupling your extension to the entire object structure is that you can use the Visitor pattern when some action makes sense for only some objects in the larger structure, but not all.

We’ll go over the basic structure of the Visitor pattern and then look at a real-world example.

Implementing the visitor design pattern

There are 5 pieces involved in implementing the visitor pattern

public interface Element {
  void accept(Visitor visitor);
}
public class SightseeingArea implements Element {
  // location-specific stuff...

  public void accept(Visitor visitor) {
    visitor.visit(this);
  }
} 

If we have default methods in Java, why can’t we fully implement the accept method in the Element interface itself? Why do we need to implement it in each concrete class?

public interface Visitor {
    void visit(SightseeingArea node);
    void visit(Museum node);
    void visit(Landmark node);
    // ... overloaded for all types of nodes 
}
public class XMLExportVisitor implements Visitor {
    public void visit(SightseeingArea node) {
        // export the SightseeingArea 
    }

    public void visit(Museum node) {
        // export the Museum 
    }

    public void visit(Landmark node) {
        // export the Museum 
    }
}

What if, in a particular Visitor, I only care about visiting some types of nodes and not others? Currently, I would need to implement a bunch of “no-op” methods because I’m forced to implement them by the Visitor interface.

Visitor visitor = new XMLExportVisitor();
for (Element current : this.locations) {
    current.accept(visitor);
}

A real-world example

A few years ago, I was conducting some analysis on the source code of a number of Java projects written by students. I wrote code to read in the code of hundreds of projects and emit some data for analysis.

Using the Eclipse Java Development Tools (JDT) API, I parsed students’ code into Abstract Syntax Trees (ASTs), and then “visited” certain nodes of interest in these resulting trees.

Because I used the JDT API to create the object structure (the AST), I only had to write the Visiting code. See this file as an example of a visitor.

In summary, the code visits MethodDeclarations and MethodInvocations, i.e., all the places methods are defined or called in the codebase, because that’s what I was interested in for that particular analysis.

Some things to note in this example are:

One language’s design pattern is another language’s native feature

In many programming languages, functions are values like anything else, and we can do things to them like give them variable names, pass them as arguments to other functions, etc. Of course, we can also call or apply functions, which is the use with which we’re all already familiar.

With this in mind, another way to think about the Visitor pattern is as follows.

Each node in the object structure (in this example, each location in the map), supports some behaviours of its own. In this respect, you could very well think of this “map” as following a Composite design pattern.

What the Visitor pattern brings to the table is that each Element (each location) also includes one key addition: an accept function that takes, as its only parameter, another function that is to be applied to the Element. This means that a client component that wants to define some arbitrary additional behaviour for the Elements (say, an XML export behaviour) needs to:

In the example above, we have done exactly this, except our “function” that we give as an argument to accept is housed in a Visitor object (i.e., XMLExportVisitor).

Visitor pattern using pattern matching

In my opinion, the Visitor pattern as described above is one of the more clunky patterns to implement in Java. This is mostly due to a lack of expressive language constructs, a situation that is quickly being improved due to many recent improvements to the Java language.

Thankfully, pattern matching, a feature common in functional languages like OCaml, is now available in Java as well.

Pattern matching

In the Visitor example above, we’ve written a Visitor interface that contains several visit method overloads, one for each type of Element we might want to visit. Then in our XMLExportVisitor, we include implementations for each visit method. This is…pretty unwieldy.

However, with pattern matching, we can write a “visitor” much more concisely, as just a function instead of an interface and a class. All we need to ensure is that our visitor knows how to visit all possible types of Elements.

The code below is using pattern matching to match the current variable with the appropriate case, depending on its type.

public static void exportXML(List<Element> elements) {
    for (Element current : elements) {
        // This switch expression won't compile unless all possible types are accounted for
        switch (current) {
            case SightseeingArea s -> System.out.println("Visiting sightseeing area");
            case Landmark l -> System.out.println("Visiting landmark");
            case Museum m -> System.out.println("Visiting museum");
            default -> throw new IllegalStateException("Element is something I didn't expect");
        }
    }
}

The compiler requires that switch pattern matching has exhaustive type coverage. That is, there shouldn’t be a possible value where our visitor doesn’t know what to do. However, our compiler does not know all possible implementing subclasses of Element, so is always going to err on the side of caution, and refuse to compile this switch expression.

To satisfy the compiler, we’ve stuck a default at the end and thrown an exception. The default is kind like the else of that switch expression—that is, we are saying “for anything that’s not a SightseeingArea, Landmark, or Museum, throw an error.

This is similar to using a series of if conditions and instanceof checks to check the dynamic type of the current Element:

public static void exportXML(List<Element> elements) {
    for (Element current : elements) {
        if (current instanceof SightseeingArea) {
          System.out.println("Visiting sightseeing area");
        } else if (current instanceof Landmark) {
          System.out.println("Visiting landmark");
        } else if (current instanceof Museum) {
          System.out.println("Visiting museum");
        } else {
          throw new IllegalStateException("Element is something I didn't expect");
        }
    }
}

What do you think of this strategy?

By using a default at the end (or the else clause in the second example), we would be taking that potential type error (i.e., we tried to visit a thing for which no visiting logic is implemented), and moving it from compile time to run time. That is, we have created a situation in which our program will crash because it received a type it was not prepared for. This should simply never happen, especially not in a statically typed language—the compiler works for us! We should use it!

In other words, it’s always better to face type errors at compile time rather than run time, because facing them at run time involves doing nothing, or worse, crashing the program, or double worse, silently doing something unexpected.

So, how do we achieve type coverage without using default?

Sealed types

We can achieve this using sealed types. The idea behind sealed types is simple. We can mark a class or interface as sealed if we want to limit which classes or interfaces can extend it.

In the code below, we are sealing the Element interface, saying that it can only be implemented by the named classes.

Remember that we want compiler hints about type errors. The sealed declaration tells the compiler that “these are the only things that will ever implement this interface”. This helps the compiler to help us.

public sealed interface Element permits Landmark, SightseeingArea, Museum {
  ...
}

The compiler can now be satisfied without the default case in the switch expression, because it knows that we have achieved type coverage.

public static void exportXML(List<Element> elements) {
    for (Element current : elements) {
        // This is now okay without the default case
        switch (current) {
            case SightseeingArea s -> System.out.println("Visiting sightseeing area");
            case Landmark l -> System.out.println("Visiting landmark");
            case Museum m -> System.out.println("Visiting museum");
        }
    }
}

Through the combination of pattern matching and sealed types, we get the following benefits:

This use of pattern matching is well-described in Visitor Pattern Considered Pointless — Use Pattern Switches Instead, though I disagree with the title. I don’t think pattern matching renders the Visitor pattern “pointless”: it just changes what it looks like.

As we talk about more design patterns, remember that they can exist both within and without Java-specific features like interfaces and abstract classes. Design patterns are ideas, templates to help solve or think about software problems, and they can exist without any particular programming language in mind.