如何使用双重调度来分析图形基元的交集?

时间:2013-10-16 10:08:15

标签: java graphics double-dispatch

我正在分析图形基元(矩形,直线,圆形等)的相互作用,并计算重叠,相对方向,合并等。这被引用作为双重调度的主要示例(例如Wikipedia

  

自适应碰撞算法通常需要在它们之间发生碰撞   不同的对象以不同的方式处理。一个典型的例子是   在游戏环境中,宇宙飞船和飞船之间的碰撞   小行星的计算方式与a之间的碰撞不同   太空飞船和太空站。1

但是我还没有理解主要的解释,我也一般不理解SO的答案。

我当前的代码(Java)使用超类Shape,如下所示:

for (int i = 0; i < shapes.size() - 1; i++) {
    for (int j = i + 1; j < shapes.size(); j++) {
        Shape shape = shapes.get(i).intersectionWith(shapes.get(j));
    }
}

在子类(此处为Rect)中具体实现,例如

public class Rect extends Shape {

    public Shape intersectionWith(Shape shape) {
        if (shape instanceof Rect) {
            return this.getCommonBoundingBox((Rect)shape);
        } else if (shape instanceof Line) {
            return this.intersection((Line)shape);
        } else if (shape instanceof Text) {
            return this.intersection((Text) shape);
        }
    }
}

无论如何我必须编写所有n*(n-1)/2方法(并且已经这样做了)。我还必须有可扩展的代码以便在以后适应(比如说):

        } else if (shape instanceof Circle) {
            return this.intersection((Circle)shape);

我没有看到双重调度模式的使用方法或价值,并且会欣赏使用Java图形基元或类似伪代码的具体示例。

更新:我接受了@Flavio(我认为)它回答了提出的确切问题。但是我实际上已经实现了@Slanec,因为它解决了我的问题,而且(对我而言)更简单,更容易阅读。我有一个附属问题“解决方案取决于对称关系吗?”。

“A交叉B”通常与“B交叉A”相同,但“A与B碰撞”并不总是与“B与A碰撞”相同。 (A ==汽车,B ==骑车人)。可以想象我的交叉点在未来可能不对称(例如,“Rect partial obscures Circle”不对称且可能具有不同的语义。

@Flavio很好地解决了维护问题,并指出编译器可以检查问题。 @Slanec通过反射来做到这一点,看起来好像它是一个有用的维护辅助工具 - 我不知道它的性能是什么。

3 个答案:

答案 0 :(得分:1)

我认为会是这样的:

import java.util.ArrayList;
import java.util.List;


public class DoubleDispatchTest {


    public static void main(String[] args) {
        List<Shape> shapes = new ArrayList<Shape>();
        shapes.add(new Line());
        shapes.add(new Circle());
        shapes.add(new Rect());

        for (int i = 0; i < shapes.size() - 1; i++) {
            for (int j = i + 1; j < shapes.size(); j++) {
                Shape shape = shapes.get(i).intersection(shapes.get(j));
            }
        }

    }

    abstract static class Shape {
        abstract Shape intersection(Shape shape);
        abstract Shape intersection(Line line);
        abstract Shape intersection(Circle line);
        abstract Shape intersection(Rect line);
    }

    static class Line extends Shape {
        Shape intersection(Shape shape) {
            return shape.intersection(this);
        }

        Shape intersection(Line line) {
            System.out.println("line + line");
            return null;
        }

        Shape intersection(Circle circle) {
            System.out.println("line + circle");
            return null;
        }

        Shape intersection(Rect rect) {
            System.out.println("line + rect");
            return null;
        }
    }

    static class Circle extends Shape {
        Shape intersection(Shape shape) {
            return shape.intersection(this);
        }

        Shape intersection(Line line) {
            System.out.println("circle + line");
            return null;
        }

        Shape intersection(Circle circle) {
            System.out.println("circle + circle");
            return null;
        }

        Shape intersection(Rect rect) {
            System.out.println("circle + rect");
            return null;
        }
    }

    static class Rect extends Shape {
        Shape intersection(Shape shape) {
            return shape.intersection(this);
        }

        Shape intersection(Line line) {
            System.out.println("rect + line");
            return null;
        }

        Shape intersection(Circle circle) {
            System.out.println("rect + circle");
            return null;
        }

        Shape intersection(Rect rect) {
            System.out.println("rect + rect");
            return null;
        }
    }
}

示例的输出是:

circle + line
rect + line
rect + circle

答案 1 :(得分:1)

免责声明:我对Double dispatch并不熟悉。我已经看过了,我已经阅读了维基文章,但就是这样。我只是想尽力解决这个问题。


instanceof地狱

我们可以利用关于两个相交的Shape对象的类信息在运行时是已知的。运行代码的Rect知道它是Rectshape参数的类型为Shape,但是当运行方法时,它会调用正确覆盖的版本具体的Shape类型。

在下面的代码中,将在正确的intersect()类型上调用正确的Shape重载:

public interface Shape {
    public Shape intersect(Shape shape);
    public Shape intersect(Line line);
    public Shape intersect(Rect rect);
}

public class Line implements Shape {
    @Override
    public Shape intersect(Shape shape) {
        return shape.intersect(this);
    }

    @Override
    public Shape intersect(Line line) {
        System.out.println("Line - Line");
        return null;
    }

    @Override
    public Shape intersect(Rect rect) {
        System.out.println("Line - Rect");
        return null;
    }
}

public Shape intersect(Shape shape);的通用实现必须被复制到所有实现类中。如果您尝试将Shape接口更改为abstract class并在那里使用该方法,那么将不会工作,因为该方法将递归调用自身:

public abstract class Shape {
    public final Shape intersect(Shape shape) {
        return shape.intersect(this);
    }
    public abstract Shape intersect(Line line);
    public abstract Shape intersect(Rect rect);
}

但是,您可以使用反射来完成它:

public abstract class Shape {
    public final Shape intersect(Shape shape) {
        try {
            Method intersect = this.getClass().getMethod("intersect", shape.getClass());
            return (Shape)intersect.invoke(this, shape);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    public abstract Shape intersect(Line line);
    public abstract Shape intersect(Rect rect);
}

答案 2 :(得分:1)

您可以通过Visitor模式在Java中实现双重调度。

public interface ShapeVisitor<P, R> { 
    R visitRect(Rect rect, P param);
    R visitLine(Line line, P param);
    R visitText(Text text, P param);
}

public interface Shape {
    <P, R> R accept(P param, ShapeVisitor<? super P, ? extends R> visitor);
    Shape intersectionWith(Shape shape);
}

public class Rect implements Shape {

    public <P, R> R accept(P param, ShapeVisitor<? super P, ? extends R> visitor) {
        return visitor.visitRect(this, param);
    }

    public Shape intersectionWith(Shape shape) {
        return shape.accept(this, RectIntersection);
    }

    public static ShapeVisitor<Rect, Shape> RectIntersection = new ShapeVisitor<Rect, Shape>() {
        public Shape visitRect(Rect otherShape, Rect thisShape) {
            // TODO...
        }
        public Shape visitLine(Line otherShape, Rect thisShape) {
            // TODO...
        }
        public Shape visitText(Text otherShape, Rect thisShape) {
            // TODO...
        }
    };
}

当您添加新的Shape子类时,必须向ShapeVisitor接口添加新方法,并且您将丢失所有缺少的方法的编译错误。这很有用,但如果您正在编写库并且允许您的用户添加Shape子类(但显然无法扩展ShapeVisitor接口),则可能会成为一个大问题。