Decoupling Operations from the Data it Operates On…
Firstly you might ask why you would want to do this, and it is a valid question. After all, in the OO paradigm we usually bind data and its operations together. Thus creating fully functional objects. It is often good that an object knows what it can do with its own data and how it can do it. But there are times when you can’t always foresee all the operations that will be performed on an objects data.
Extending an object’s functions
Continually adding new functionality to an object can have several side effects: it can make the class become much bigger and harder to read and it can pollute the class with things it possible doesn’t need to know about or be aware of. Before long, you end up with a big messy object that encapsulates more than it should.
Context
I’m in the middle of building a software metric tool with my research supervisor and my first order of business was to refactor the existing software into something more manageable. We have this saying that it was initially built as me ware. This basically means that it was built for my own purpose and by no means was intended to be used by anybody else. As such, their is no guarantees. This was fine, and a good choice for my supervisor as he was the only person using it since he started it. It suited his purpose fine. It is however now becoming us ware. Could potentially become our ware in the future if more people contribute to it later down the track. But I digress…
My first order of business was to decouple the reporting from the meta-model. These classes were huge, and it meant that it would be hard to add further metric reports down the track. The goal was to decouple the reports as to allow the types of reports that could be generated to grow independently of the meta-model they report on. What’s more, we plan on serializing the meta-model so the smaller the classes the better.
I wanted something that would allow some form of pluggability. That is, provide easy extension for adding new reporting types. This sounded like a good candidate for visitors.
Double dispatch at its best
The great thing about visitors is their use of double-dispatch. The method which gets called not only depends on the type of visitor visiting, but the type of object visited. With this in mind, I thought I would create a new visitor for a new report. Only it’s not quite that simple.
Disadvantages of visitors
Visitors are not very useful when the element structure keeps changing because any new elements added need to be added to each visitor. In this particular case, that wasn’t so much of a problem. The meta-model doesn’t change very often. What is sometimes a problem however, is when a visitor doesn’t always use every visit defined by its interface.
For example, our meta-model wraps classes, methods, versions etc. Each element in this meta-model needs to be visitable, allowing reports to be generated on its data. Or other types of visitors to do things when they visit. The biggest problem I had with the reporting was that not all metric reports can be generated for every layer of the meta-model. This essentially meant that some report visitor’s could visit some parts of the meta-model and not other parts. Thusly, a visitor that cannot visit a particular part of the meta-model would be stuck with empty visit methods. But what do you do when you have to implement a common interface but can only support part of it?
One approach might be to break up the visiting interface, creating one for each layer. This however also means that you then need a visitable interface for each separate visitor. If you had half a dozen elements in your model you were going to visit this would mean a total of twelve interfaces! Six visitors and six visitable’s. This is really starting to take aware from the double dispatch notion of using a single interface. So I decided to just stay with one interface for my implementation.
Handling a less than perfect situation
Rather than implementing this visitor interface (which had half a dozen visit methods defined) in each report. I decided to create an abstract ReportVisitor class. The reason for this is it allows concrete reports to override only the visit’s they support. For the ones they don’t, they would at least be hidden in the super class and not repeated in concrete reports. Furthermore, it allowed for a default message to be displayed if the concrete report never overrided a particular visit method. Now this is a less than perfect situation. But I think this approach at least handles it better.
Assuming we had an object called foo which was supported by our visitor and an object bar, which wasn’t. We might get something like this.
Visitor v = new ReportVisitor();
Foo foo = new Foo();
foo.accept(v); // This works fine.
Bar bar = new Bar();
bar.accept(v); // V does not have an operation to support bar.
Output: This report and provided metric definition does not support reporting on bar.
There were plenty more details and considerations that had to be taken into account, but details that need not be repeated here. The upshot is though, that new reports can easily be generated simply be creating a new visitor that inherits from ReportVisitor. The key here is to simply override the visit methods for the layers that this new report supports. So whilst not perfect, it is much better than what we had and it will allow for easy extension.

Leave a Reply