Java最佳实践:子类中有更详细的类变量

4

我正在使用Java为一些算法建模可绘制的平面图。我的基本类如下:

public class Node {
    private String label;
}

并且

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

这对算法来说效果相当不错。为了绘制图形,我扩展了节点类,加入了位置信息:

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

我的问题是关于绘制边缘的类。我想写出类似以下的代码:

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

但我从未见过这样的设计。而且,编译器需要一个构造函数 public GraphicalNode(Node node0, Node node1) 来继承超类。

有没有人有实现这个的想法?


1
额?我不太明白你在说什么。 - Stultuske
由于你的 GraphicalNode 是一个 Node,所以你可以将它存储在 Edge 的成员中,并添加两个 getter 到 GraphicalEdge 中,当访问 GraphicalNode 时封装转换。 - Andreas Fester
@AndreasFester 如果我在GraphicalEdge中存储一个真正的Node而不是GraphicalNode,那么我就会冒着ClassCastException的风险,不是吗? - Stefan
是的,这就是缺点 - 您需要确保存储正确的对象。您可以使用@wero的基于泛型的方法 - 在那里编译器确保您只将“GraphicalNodes”存储到“GraphicalEdges”中。无论如何:避免子类中的成员重复。 - Andreas Fester
谢谢。我认为Weros的方法在这种情况下是最聪明的。 - Stefan
3个回答

2
您可以使用泛型并将Edge参数化为Node类。
public class Edge<N extends Node> {
    private N node0;
    private N node1;
    public Edge(N n0, N n1) { this.node0 = n0; this.node1 = n1; }
}

GraphicalEdge 然后变成

public class GraphicalEdge extends Edge<GraphicalNode> {
    public GraphicalEdge (GraphicalNode n0, GraphicalNode n1) { super(n0, n1); }
}

注意:这些类层次结构通常变得难以处理。您也可以简单地决定将xy成员放入Node基类中,无需使用层次结构。

谢谢你的想法。这看起来很有趣。我会考虑这个解决方案。我不想将x和y放入节点中,因为如果我只使用图形模型而不是图形视图,我就不需要它们。 - Stefan

1

也许我在这里误解了什么- 下降票会显示- 但是...

这听起来像使用Covariance的教科书范例。

当你有一个类Edge,它具有方法Node getNode(),那么你可以定义一个更具体的返回类型,该类型在extends Edge的类中定义。例如:

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();
}

或者,使用您提供的类,扩展了您提到的构造函数和一些getter方法,组合成一个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;
    }

}

关键点在于:当你拥有一个类型为Edge的引用时,你只能从中获取一个Node(即使该引用所指的对象实际上是一个GraphicalEdge)。只有当引用的类型是GraphicalEdge时,你才能从中获取一个GraphicalNode

这在许多情况下非常方便,并且通常允许干净地分离关注点:当一个方法只需要操作EdgeNode对象,并且不关心它们的图形表示时,你可以使用基类编写其签名:

void computeSomething(Edge edge) {
    Node n0 = edge.getNode();
    Node n1 = edge.getNode();
    ...
}

void run() {
    GraphicialEdge e = new GraphicalEdge(...);

    computeSomething(e);
}

当一个方法确实需要图形表示时,你让它拥有图形优势。
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.
}

一则旁注,或许是关键点,考虑到你的问题特别涉及...

类变量:

根据你所设计的当前结构,每个GraphicalEdge将会存储其节点两次——一次作为GraphicalNode,另一次作为简单的Node。可以通过定义接口来避免这种情况:

interface Node {}
interface Edge { 
    Node getNode();
}

interface GraphicalNode extends Node {}
interface GraphicalEdge extends Edge { 
    @Override
    GraphicalNode getNode();
}

class DefaultEdge implements GraphicalEdge { ... }

使用泛型,如wero在他的回答中建议的那样,可能会增加一些更多的自由度,也许可以实现更清晰和更灵活的设计,以便进一步处理Node类型。例如,您可能希望稍后引入类似于ColoredGraphicalNode之类的东西,而这可以很好地被泛型类型参数所覆盖。但是,这将付出一些更加难以理解的方法签名的代价:编写通用形式的方法,允许“路由”所需的类型信息可能会变得有点麻烦,具体取决于您想要到达哪个程度。

我在另一个论坛上得到了部分相同的答案,我问了这个问题:D 我没有理解协方差的方法。这是我绝对最喜欢的!非常感谢! - Stefan

0
我认为最简单的方法是让你的Node类实现Point接口:
public interface Point {
    double getX();
    double getY();
}

像这样:

public class Node implements Point {
    private String label;
    // ...
}

那么你可以像原来一样拥有一个Edge类:

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

    public List<Node> getNodes() {
    // ...
}

还有一个负责绘制的对象:

public class PointsDrawer {

    public void drawPoints(List<? extends PointPoint> pointsToDraw) {
        // draw logic here
    }
}

你可以像这样使用它:

Edge edge = new Edge();
// initialization here ...
PointsDrawer pd = new PointsDrawer();
pd.drawPoints(edge.getNodes());

使用这个方法,您可以将绘图逻辑与数据结构分离,只需在您的Node类中保存xy坐标即可。您可以通过使用装饰器或Map存储附加的Point数据来进一步解耦。


谢谢。但这个问题与在节点中存储x和y坐标的问题相同。如果我只想使用图形模型,那么坐标就是太多的信息了。 地图实际上也是一个可能的解决方案。我只想尽可能少地使用数据结构。作为类变量,我不需要地图。 - Stefan
你可以像这样将坐标放入一个 Map 中:Map<Node, Point> - Adam Arold

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接