I am trying to create an application in Java which allows for generation of large Provenance graphs from small seed graphs but I am having a little trouble figuring out the best way to design my classes.
To begin with, Provenance essentially has a graph structure, nodes and edges. I have created a Java library which acts as a mapping of the Provenance Data Model to Java Objects. This allows me to abstract my application specific information from Provenance model.
My class structure looks a little like this:
- Graph (containing Sets of Node and Edge)
- abstract Node (Just a String name for now)
- Agent
- Activity
- Entity
- Other subclasses of Node
- abstract Edge
- Generation
- Association
- Other subclasses of Edge
Now, what I would like to do is provide weighting on the nodes and edges which act as multipliers/saturation levels. There are a couple of ways I could use the library to achieve this aim but I’m not clear on what is the best from a development and maintainability perspective.
Firstly, I defined a Weighable interface with some simple methods such as get and set minimum and maximum weights.
Now, I could either extend each subclass Node e.g.
class WeighableAgent extends Agent implements Weighable
But then this requires an extended class for every type of node available, and required me to implement the Weighable interface at every level.
Alternatively, I could provide a mixin but it would still rely on me implementing the same functionality across each subclass.
Alternatively I could have a class that composes upon Agent, but that still requires me to implement Weighable across every composition class.
Alternatively, I could compose solely on the Node class e.g.
class WeighableNode implements Weighable {
private Node node;
public WeighableNode(Node node) {
this.node = node;
}
etc etc...
And this would allow me to only implement Weighable in one place. However, then I lose some important information about the concrete class type from anything using WeighableNode and the only way around this that I see is to provide a method:
Node getNode();
Which I’m concerned about due to the Law Of Demeter. Furthermore, this WeighableNode will be composed upon by a PresentationNode, to help with Swing which would mean I would end up chaining calls such as:
presentationNode.getWeighableNode().getNode() instanceof Agent
Which seems very unpleasant.
One final solution which I just thought of is to compose upon Node as above and then extend WeighableNode with WeighableAgent, etc etc. This means I do not need to reimplement Weighable each time and if I know I have
instanceof WeighableAgent
then the wrapped node is an Agent.
I appreciate this is quite long but I hope that an experienced developer will very quickly see the correct design practice.
Too bad Java has neither real mixins nor multiple inheritance…
Generic wrapper
I guess I’d use
WeighableNode<NodeType extends Node>as a generic wrapper type. That way, all places which require nodes of a specific type could clearly state that fact. ItsgetNodemethod could return the correct class. And you wouldn’t end up with too many classes all over the place.Conversion idiom
Even if you don’t use the above approach, the following idiom might be interesting:
You could change the above to return
nullinstead of throwing an exception if you prefer. The idea is that the calling code doesn’t have to worry about how to convert your node to various types. You’d e.g. simply writeThis separates implementation from interface, giving you much freedom to change things later on if the need arises. The basic
Nodeimplementation would know how to convert itself, taking care of derived classes. TheNodeWrapperwould add conversion using composition. You could use that as the base class forWeighableNodeand yourpresentationNode.Fat interface
You could implement the weighing stuff in
Nodeitself, and useWeighedNodejust as a tagging interface, or not at all. Not the nicest solution, but will help keep your number of classes down, and shouldn’t do any real harm except for the bit of memory consumed by the extra data. The levels of indirection required by the other schemes will probably outweight that memory requirement by far.