What's the best way to check if an object can be dropped on another object without using instanceof?
I'm developing a drag and drop shape editor. However I also need a way of "connecting" shapes together, where only specific shapes can be connected to other specific shapes. For example, a square can only be connected to a circle, but a triangle can be connected to both a square and a circle.
So what I did was to create a super class "Shape" and have all other shapes be objects of classes which inherit from the Shape class. In the Shape class I put a a method called "canBeConnectedTo(Shape s)" which returns whether or not that particular object can be connected to another particular object, but the only way I see how to do that would be using the instanceof operator which leads me to think that there might be a better design pattern.
At the moment the Square class implements the method like this:
boolean canBeConnectedTo(Shape s) {
return s instanceof Circle;
}
I would like to do this in such a way that the shapes a particular shape can be connected to is parameterized so that they can be added or removed at run-time. Plus it has to be extendable so that new shapes can be easily added without changing code.
An alternative I came up with is by creating an instance variable in each Shape object w开发者_如何学运维hich contains the type of shape the object is but that isn't a design pattern, it's just a way to avoid using instanceof without changing the structure.
So is my thinking flawed from the start or is there no better way than the mentioned alternative?
I would separate the logic of which shapes can connect to each other into a separate object (a connection manager) which can be configured at run time. Shapes would need to implement a new IConnectable interface. The connection manager can be configured at runtime to specify which connectables can be connected.
Excample C# code (untested):
// The connectable categories don't *need* to be coupled with shape types,
// but you could have a separate category for each shape if required.
public enum ConnectableCategory
{
Square,
Triangle
}
// All shapes would implement this interface.
public interface IConnectable
{
ConnectableCategory Catgetory { get; }
}
// Shapes (or other control logic) would have access to a connection manager
// which determines which connectables can be connected to each other.
public interface IConnectionManager
{
bool CanConnect(IConnectable first, IConnectable second);
}
// Here's an example impelentation of a connection manager.
public class ConnectionManager : IConnectionManager
{
private Dictionary<ConnectableCategory, HashSet<ConnectableCategory>>
permittedConnections =
new Dictionary<ConnectableCategory, HashSet<ConnectableCategory>>();
// Configure the connection manager to permit connections between specified
// categories of connectables.
public void PermitConnection(
ConnectableCategory first,
ConnectableCategory second)
{
HashSet<ConnectableCategory> permittedTargets;
if (this.permittedConnections.TryGetValue(first, out permittedTargets))
{
permittedTargets.Add(second);
}
else
{
permittedTargets = new HashSet<ConnectableCategory>() { second };
this.permittedConnections.Add(first, permittedTargets);
}
}
// Test if two connectables can be connected.
public bool CanConnect(IConnectable first, IConnectable second)
{
HashSet<ConnectableCategory> permittedTargets;
if (this.permittedConnections.TryGetValue(
first.Catgetory,
out permittedTargets))
{
return permittedTargets.Contains(second.Catgetory);
}
else
{
return false;
}
}
}
Here is a design that:
- doesn't use
instanceof
or equivalents. shapes a particular shape can be connected to can be added or removed at run-time.
public class Shape { public Map<Type, Boolean> connections = new EnumMap<Type, Boolean>(Type.class); public Boolean canConnectTo(Shape shape) { Boolean canConnect = connections.get(shape.getType()); if (canConnect == null) canConnect = false; return canConnect; } } public class Circle extends Shape { public Circle() { connections.put(typeof(Square), true); connections.put(typeof(Triangle), false); } } public class Square extends Shape { public Square() { connections.put(typeof(Circle), true); connections.put(typeof(Triangle), true); } } public class Triangle extends Shape { public Triangle() { connections.put(typeof(Circle), false); connections.put(typeof(Square), true); } }
The connections
EnumMap maps the possible connections between shapes. If the key mapped to a certain shape's type is either missing or explicitly false, then the connection is not allowed, otherwise the two shapes (the current one and the one received as parameter) may connect.
As connections
is public, it can be modified on each instance of each subclass of Shape
, therefore you may decide at runtime which shapes can connect and which cannot.
I would like to do this in such a way that the shapes a particular shape can be connected to is parameterized so that they can be added or removed at run-time.
In that case checking the types appears to be necessary. The Class class has methods isAssignableFrom
and isInstance
which are reflection's equivalents for instanceof
, or just use the equals
method if subtyping does not need to be taken into consideration (anyways it's recommendable to avoid extending concrete classes). Using them you could have one class which keeps track of which shapes can connect to which shapes (for example using a Map of Class instances), and thus have that logic in one place instead of being spread over all subclasses of Shape.
It could be done with something like this, assuming that subclassing is not used so we can just compare the classes for equality. (Disclaimer: compiled, not tested)
import java.util.*;
public class ConnectionPermissions {
private final Set<Connection> allowed = new HashSet<Connection>();
public void allowConnection(Class<?> from, Class<?> to) {
allowed.add(new Connection(from, to));
}
public void disallowConnection(Class<?> from, Class<?> to) {
allowed.remove(new Connection(from, to));
}
public boolean isConnectionAllowed(Class<?> from, Class<?> to) {
return allowed.contains(new Connection(from, to));
}
// Could also use a method like this, to hide the detail that permissions
// are based on the types of the objects.
public boolean isConnectionAllowed(Object from, Object to) {
return isConnectionAllowed(from.getClass(), to.getClass());
}
private static class Connection {
private final Class<?> c1;
private final Class<?> c2;
public Connection(Class<?> c1, Class<?> c2) {
// Remove this condition if the connections should not be symmetric.
if (c1.getName().compareTo(c2.getName()) < 0) {
this.c1 = c2;
this.c2 = c1;
} else {
this.c1 = c1;
this.c2 = c2;
}
}
public boolean equals(Object obj) {
Connection that = (Connection) obj;
return this.c1.equals(that.c1) && this.c2.equals(that.c2);
}
public int hashCode() {
int result = c1.hashCode();
result = 31 * result + c2.hashCode();
return result;
}
}
}
精彩评论