blob: 0b6e9a500ded4694aa38d2ede2a81a35e98c73a7 [file] [log] [blame]
//////////////////////////////////////////
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
//////////////////////////////////////////
= Visitor Pattern
The https://en.wikipedia.org/wiki/Visitor_pattern[Visitor Pattern] is one of those well-known but not
often used patterns. Perhaps this is because it seems a little complex at first.
But once you become familiar with it, it becomes a powerful way to evolve your code
and as we'll see, Groovy provides ways to reduce some to the complexity, so there is
no reason not to consider using this pattern.
The goal of the pattern is to separate an algorithm from an object structure.
A practical result of this separation is the ability to add new operations to existing
object structures without modifying those structures.
== Simple Example
This example considers how to calculate the bounds of shapes (or collections of shapes).
Our first attempt uses the traditional visitor pattern.
We will see a more Groovy way to do this shortly.
[source,groovy]
----
include::../test/DesignPatternsTest.groovy[tags=visitor_simple_example,indent=0]
----
That took quite a bit of code, but the idea now is that we could add
further algorithms just by adding new visitors with our shape classes remaining
unchanged, e.g. we could add a total area visitor or a collision detection visitor.
We can improve the clarity of our code (and shrink it to about half the size) by
making use of Groovy Closures as follows:
[source,groovy]
----
include::../test/DesignPatternsTest.groovy[tags=visitor_simple_example2,indent=0]
----
Or, using lambdas as follows:
[source,groovy]
----
include::../test/DesignPatternsTest.groovy[tags=visitor_simple_example3,indent=0]
----
== Advanced Example
Let's consider another example to illustrate some more points about this pattern.
[source,groovy]
----
include::../test/DesignPatternsTest.groovy[tags=visitor_advanced_example,indent=0]
----
If we now use `NodeType1Counter` on a tree like this:
[source,groovy]
----
include::../test/DesignPatternsTest.groovy[tags=visitor_advanced_example2,indent=0]
----
Then we have one `NodeType1` object as root and one of the children is also a `NodeType1` instance.
The other child is a `NodeType2` instance.
That means using `NodeType1Counter` here should count 2 `NodeType1` objects as the last statement verifies.
=== When to use this
This example illustrates some of the advantages of the visitor pattern.
For example, while our visitor has state (the count of `NodeType1` objects), the tree of objects itself is not changed.
Similarly, if we wanted to have a visitor counting all node types,
or one that counts how many different types are used, or one that gathers information
using methods special to the node types, again, the visitor alone is all that would need to be written.
=== What happens if we add a new type?
In this case we might have a fair bit of work to do. We probably have to change the `Visitor` interface to accept the new type,
and change potentially most existing visitors based on that interface change,
and we have to write the new type itself.
A better approach is to write a default implementation of the visitor which all concrete visitors will extend.
We'll see this approach in use shortly.
=== What if we want to have different iteration patterns?
Then you have a problem.
Since the node describes how to iterate, you have no influence and stop iteration at a point or change the order.
So maybe we should change this a little to this:
[source,groovy]
----
include::../test/DesignPatternsTest.groovy[tags=visitor_advanced_example3,indent=0]
----
Some small changes but with big effect.
The visitor is now recursive and tells me how to iterate.
The implementation in the Nodes is minimized to `visitor.visit(this)`, `DefaultVisitor` is now able to catch the new types, we can stop iteration by not delegating to super.
Of course the big disadvantage now is that it is no longer iterative, but you can't get all the benefits.
=== Make it Groovy
The question now is how to make that a bit more Groovy.
Didn't you find this `visitor.visit(this)` strange? Why is it there?
The answer is to simulate double dispatch.
In Java, the compile time type is used, so for `visitor.visit(children[i])` the compiler won't be
able to find the correct method, because `Visitor` does not contain a method `visit(Visitable)`.
And even if it would, we would like to visit the more special methods with `NodeType1` or `NodeType2`.
Now Groovy is not using the static type, Groovy uses the runtime type.
This means we can use `visitor.visit(children[i])` without any problem.
Since we minimized the accept method to just do the double dispatch part and
since the runtime type system of Groovy will already cover that, do we need the accept method?
Not really, but we can do even more.
We had the disadvantage of not knowing how to handle unknown tree elements.
We had to _extend_ the interface `Visitor` for that, resulting in changes to `DefaultVisitor` and
then we have the task to provide a useful default like iterating the node or not doing anything at all.
Now with Groovy we can catch that case by adding a `visit(Visitable)` method that does nothing.
That would be the same in Java btw.
But don't let us stop here. Do we need the `Visitor` interface?
If we don't have the accept method, then we don't need the `Visitor` interface at all.
So the new code would be:
[source,groovy]
----
include::../test/DesignPatternsTest.groovy[tags=visitor_advanced_example4,indent=0]
----
Looks like we saved a few lines of code here, but we made more.
The `Visitable` nodes now do not refer to any `Visitor` class or interface.
This is about the best level of separation you might expect here, but we can go further.
Let's change the `Visitable` interface a little and let it return the children we want to visit next.
This allows us a general iteration method.
[source,groovy]
----
include::../test/DesignPatternsTest.groovy[tags=visitor_advanced_example5,indent=0]
----
`DefaultVisitor` now looks a bit different.
It has a `doIteration` method that will get the children it should iterate over and then call visit on each element.
Per default this will call `visit(Visitable)` which then iterates over the children of this child.
`Visitable` has also changed to ensure that any node will be able to return children (even if empty).
We didn't have to change the `NodeType1` and `NodeType2` class, because the way the children field was
defined already made them a property, which means Groovy is so nice to generate a get method for us.
Now the really interesting part is `NodeType1Counter`, it is interesting because we have not changed it.
`super.visit(n1)` will now call `visit(Visitable)` which will call `doIteration` which will start the next level of iteration.
So no change. But `visit(it)` will call `visit(NodeType1)` if it is of type `NodeType1`.
In fact, we don't need the `doIteration` method, we could do that in `visit(Visitable)` too,
but this variant has some benefits. It allows us to write a new `Visitor` that overwrites visit(`Visitable`)
for error cases which of course means we must not do `super.visit(n1)` but `doIteration(n1)`.
=== Summary
In the end we got ~40% less code, a robust and stable architecture,
and we completely removed the Visitor from the Visitable.
To achieve the same in Java, you would probably need to resort to reflection.
The visitor pattern has sometimes been described as a poor fit for extreme programming
techniques because you need to make changes to so many classes all the time.
With our design, if we add new types we don't need to change anything.
So, the pattern is a good fit for agile approaches when using Groovy.
There are variants of the Visitor pattern, like the https://java-design-patterns.com/patterns/acyclic-visitor/[acyclic visitor pattern],
that try to solve the problem of adding new node types with special visitors.
The implementations of these visitors have their own code smells, like using casts, overuse of `instanceof`, and other tricks.
What's more the problems such approaches are trying to solve don't occur within the Groovy version. We recommend avoiding that variant of this pattern.
Finally, in case it isn't obvious, `NodeType1Counter` could be implemented in Java as well.
Groovy will recognize the visit methods and call them as needed because `DefaultVisitor` is still Groovy and does all the magic.
== Further Information
- http://se.ethz.ch/~meyer/publications/computer/visitor.pdf[Componentization: the Visitor example]