How do I use double dispatch to analyze intersection of graphic primitives?

巧了我就是萌 提交于 2019-12-24 03:20:13

问题


I am analyzing the interaction of graphics primitives (rect, line, circle, etc.) and computing the overlap, relative orientation, merging, etc. This is quoted as a prime example of Double Dispatch (e.g. Wikipedia)

Adaptive collision algorithms usually require that collisions between different objects be handled in different ways. A typical example is in a game environment where the collision between a spaceship and an asteroid is computed differently than the collision between a spaceship and a spacestation.1

but I haven't understood the main explanations and I also don't generally understand the answers on SO.

My current code (Java) uses a superclass Shape and is something like:

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

with specific implementations in subclasses (here Rect) such as

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

I have to write all the n*(n-1)/2 methods anyway (and have done so). I also have to have extensible code to accommodate (say) at a later date:

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

I don't see how to use, or the value of, the double dispatch pattern and would appreciate a concrete example using Java graphics primitives or similar pseudocde.

UPDATE: I have accepted @Flavio as (I think) it answers the exact question asked. However I have actually implemented @Slanec as it solves my problem and (to me) is simpler and easier to read. I have a subsidiary question "Do the solutions depend on the relationship being symmetric?".

"A intersects B" is usually identical to "B intersects A" but "A collides with B" is not always the same as "B collides with A". (A == car, B == cyclist). It is conceivable that my intersections may not be symmetric in futute (e.g. "Rect partially obscures Circle" is not symmetric and may have different semantics.

@Flavio addresses the maintenance problem well, and points out that the compiler can check for problems. @Slanec does this through reflection which looks as if it is a useful maintenance aid - I don't know what the performance hit is.


回答1:


You can implement double dispatch in Java through the Visitor pattern.

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...
        }
    };
}

When you add a new Shape subclass, you must add a new method to the ShapeVisitor interface, and you get compile errors for all the methods you are missing. This is useful, but can become a big problem if you are writing a library and your users are allowed to add Shape subclasses (but clearly can not extend the ShapeVisitor interface).




回答2:


I think it would be something like this:

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

The output of the example is:

circle + line
rect + line
rect + circle



回答3:


Disclaimer: I am not really familiar with Double dispatch. I've seen it, I've read the wiki article, but that's it. I am simply trying to tackle the problem the best I can.


The instanceof hell

We can leverage that the class information about both intersected Shape objects is known at runtime. The Rect running your code knows it's a Rect and the shape parameter is of type Shape, but when a method is ran on it, it will invoke the correctly overridden version of the concrete Shape type.

In the code below, the correct intersect() overload will be called on the correct Shape type:

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

The generic implementation of public Shape intersect(Shape shape); must be copypasted into all implementing classes. If you tried to change the Shape interface to a an abstract class and have the method there, it wouldn't work, because the method will call itself recursively:

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

However, you can use reflection to get it done:

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


来源:https://stackoverflow.com/questions/19400582/how-do-i-use-double-dispatch-to-analyze-intersection-of-graphic-primitives

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!