Behavioral Design Patterns
Imagine you have a stable set of classes representing the structure of a document. A Document can contain Paragraph elements and Image elements. Now, you need to add several new, unrelated operations that work on these elements:
The most direct approach is to add a new method (exportToHtml(), toMarkdown(), extractText()) to the Paragraph and Image classes.
// BAD: This class gets bloated with unrelated responsibilities.
public class Paragraph {
private String text;
public void exportToHtml() { /* ... */ }
public void toMarkdown() { /* ... */ }
public String extractText() { /* ... */ }
}
public class Image {
private String src;
public void exportToHtml() { /* ... */ }
public void toMarkdown() { /* ... */ }
public String extractText() { /* return image caption */ }
}
This design is flawed. The element classes violate the Single Responsibility Principle; they are now responsible for their own internal logic and for multiple export formats. They also violate the Open/Closed Principle, because every time a new operation is needed (e.g., "export to PDF"), you have to modify every single class in the element hierarchy.
The Visitor is a behavioral design pattern that lets you separate algorithms from the objects on which they operate. It allows you to define a new operation without changing the classes of the elements on which it operates.
The core idea is to move the operational logic into a separate "visitor" class. You have one visitor class for each distinct operation (HtmlExportVisitor, MarkdownExportVisitor). The element objects are "visitable"; their only new responsibility is to have an accept(visitor) method.
When an operation needs to be performed, the client passes a visitor object to an element's accept method. The element then calls the correct method on the visitor, passing itself as an argument. This technique, known as double dispatch, ensures that the correct operation is executed for the correct element type.
visit() method for each type of Concrete Element in the object structure (e.g., visit(Paragraph p), visit(Image i)).Visitor interface and contains the logic for a specific operation across all element types.accept(Visitor v) method.Element interface. Its accept method's sole purpose is to call the correct visit method on the visitor: visitor.visit(this).Let's refactor our document elements to use the Visitor pattern.
// The Element interface
interface IDocumentElement {
void accept(IDocumentVisitor visitor);
}
// Concrete Elements
class Paragraph implements IDocumentElement {
public String text;
public Paragraph(String text) { this.text = text; }
@Override
public void accept(IDocumentVisitor visitor) {
// This is the first dispatch (based on element type)
visitor.visit(this);
}
}
class Image implements IDocumentElement {
public String src;
public Image(String src) { this.src = src; }
@Override
public void accept(IDocumentVisitor visitor) {
visitor.visit(this);
}
}
// The Visitor interface with overloaded visit methods
interface IDocumentVisitor {
// This is the second dispatch (based on visitor and element type)
void visit(Paragraph p);
void visit(Image i);
}
// Concrete Visitor for HTML export
class HtmlExportVisitor implements IDocumentVisitor {
@Override
public void visit(Paragraph p) {
System.out.println("<p>" + p.text + "</p>");
}
@Override
public void visit(Image i) {
System.out.println("<img src='" + i.src + "' />");
}
}
// Concrete Visitor for Text extraction
class TextExtractorVisitor implements IDocumentVisitor {
@Override
public void visit(Paragraph p) {
System.out.println(p.text);
}
@Override
public void visit(Image i) {
// Do nothing for images when extracting plain text
}
}
// The Client
public class Client {
public static void main(String[] args) {
List<IDocumentElement> document = List.of(
new Paragraph("This is the first paragraph."),
new Image("logo.png"),
new Paragraph("This is the second paragraph.")
);
System.out.println("--- Exporting to HTML ---");
IDocumentVisitor htmlVisitor = new HtmlExportVisitor();
for (IDocumentElement element : document) {
element.accept(htmlVisitor);
}
System.out.println("\n--- Extracting Plain Text ---");
IDocumentVisitor textVisitor = new TextExtractorVisitor();
for (IDocumentElement element : document) {
element.accept(textVisitor);
}
}
}
Benefits:
Drawbacks:
Concrete Element class (e.g., a Table element), you must update the IDocumentVisitor interface to add a new visit(Table t) method. This is a breaking change that forces you to update every single concrete visitor you've ever written. The pattern makes adding new operations easy but makes adding new element types hard.Visitor and the type of the Element. You can explain it as: element.accept(visitor) dispatches based on the element's type, which in turn calls visitor.visit(element), which dispatches based on the visitor's type.