The concept of visitors solves a specific problem. It allows you to apply a function to UNION data, and it allows you to have more flexible object-oriented code.
THE PROBLEM:
You have some sort of union data (e.g. Shapes). It can be “one of” many values.
NOTE: I made Square be Rect initially but changed to Square to make it simpler but that made xSide and ySide redundant. Let’s just stick with it :)
Let us try to get the area of each shape.
THE PREVIOUS WAY (DO NOT DO THIS!):
Let’s just have a general getArea() method to return the area! No problem.
What’s the problem here?
Now let me ask you to get the perimeter of a shape. You have to add:
- A new method to the interface
- A new method to each class that implements the interface, so
- A method for Circle
- A method for Square
On a small scale, this doesn’t seem bad.
What if you had an interface with 10 classes implementing it? What if your interface needed to have 50 methods? You can imagine how many methods you’d need to implement. Your code will be increasingly complex because you have no choice but to lump all your logic into one big interface and one big file.
THE SOLUTION:
Keep the interface and all its subclasses stupid simple. Export all the logic to smaller, understandable classes.
This is where the visitor design pattern comes in! But you still have the question: “What is a visitor?????”
A VISITOR IS A FUNCTION.
Like a math function. It takes an X and returns a Y. It takes in a T and returns an R. It takes in an INPUT PARAMETER and returns an OUTPUT PARAMETER. That’s all it is.
Let’s return to the original task: Let’s get the area of each shape. (I reset IShape, Circle, and Square to have no methods).
However, let’s think about it in a different way.
We have a Shape. Let’s create a function that takes in a Shape and returns the area
Function: Shape → area
If we can apply that function onto the Shape, it would look something like:
Function.apply(Shape) → area
Let’s start by creating an IShapeVisitor (which is a function!!!) as a general function template for anything to do with Shapes.
IShapeVisitor: IShape -> whatever you’d like (so generic type, let’s name it T)
Remember: our goal here is to create a function that will get us the area of a shape. Let’s create a ShapeToArea visitor (aka a function!) that takes in an IShape (already covered by IShapeVisitor) and returns a Double (for the area).
Eclipse is angry because we need to implement some method. Why? The hierarchy of classes goes:
IFunc → IShapeVisitor → ShapeToArea
And we have a method, apply() from IFunc, that we haven’t implemented yet! Let’s do that.
Okay, we got the function ShapeToArea. We have the input IShape. How do we get the area?
THE PROBLEM:
Because Shape is a union data type, it can either be a Circle or a Square. We can’t put a general getArea() and do dynamic dispatch, because that’s the opposite of what we’re trying to do. Remember! Our major goal is to export the logic of getting the area to outside of the IShape and into the ShapeToArea function/visitor/class.
- We’re starting in the ShapeToArea. Let’s visit the IShape and ask, “Hey, I don’t know what shape you are. What shape am I visiting right now?”
- The IShape will accept() the visitor and reply, “Hi! You are currently visiting a Circle/Square. Here’s all my data!”
- The ShapeToArea will reply, “Thanks! I’ll do what I need to do with the data you gave me.”
Sounds like you’re going back and forth, right?
Starting from ShapeToArea → Go to IShape → Come back to ShapeToArea
This sounds like a job for…
THE SOLUTION:
Double Dispatch!
1. In order for the ShapeToArea to go over and visit the IShape, the IShape first needs to accept() the visitor. The visitor is here on the task to turn IShape into Double (aka type T). Thus, the IShape needs to assist the visitor on its task and help it return a Double (aka type T).
2. Once the IShape has accepted the visitor, it needs to tell the visitor, “Hi! You are currently visiting a Circle/Square. Here’s all my data!”
3. We haven’t yet created the methods for when a visitor knows what it’s visiting!
Using this double dispatch, we now figured out what data type we’re actually visiting. Now, it’s a matter of writing the logic in this exported, outside ShapeToArea function.
And with the apply() function where we started with an IShape as the input, all we have to do is visit it and let the IShape accept us, the visitor.
And we’re done!
Why is this “so much better”?
Well, let’s write a test first to see how it works in action.
Instead of doing something like this.circle.getArea(), it’s now new ShapeToArea().apply(this.circle). We are applying a function (ShapeToArea: IShape -> Double) onto an IShape.
Now, I present to you a new task:
Task: Get the perimeter of a shape.
PREVIOUSLY (Interface Methods)
Have to add:
- getPerimeter() to the interface
- A new method to each class that implements the interface, so
- getPerimeter() for Circle
- getPerimeter() for Square
NOW (Visitors)
Have to add:
- A new visitor, ShapeToPerimeter
- ……..that’s it!
To add that new functionality and logic, we didn’t even touch the IShape class.
Now that’s awesome.
Mingle Li | li.mingle@northeastern.edu
Written 3/1/2022 to explain visitors in Fundies 2