Search code examples
javagraphdrawable

Java best practice: More detailed class variable in subclass


i'm modelling a drawable planar graph in java for some algorithms. My basic classes are:

public class Node {
    private String label;
}

and

public class Edge {
    private Node node0;
    private Node node1;
}

This works pretty good for the algorithms. For drawing the graph, i extended the node-class with positions:

public class GraphicalNode extends Node {
    private int x;
    private int y;
}

My problem is the class for drawable edges. I'd like to write something like this:

public class GraphicalEdge extends Edge {
    private GraphicalNode node0;
    private GraphicalNode node1;
}

But i've never seen a design like this before. And on top of that, the compiler need a constructor public GraphicalNode(Node node0, Node node1) for the superclass.

Does anyone have an idea to realize that?


Solution

  • Maybe I misunderstood something here - the downvotes will show - but...

    This sounds like a textbook example for using Covariance.

    When you have a class Edge that has a method Node getNode(), then you can define a more specific return type for this method, in a class that extends Edge. For example:

    class Node {}
    class Edge {
        Node getNode();
    }
    
    class GraphicalNode extends Node {}
    class GraphicalEdge extends Edge {
        // This really overrides the method, with a more specific return type!
        @Override
        GraphicalNode getNode();
    }
    

    Or, using the classes that you provided, extended with the constructors that you mentioned, and some getters, assembled into an MCVE:

    public class WhatIsCovariance
    {
        public static void main(String[] args)
        {
            Node n0 = new Node();
            Node n1 = new Node();
            Edge e0 = new Edge(n0, n1);
    
            Node n = e0.getNode0(); // Works
    
    
            GraphicalNode gn0 = new GraphicalNode();
            GraphicalNode gn1 = new GraphicalNode();
            GraphicalEdge ge0 = new GraphicalEdge(gn0, gn1);
    
            GraphicalNode gn = ge0.getNode0(); // Works
        }
    }
    
    class Node
    {
        private String label;
    }
    
    class Edge
    {
        private Node node0;
        private Node node1;
    
        Edge(Node node0, Node node1)
        {
            this.node0 = node0;
            this.node1 = node1;
        }
    
        public Node getNode0()
        {
            return node0;
        }
    
        public Node getNode1()
        {
            return node1;
        }
    }
    
    class GraphicalNode extends Node
    {
        private int x;
        private int y;
    }
    
    class GraphicalEdge extends Edge
    {
        private GraphicalNode node0;
        private GraphicalNode node1;
    
        GraphicalEdge(GraphicalNode node0, GraphicalNode node1)
        {
            super(node0, node1);
    
            this.node0 = node0;
            this.node1 = node1;
        }
    
        @Override
        public GraphicalNode getNode0()
        {
            return node0;
        }
    
        @Override
        public GraphicalNode getNode1()
        {
            return node1;
        }
    
    }
    

    The key point here is: When you have a reference of the type Edge, then you can only obtain a Node from it (even if the object that the reference refers to actually is a GraphicalEdge). Only when the type of the reference is a GraphicalEdge, you can also obtain a GraphicalNode from it.

    This is handy in many contexts, and often allows a clean separation of concerns: When a method only has to operate on Edge and Node objects, and is not interested in their graphical representation, then you can write its signature using the base classes:

    void computeSomething(Edge edge) {
        Node n0 = edge.getNode();
        Node n1 = edge.getNode();
        ...
    }
    
    void run() {
        GraphicialEdge e = new GraphicalEdge(...);
    
        computeSomething(e);
    }
    

    When a method really needs the graphical representation, you let it take the graphical edge:

    void drawSomething(GraphicalEdge edge) {
        GraphicalNode n0 = edge.getNode();
        GraphicalNode n1 = edge.getNode();
        ...
    }
    
    void run() {
        GraphicialEdge e = new GraphicalEdge(...);
    
        computeSomething(e); // Works
        drawSomething(e); // Works as well
    
        Edge edge = e;
        drawSomething(edge); // Does not work. A GraphicalEdge is required.
    }
    

    A side note, or maybe the key point, considering that your question was particularly about...

    the class variables:

    With the current design that you sketched there, each GraphicalEdge will store its nodes twice - once as a GraphicalNode, and once as a simple Node. This could be avoided, for example, by defining things as interfaces:

    interface Node {}
    interface Edge { 
        Node getNode();
    }
    
    interface GraphicalNode extends Node {}
    interface GraphicalEdge extends Edge { 
        @Override
        GraphicalNode getNode();
    }
    
    class DefaultEdge implements GraphicalEdge { ... }
    

    Using generics, as wero suggested in his answer, may add some more degrees of freedom, and maybe an even cleaner and more flexible design, in view of further Node types. For example, you might later want to introduce something like a ColoredGraphicalNode, and this could nicely be covered by the generic type parameter. But it comes at the cost of somewhat more cryptic method signatures: Writing the methods in the generic form, that allows "routing through" the required type information can become a bit cumbersome, depending on how far you want to go there.