Essentially you are building graphs. You will need to separate the edges from the vertices.
Let's start by creating some interfaces that separate some concerns:
interface Shape {
}
interface ShapeConnection {
Shape[] getConnectedShapes();
}
Then let's introduce an annotation that will mark shapes that need to cascade delete their connected shapes.
@interface CascadeDeleteConnectedShapes {
}
Rectangle and Line can then be defined as:
@CascadeDeleteConnectedShapes
class Rectangle implements Shape {
}
class Line implements Shape, ShapeConnection {
Rectangle from, to;
public Line(Rectangle from, Rectangle to) {
this.from = from;
this.to = to;
}
@Override
public Shape[] getConnectedShapes() {
return new Shape[] { from, to };
}
}
Finally, you will need a place where you can put it all together.
class Canvas {
private ConnectionManager connectionManager = new ConnectionManager();
private Set<Shape> shapes = new HashSet<Shape>();
public Canvas() {
}
public void removeShape(Shape shape) {
if (!shapes.remove(shape))
return;
if (shape.getClass().isAnnotationPresent(CascadeDeleteConnectedShapes.class)) {
cascadeDeleteShape(shape);
}
if (shape instanceof ShapeConnection) {
connectionManager.remove((ShapeConnection) shape);
}
}
private void cascadeDeleteShape(Shape shape) {
List<ShapeConnection> connections = connectionManager.getConnections(shape);
for (ShapeConnection connection : connections) {
if (connection instanceof Shape) {
this.removeShape((Shape) connection);
} else {
connectionManager.remove(connection);
}
}
}
public void addShape(Shape shape) {
if (shapes.contains(shape))
return;
if (shape instanceof ShapeConnection) {
addShapeConnection((ShapeConnection) shape);
}
shapes.add(shape);
}
private void addShapeConnection(ShapeConnection shapeConnection) {
for (Shape shape : shapeConnection.getConnectedShapes()) {
if (!shapes.contains(shape))
throw new Error("cannot connect unknown shapes");
}
connectionManager.add(shapeConnection);
}
}
A shape can be a shape connection at the same time. Once a few rectangles are added to the canvas, lines can be added to connect them. Since a line in your design is recognized as a ShapeConnection
any operation involving the line will invoke the Canvas
to let the ConnectionManager
handle the graph. In this design it is important that Line
is immutable.
Cascades are triggered by the removal of an annotated shape. You'll need to manage these cascades carefully: if an exception occurs somewhere along the way you are left with an incomplete graph.
This code only serves to give you an idea. Also I'm leaving the implementation of the connection manager to your imagination. Guava has been mentioned in one of the comments. A BiMultiMap would have served your purpose just right, too bad they haven't released it yet. In any case, I would certainly have a look at letting the specifics of the ConnectionManager
be handled by an existing library; many have been written that will fit your needs.
Note that there are no circular dependencies in this design.