Suppose you want to navigate an object composite to produce a corresponding XML document. The problem is best solved recursively: Transform the root object’s primitive fields into simple elements; then, navigate into each non-primitive field and similarly transform it.
Your best tactic is to split the problem into two: navigation of the object hierarchy, and transformation of Java elements into corresponding XML elements. Navigating the object hierarchy involves use of the Java reflection API. Producing the XML involves simple text manipulation. To adhere to the single responsibility principle (SRP), your solution thus requires two separate classes: a navigator class and a transformer class.
The visitor pattern provides one conforming solution, and also presents opportunities for reuse. The pattern refers to something that will be navigated, or visited, as an object structure. In the XML example, the object structure is simply your Java object structure as accessed via reflection. The visitor contains logic to execute upon visiting each pertinent element in the object structure. In the XML example, the visitor is the transform logic.
Basically, the visitor pattern is organized as a callback structure. The object structure accepts a reference to a visitor interface. Your XML transformer is one implementation of the visitor interface, but you could supply alternate implementations for different needs. As logic in the object structure navigates through the Java object hierarchy, it calls back to a method (conventionally named visit) on the visitor object.
Listing 1 presents a visitor interface for the XML example. Not only does it represent the callbacks for visiting an object, but also for departure and a visit to each primitive attribute in a Java object. (To keep the example simple, I omit logic for iterating collections.)
Listing 1: A visitor interface.
import util.*; public interface Visitor { void visit(HierarchyObject instance); void visitPrimitiveAttribute(HierarchyObject instance, HierarchyField attribute); void depart(HierarchyObject instance); }
The HierarchyWalker class (see Listing 2) provides the high-level policy for navigating Java objects. This is the object structure representation for the visitor pattern. The walk method takes the object to be navigated and the visitor callback. The supporting classes, HierarchyObject (see Listing 3) and HierarchyField (see Listing 4), encapsulate the mildly gory details of Java reflection.
First things first. The walker visits each instance:
visitor.visit(instance);
The walk method then iterates through each primitive attribute on the passed object:
for (HierarchyField attribute: instance.primitiveAttributes()) visitor.visitPrimitiveAttribute(instance, attribute);
The code then walks through each non-primitive, recursively calling the walk method, for each:
for (HierarchyField attribute: instance.nonPrimitiveAttributes()) walk(attribute.getValue(object), visitor);
Finally, the walk method calls the depart method on the visitor.
visitor.depart(instance);
Listing 2: HierarchyWalker.
import util.*; public class HierarchyWalker { public void walk(Object object, Visitor visitor) { HierarchyObject instance = new HierarchyObject(object); visitor.visit(instance); for (HierarchyField attribute: instance.primitiveAttributes()) visitor.visitPrimitiveAttribute(instance, attribute); for (HierarchyField attribute: instance.nonPrimitiveAttributes()) walk(attribute.getValue(object), visitor); visitor.depart(instance); } }
Listing 3: HierarchyObject.
import java.lang.reflect.*; import java.util.*; public class HierarchyObject { private Object object; public HierarchyObject(Object object) { this.object = object; } public String typeName() { return object.getClass().getSimpleName(); } public Collection<HierarchyField> primitiveAttributes() { Collection<HierarchyField> attributes = new ArrayList<HierarchyField>(); for (Field field: attributeFields()) if (isPrimitiveOrString(field)) attributes.add(new HierarchyField(field)); return attributes; } public Collection<HierarchyField> nonPrimitiveAttributes() { Collection<HierarchyField> attributes = new ArrayList<HierarchyField>(); for (Field field: attributeFields()) if (!isPrimitiveOrString(field)) attributes.add(new HierarchyField(field)); return attributes; } private Field[] attributeFields() { return object.getClass().getDeclaredFields(); } public Object getObject() { return object; } private boolean isPrimitiveOrString(Field field) { return field.getType().isPrimitive() || field.getType() == String.class; } public String valueString(HierarchyField attribute) { return attribute.getValue(object).toString(); } }
Listing 4: HierarchyField.
package util; import java.lang.reflect.*; public class HierarchyField { private Field field; public HierarchyField(Field field) { this.field = field; field.setAccessible(true); } public String name() { return field.getName(); } public Object getValue(Object object) { try { return field.get(object); } catch (IllegalAccessException e) { throw new RuntimeException(); } } @Override public boolean equals(Object object) { if (object == null || !(object instanceof HierarchyField)) return false; HierarchyField that = (HierarchyField)object; return that.field.getName().equals(this.field.getName()); } public Class<?> type() { return field.getType(); } }
It’s up to the visitor implementation to decide what to do with each of these callbacks. Listing 5 shows an implementation for the XML transformer. The XmlBuilder class implements the Visitor interface, providing appropriate logic for each of the visit, depart, and visitPrimitiveAttribute methods.
Listing 5: Implenting the XML transformer.
import util.*; import visitor.*; public class XmlBuilder implements Visitor { private static final String EOL = System.getProperty("line.separator"); private static final String PAD = " "; private int level = 0; private StringBuilder builder; public XmlBuilder(StringBuilder builder) { this.builder = builder; } @Override public void visit(HierarchyObject instance) { indent(++level); appendLine(beginTag(instance.typeName())); } @Override public void depart(HierarchyObject instance) { indent(level); appendLine(endTag(instance.typeName())); --level; } @Override public void visitPrimitiveAttribute(HierarchyObject instance, HierarchyField attribute) { indent(level + 1); appendLine(beginTag(attribute.name()) + instance.valueString(attribute) + endTag(attribute.name())); } private void indent(int levels) { for (int i = 0; i < levels; i++) append(PAD); } private String beginTag(String text) { return "<" + text + ">"; } private String endTag(String text) { return "</" + text + ">"; } private void appendLine(String text) { builder.append(text + EOL); } private void append(String text) { builder.append(text); } }
The main points? XmlBuilder isn’t fettered with the navigation and reflection logic, making it very easy to comprehend. And, HierarchyWalker supports the ability to do something other than produce an XML document.
Visitor does present challenges, particularly as things change. Several variants of the visitor pattern exist to try and circumvent these issues. But as complex as visitor can be, the heart of the pattern is about simplifying things through use of the SRP.
Figure 1: Visitor
About the Author
Jeff Langr is a veteran software developer with over a quarter century of professional software development experience. He’s written two books, including Agile Java: Crafting Code With Test-Driven Development (Prentice Hall) in 2005. Jeff is contributing a chapter to Uncle Bob Martin’s upcoming book, Clean Code. Jeff has written over 50 published articles on software development, with more than a couple dozen appearing at Developer.com You can find out more about Jeff at his site, http://langrsoft.com, or you can contact him via email at jeff at langrsoft dot com.