I've been trying to come up with a generic shape design that would support shape-shape interactions (like Intersect), based on the shape's type.
So a common class/interface was needed, but a problem arises from shape types since not all shapes have common properties (eg. Circle is composed of a point and a radius, while polygons are composed of multiple points) so each pair of shape types has to have their own Intersect method.
After a lot of reading up on the subject of generics (and the like), i've come up with the following structure:
Collection class for storing delegates to shape Intersect methods
class IRdelegateCollection<T> // IntersectResult delegate collection
{
public delegate IntersectResult IntersectAction<in U>(T a, U b);
Dictionary<Type, Delegate> _jumpTable;
public IRdelegateCollection()
{
_jumpTable = new Dictionary<Type, Delegate>();
}
public IRdelegateCollection(Dictionary<Type, Delegate> jumpTable)
{
_jumpTable = jumpTable;
}
public void Set<U>(IntersectAction<U> action)
{
if (_jumpTable.ContainsKey(typeof(U)))
_jumpTable[typeof(U)] = action;
else
_jumpTable.Add(typeof(U), action);
}
public IntersectAction<U> Get<U>()
{
if (!_jumpTable.ContainsKey(typeof(U)))
{
return null;
}
return _jumpTable[typeof(U)] as IntersectAction<U>;
}
}
Interfaces
interface IShape
{
IntersectResult Intersects(IShape shape, out bool flipped);
IntersectResult Intersects<T>(IShape<T> shape, out bool flipped);
}
interface IShape<T> : IShape
{
T Instance { get; }
IRdelegateCollection<T> IC { get; }
}
Base Shape class (all shapes will derive from this, although i guess they don't have to as long as they have their own IShape interface implementation)
abstract class Shape<T> : IShape<T>
{
public abstract T Instance { get; } // shape instance
public abstract IRdelegateCollection<T> IC { get; } // shape type's intersection delegate collection
public IntersectResult Intersects(IShape shape, out bool flipped)
{
IntersectResult r = shape.Intersects<T>(this, out flipped); // call the other shape's intersect method, specifying our type
flipped = !flipped; // results are flipped since we're calling the other shape's method
return r;
}
public IntersectResult Intersects<T2>(IShape<T2> shape, out bool flipped)
{
flipped = false;
var del1vs2 = IC.Get<T2>();
if (del1vs2 != null) // we have the intersection delegate
return del1vs2(Instance, shape.Instance);
var del2vs1 = shape.IC.Get<T>(); // we dont have the delegate, try get the other shape's intersection delegate
if (del2vs1 != null)
{
flipped = true; // we're returning the results of other shape's delegate, mark it as flipped
return del2vs1(shape.Instance, Instance);
}
// neither of the two shapes have the intersection delegate
return new IntersectResult();
}
}
Some concrete shape definitions. (ShapeA does not have delegate for ShapeA-ShapeB intersection, but since ShapeB has one for ShapeB-ShapeA thats the one that gets called when intersecting ShapeB on ShapeA)
class ShapeA : Shape<ShapeA>
{
static IRdelegateCollection<ShapeA> _ic =
new IRdelegateCollection<ShapeA>(
new Dictionary<Type, Delegate>()
{
{ typeof(ShapeA), new IRdelegateCollection<ShapeA>.IntersectAction<ShapeA>(MathCol.AIntersectsA) }
}
);
public override ShapeA Instance { get { return this; } }
public override IRdelegateCollection<ShapeA> IC { get { return _ic; } }
}
class ShapeB : Shape<ShapeB>
{
static IRdelegateCollection<ShapeB> _ic =
new IRdelegateCollection<ShapeB>(
new Dictionary<Type, Delegate>()
{
{ typeof(ShapeB), new IRdelegateCollection<ShapeB>.IntersectAction<ShapeB>(MathCol.BIntersectsB) },
{ typeof(ShapeA), new IRdelegateCollection<ShapeB>.IntersectAction<ShapeA>(MathCol.BIntersectsA) }
}
);
public override ShapeB Instance { get { return this; } }
public override IRdelegateCollection<ShapeB> IC { get { return _ic; } }
}
With this kind of structure i can have a list of shapes and be able to intersect them appropriately without having to manually check their types.
ShapeA a = new ShapeA();
ShapeB b = new ShapeB();
bool flipped = false;
// These are called via .Intersects<T>(T shape, out bool flipped)
a.Intersects(a, out flipped); //calls AintersectsA, flipped: False
a.Intersects(b, out flipped); //calls BintersectsA, flipped: True
b.Intersects(a, out flipped); //calls BintersectsA, flipped: False
b.Intersects(b, out flipped); //calls BintersectsB, flipped: False
List<IShape> shapes = new List<IShape>();
shapes.Add(a);
shapes.Add(b);
// These are called via .Intersects(IShape shape, out bool flipped)
shapes[0].Intersects(shapes[0], out flipped); //calls AintersectsA, flipped: True
shapes[0].Intersects(shapes[1], out flipped); //calls BintersectsA, flipped: True
shapes[1].Intersects(shapes[0], out flipped); //calls BintersectsA, flipped: False
shapes[1].Intersects(shapes[1], out flipped); //calls BintersectsB, flipped: True
So there it is, now what i would like to know is:
- Is this design appropriate for my requirement (having base shape that can intersect other shapes based on their subtype)?
- The double interface - is this considered bad design (since
IShape
's method accepts IShape<T>
argument and IShape<T>
is derived from IShape
)?
Note: i've also tried an alternative design where the IShape.Intersect<T>
parameter was the abstract class Shape<T>
but that didn't seem right as well.
- I've had constraints
where T : IShape
on both the interface, abstract class Shape<T>
and IRdelegateCollection<T>
since i was trying to avoid the T Instance
which is used in the Intersection<T>()
method, but the compiler still had problems converting Shape<T> to T
.
- Is there a way to use the sahpes directly in the delegate invokation of the
Shape<T>.Intersects<T2>
method? (preferably: del1v2(this, shape)
- was throwing the above mentioned error, even with the constraints all over the place)
- Should there be constraints of
where T : IShape
on all the T
s? (There does not appear to be any advantage to constraining it)
- Class
IRdelegateCollection<T>
uses a "jump table". My implementation needs a _jumpTable.ContainsKey(typeof(U))
check for both setting and getting. Is there a better way of making this kind of jump table?