I've implemented a basic graph class (not as in "plotting" but as in "network"!), that's to be used for basic graph theoretical tasks. (see summarised header file snippets below)
In addition to the generic graph functionality it also implements functionality for node positioning in 3D space. And this extended 3D functionality I'd like to isolate into a subclass, resulting in:
- light-weight generic classes
(MyGenericGraph, MyGenericGraphNode, MyGenericGraphEdge)
- heavier-weight specialized subclasses
(My3DGraph, My3DGraphNode, My3DGraphEdge)
So far so good, in theory, that is.
Problem:
I'd have to ensure (and preferably at compile time) that one cannot add generic MyGenericGraphNodes
to the spezialized My3DGraph
, as it highly depends on the added 3D logic inside My3DGraphNode
. (While the generic MyGenericGraph
would simply not care.)
The core issue is as simple as this: I cannot override these methods from MyGenericGraph:
- (void)addNode:(MyGenericGraphNode *)aNode;
- (void)removeNode:(MyGenericGraphNode *)aNode;
with these methods in my subclass My3DGraph:
- (void)addNode:(My3DGraphNode *)aNode;
- (void)removeNode:(My3DGraphNode *)aNode;
I've so far come up with three possible solutions but before going for any of them I'd like to hear some opinions on them. (and hopefully spare me some unforeseen troubles on my way)
I'm wondering if there's another and superior solution or design pattern to this that I'm missing? Or if not: which of my solutions would you go for?
I'd love to hear your opinion on this.
Possible solution 1
- Adding an abstract class
MyAbstractGraph
that would basically be identical to the generic parts of my current implementation ofMyGenericGraph
(see below), but would lack any node-addition/removal methods.MyGenericGraph
andMy3DGraph
would then simply be subclasses ofMyAbstractGraph
. And whileMyGenericGraph
would only implement the lacking node-addition/removal methods,My3DGraph
would further more implement all 3D space functionality. Both requiring their respective node class types. (same forMyGenericGraphNode
andMyGenericGraphEdge
and their 3D counterparts)
Problems with this solution: It would add a significant amount of complexity to an otherwise rather simple problem.
Further more as My3DGraph
should be able to deal with My3DGraphNodes
AND MyGenericGraphNodes
, I'd have to implement MyGenericGraph
's method as:
- (void)addNode:(MyAbstractGraphNode *)aNode;`
but My3DGraph
's method as:
- (void)addNode:(My3DGraphNode *)aNode;
as well, as otherwise my generic graph it wouldn't accept 3d nodes. This would expose the abstract class unnecessarily though.
Possible solution 2
True and simple subclasses + moving MyGenericGraphNode/My3DGraphNode
allocation right into MyGenericGraph/My3DGraph
, to get something like: - (MyGenericGraphNode *)newNode;
, which would allocate and return a node of correct type and add it to the graph right away. One would then get rid of - (void)addNode:(MyGenericGraphNode *)aNode;
entirely, leaving no chance to add nodes than from within the Graph itself (hence assuring proper class membership).
Problems with this solution: While it would not add any noteworthy complexity to the classes, it would on the other hand basically get me into the same predicament again, as soon as I—let's say—wanted to add functionality to my My3DGraph
for moving a node from one graph to another. And imho a class should be able to deal with an object no matter who created it and why.
Possible solution 3
True and simple subclasses + adding specialized method for 3D nodes and disabling generic method, like so:
- (void)addNode:(MyGenericGraphNode *)aNode {
[self doesNotRecognizeSelector:_cmd];
}
- (void)add3DNode:(My3DGraphNode *)aNode {
//bla
}
Problems with this solution: The generic [a3DGraph addNode:aNode]
method would still show up in Xcode's auto-complete, pass silently on compile, but unexpectedly throw exception on run. Frequent headaches foreseen.
Possible solution 4
True and simple subclasses for my graph and just the generic classes for node and edge, but with an additional ivar pointer My3DUnit *dimensionalUnit:
in node class (defaulting to nil for MyGraph) that implements all the logic and properties and provides 3D functionality to the node class. My3DUnit
can simply be silently created (with position (0,0,0) e.g.) and attached to generic nodes in case they get added to a 3d graph, and hence made compatible. Vice versa if a node with a DL3DUnit
gets added to a generic graph it simply keeps it attached and adds the node.
Header Files
Here are the (shortened) headers of my classes:
@interface MyGraph : NSObject {
// properties:
NSMutableSet *nodes;
//...
//extended 3D properties:
double gravityStrength;
//...
}
// functionality:
- (void)addNode:(MyGraphNode *)aNode;
- (void)removeNode:(MyGraphNode *)aNode;
//...
//extended 3D functionality:
- (double)kineticEnergy;
//...
@end
@interface MyGraphNode : NSObject {
// properties:
MyGraph *graph;
NSMutableSet *edges;
//...
//extended 3D properties:
My3DVector position;
//...
}
// properties:
@property (nonatomic, readonly) MyGraph *graph;
@property (nonatomic, readonly) NSSet *edges;
@property (nonatomic, readonly) NSSet *neighbors;
@property (nonatomic, readonly) NSUInteger degree;
//...
//extended 3D properties
@property (nonatomic, assign) My3DVector position;
//...
// functionality:
- (void)attachToGraph:(MyGraph *)aGraph;
- (void)detachFromGraph;
- (void)addNeighbor:(MyGraphNode *)aNode;
- (void)removeNeighbor:(MyGraphNode *)aNode;
- (BOOL)hasNeighbor:(MyGraphNode *)aNode;
- (NSSet *)neighbors;
- (NSUInteger)degree;
//...
//extended 3D functionality:
- (double)distanceToNode:(DLGraphNode *)aNode;
//...
@end
@interface MyGraphEdge : NSObject {
// properties:
MyGraphNode *predecessor;
MyGraphNode *successor;
//...
}
// properties:
@property (nonatomic, readonly) MyGraphNode *predecessor;
@property (nonatomic, readonly) MyGraphNode *successor;
//...
// functionality:
- (id)initWithPredecessorNode:(MyGraphNode *)predecessorNode successorNode:(MyGraphNode *)successorNode;
+ (MyGraphEdge *)edgeWithPredecessorNode:(MyGraphNode *)predecessorNode successorNode:(MyGraphNode *)successorNode;
- (BOOL)hasNeighbor:(MyGraphNode *)aNode;
- (BOOL)hasSuccessor:(MyGraphNode *)aNode;
- (BOOL)hasPredecessor:(MyGraphNode *)aNode;
@end
This is basically how my graph is implemented right now. There's obviously quite a bunch more to it, but you should get the idea.
(As you might have noticed MyGenericGraphEdge currently implements no 3D space functionality, but it might in the future, like computing its center point, e.g., hence I've included it here.)
[Edit: Added solution 4 inspired by ughoavgfhw; fixed a mistake in solution 1, sorry for that :(]