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 acceptsIShape<T>
argument andIShape<T>
is derived fromIShape
)? Note: i've also tried an alternative design where theIShape.Intersect<T>
parameter was the abstract classShape<T>
but that didn't seem right as well. - I've had constraints
where T : IShape
on both the interface, abstract classShape<T>
andIRdelegateCollection<T>
since i was trying to avoid theT Instance
which is used in theIntersection<T>()
method, but the compiler still had problems convertingShape<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 theT
s? (There does not appear to be any advantage to constraining it)
- Is there a way to use the sahpes directly in the delegate invokation of the
- 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?
EDIT: after reading @Theraot's answer, and finding out that non-static classes can also have a static constructor, i've arrived at the following stucture:
delegate IntersectResult IntersectAction<in T, in U>(T a, U b);
interface IShape
{
IntersectResult Intersects(IShape shape, out bool flipped);
IntersectResult Intersects<T>(Shape<T> shape, out bool flipped);
}
abstract class Shape<T> : IShape
{
public static class Interactor<U>
{
public static IntersectAction<T, U> Intersect { get; set; }
}
public abstract T Instance { get; } // shape instance
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>(Shape<T2> shape, out bool flipped)
{
flipped = false;
if (Interactor<T2>.Intersect != null)
return Interactor<T2>.Intersect(Instance, shape.Instance);
if (Shape<T2>.Interactor<T>.Intersect != null)
{
flipped = true; // results are flipped since we're calling the other shape's method
return Shape<T2>.Interactor<T>.Intersect(shape.Instance, Instance);
}
return new IntersectResult();
}
}
class ShapeA : Shape<ShapeA>
{
static ShapeA()
{
Interactor<ShapeA>.Intersect = MathCol.AintersectsA;
Interactor<ShapeB>.Intersect = MathCol.AintersectsB;
}
public override ShapeA Instance { get { return this; } }
}
I've decided to nest the static Interactor class inside the Shape because it didn't feel right having it completely decoupled from the Shape class.
I'm still not happy about public abstract T Instance { get; }
but i guess it is required if I'm doing the work in the abstract class.
I'll leave the question open for a bit to see if someone else might have some more insight on the whole matter.