02.14
This is a really nice and simple pattern. Imagine having a collection of objects. Let’s have a class Feature with single method String getName(). Of course the class hierarchy grows in time and here come two additional features: BooleanFeature and StringFeature. They override the original getName() behaviour.
And it looks like that:
class Feature {
public String getName() {
return "Feature";
}
}
class BooleanFeature extends Feature{
@Override
public String getName() {
return "BooleanFeature";
}
}
class StringFeature extends Feature{
@Override
public String getName() {
return "StringFeature";
}
}
That’s a common hierarchy. I’m sure you’ve seen bigger trees.
So, you get a list of Feature objects and you know that some of them are BooleanFeatures, others are StringFeatures. Something similar to:
List<Feature> features = new ArrayList<Feature>() {{
add(new Feature());
add(new BooleanFeature());
add(new StringFeature());
}};
Nothing fancy. Three elements, each of different type. Now, you’re to process each element in different way. It’s java, it can’t be difficult. The processing is the printIt(...) method, which prints the type it processes and the name returned by getName().
class Printer {
public void printIt(Feature feature) {
System.out.println("Printing Feature: "+feature.getName());
}
public void printIt(BooleanFeature feature) {
System.out.println("Printing BooleanFeature: "+feature.getName());
}
public void printIt(StringFeature feature) {
System.out.println("Printing StringFeature: "+feature.getName());
}
}
public class PrintNames {
public static void main(String[] args) {
List<Feature> features = new ArrayList<Feature>() {{
add(new Feature());
add(new BooleanFeature());
add(new StringFeature());
}};
Printer printer = new Printer();
for(Feature feature : features) {
printer.printIt(feature);
}
}
}
Logic is simple, but does it produce what we expect? The expected output looks like this:
Printing Feature: Feature Printing BooleanFeature: BooleanFeature Printing StringFeature: StringFeature
and actual result:
Printing Feature: Feature Printing Feature: BooleanFeature Printing Feature: StringFeature
WHOAAA, the override worked fine, but what happened to the printed types? It looks like the printIt(Feature feature) has been called for each element. What about the other two methods?
If you have been playing java for some time, you know why that happened. Java resolves types at compile time. It means that it sees a list of Feature elements. During compilation, Feature is the only known type kept in the list, so the printIt(Feature feature) method is being used. Advantage of this is faster execution, as there are no type checks during runtime. Above you can see a disadvantage of compile-time type resolution.
Here starts usage of instanceof operator. It hides in many forms. You can create a single method composed of consecutive if-elses and mentioned instanceof checking or create some external class hierarchy that processes Feature in per-subclass manner (using instanceof
), etc.
There’s one solution, which doesn’t introduce the usage of instanceof operator. It’s the Visitor Pattern. How does it work? Well, the common ancestor (Feature in this case) and all its children have to implement function void accept(Visitor visitor>. The implementation is a one-liner: visitor.visit(this). And it’s the same for each class.
class Feature {
public String getName() {
return "Feature";
}
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
class BooleanFeature extends Feature{
@Override
public String getName() {
return "BooleanFeature";
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
class StringFeature extends Feature {
@Override
public String getName() {
return "StringFeature";
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
The Visitor interface has three visit methods – one for each type it can visit.
interface Visitor {
void visit(Feature feature);
void visit(BooleanFeature feature);
void visit(StringFeature feature);
}
All magic is behind this. The accept(Visitor visitor) implementation in each ...Feature calls the visitor.visit(this) method. During compilation, the type of this reference is known in each element, so the compiler can link it to correct method in the Visitor class.
The Printer class just implements the Visitor interface.
class Printer implements Visitor {
public void visit(Feature feature) {
System.out.println("Printing Feature: " + feature.getName());
}
public void visit(BooleanFeature feature) {
System.out.println("Printing BooleanFeature: " + feature.getName());
}
public void visit(StringFeature feature) {
System.out.println("Printing StringFeature: " + feature.getName());
}
}
public class PrintNames {
public static void main(String[] args) {
List<Feature> features = new ArrayList<Feature>() {{
add(new Feature());
add(new BooleanFeature());
add(new StringFeature());
}};
Printer printer = new Printer();
for(Feature feature : features) {
feature.accept(printer);
}
}
}
And the output is as expected:
Printing Feature: Feature Printing BooleanFeature: BooleanFeature Printing StringFeature: StringFeature
What’s worth noticing is that in opposite to previous calls, now the feature accepts the printer object.
Thanks to the Visitor Pattern, you can extend your classes with additional features. The main disadvantage is that any changes in class hierarchy (eg. introducing a class IntegerFeature), result in adding new methods to Visitor interface and all classes that implement it. But you can always create a default/abstract implementation: DefaultVisitor that will be a father for all implementations.
Think of it next time you start processing a collection using the instanceof checking.
No Comment.
Add Your Comment