This is an example of the ideas described in IndependentVisitorPattern, with running code and disciplined refactoring. The goal is to show how the VisitorPattern can be refactored to eliminate circular dependencies. Two refactorings are shown, the first eliminating the dependence of the visitor on the structure, and the second eliminating the dependence of the structure on the visitor. Either refactoring alone would eliminate the circular dependencies; they are orthogonal and complementary. For example, if you want to pass in the whole object to the visitor, but still want to get rid of the circularity, just use the second refactoring alone.
What follows are three versions of the code, each with my notes of the refactoring steps I used along with a class diagram. The first version was built up test-first, while the second and third didn't add any tests because they were just refactorings. The tests aren't included here. In the class diagrams, a solid upward line indicates class inheritance; a dashed sideways arrow indicates some other kind of dependency.
First I built up this sample code test-first. I was pleasantly surprised that going test-first took me gradually through these stages:
+---------+ | Element | - - - - - - - - - - - - . +---------+ . | | V | | +---------+ | +----------+ | Visitor | | | Document | < - - - - - > +---------+ | +----------+ A +-----------+ . | Directory | < - - - - - - - - - - - - - - +-----------+Element.java
public class Element { private String name; public Element(String name) { this.name = name; } public String getName() { return name; } public void accept(Visitor vis) { } }Document.java
public class Document extends Element { public Document(String name) { super(name); } public void accept(Visitor vis) { vis.visitDocument(this); } }Directory.java
import java.util.*; public class Directory extends Element { private ArrayList children = new ArrayList(); public Directory(String name) { super(name); } public void addChild(Element element) { children.add(element); } public int getChildCount() { return children.size(); } public Element getChild(int index) { return (Element) children.get(index); } public void accept(Visitor vis) { vis.enterDirectory(this); for (int i = 0; i < getChildCount(); i++) { getChild(i).accept(vis); } vis.leaveDirectory(); } }Visitor.java
public class Visitor { private String result = ""; private String currentIndent = ""; public void visitDocument(Document doc) { result = result + currentIndent + "[doc] " + doc.getName() + "\n"; } public void enterDirectory(Directory dir) { result = result + currentIndent + "[dir] " + dir.getName() + "\n"; currentIndent = currentIndent + " "; } public void leaveDirectory() { currentIndent = currentIndent.substring(4); } public String getResult() { return result; } }
Next I wanted to do the first of my refactorings, to eliminate the circular dependencies. Here are the steps I took:
+---------+ | Element | - - - - - - - - - - - - . +---------+ . | | V | | +---------+ | +----------+ | Visitor | | | Document | - - - - - - > +---------+ | +----------+ A +-----------+ . | Directory | - - - - - - - - - - - - - - - +-----------+Element.java
public class Element { private String name; public Element(String name) { this.name = name; } public String getName() { return name; } public void accept(Visitor vis) { } }Document.java
public class Document extends Element { public Document(String name) { super(name); } public void accept(Visitor vis) { vis.visitDocument(getName()); } }Directory.java
import java.util.*; public class Directory extends Element { private ArrayList children = new ArrayList(); public Directory(String name) { super(name); } public void addChild(Element element) { children.add(element); } public int getChildCount() { return children.size(); } public Element getChild(int index) { return (Element) children.get(index); } public void accept(Visitor vis) { vis.enterDirectory(getName()); for (int i = 0; i < getChildCount(); i++) { getChild(i).accept(vis); } vis.leaveDirectory(); } }Visitor.java
public class Visitor { private String result = ""; private String currentIndent = ""; public void visitDocument(String docName) { result = result + currentIndent + "[doc] " + docName + "\n"; } public void enterDirectory(String dirName) { result = result + currentIndent + "[dir] " + dirName + "\n"; currentIndent = currentIndent + " "; } public void leaveDirectory() { currentIndent = currentIndent.substring(4); } public String getResult() { return result; } }
Finally I wanted to try the TourGuide refactoring, so that Element et al. wouldn't be dependent on Visitor. Here are the steps I took:
+---------+ | Element | < - - - - - - - . +---------+ . | | . | | +-----------+ +---------+ | +----------+ | TourGuide | - - > | Visitor | | | Document | < - - +-----------+ +---------+ | +----------+ . +-----------+ . | Directory | < - - - - - - - - - - +-----------+Element.java
public class Element { private String name; public Element(String name) { this.name = name; } public String getName() { return name; } // removed accept(Visitor) }Document.java
public class Document extends Element { public Document(String name) { super(name); } // removed accept(Visitor) }Directory.java
import java.util.*; public class Directory extends Element { private ArrayList children = new ArrayList(); public Directory(String name) { super(name); } public void addChild(Element element) { children.add(element); } public int getChildCount() { return children.size(); } public Element getChild(int index) { return (Element) children.get(index); } // removed accept(Visitor) }Visitor.java
public class Visitor { private String result = ""; private String currentIndent = ""; public void visitDocument(String docName) { result = result + currentIndent + "[doc] " + docName + "\n"; } public void enterDirectory(String dirName) { result = result + currentIndent + "[dir] " + dirName + "\n"; currentIndent = currentIndent + " "; } public void leaveDirectory() { currentIndent = currentIndent.substring(4); } public String getResult() { return result; } }TourGuide.java
public class TourGuide { public void traverseDocument(Document doc, Visitor vis) { vis.visitDocument(doc.getName()); } public void traverseDirectory(Directory dir, Visitor vis) { vis.enterDirectory(dir.getName()); for (int i = 0; i < dir.getChildCount(); i++) { traverseElement(dir.getChild(i), vis); } vis.leaveDirectory(); } public void traverseElement(Element elt, Visitor vis) { if (elt instanceof Document) { traverseDocument((Document) elt, vis); } else if (elt instanceof Directory) { traverseDirectory((Directory) elt, vis); } } }