我有以下Shape接口,它由多个其他类实现,例如Rectangle,Circle,Triangle ......
interface IShape{
bool IsColliding(IShape other);
}
IsColliding方法应检查Shape是否与其他人发生碰撞,无论其具体类型如何。 但是,每对形状(矩形/矩形,矩形/圆形,圆形/三角形等......)都有自己的碰撞检查实现。
我正在努力为这个问题找到一个好的设计解决方案。
天真的方法是切换“其他”形状的类型以调用正确的实现:
class Rectangle : IShape{
bool IsColliding(IShape other){
if(other is Rectangle){
return CollisionHandler.CheckRectangleVsRectangle(this,(Rectangle)other);
}else if(other is Circle){
return CollisionHandler.CheckRectangleVsCircle(this,(Circle)other);
} else
// etc ...
}
}
但添加新形状意味着修改每个派生类中的方法以添加新案例。
我还想过调用一个像这样的独特的静态方法:
static bool IsColliding(IShape shapeA, IShape shapeB);
但是,即使它集中了所有内容,它也会使执行的类型测试数量增加一倍,而且我仍然需要在每个第一级“if”中添加一个新案例。
if(shapeA is Rectangle){
if(shapeB is Rectangle){
// Rectangle VS Rectangle
}else if(shapeB is Circle){
// Rectangle VS Circle
}else{
// etc ...
}
}else if(shapeA is Circle){
if(shapeB is Rectangle){
// Rectangle VS Circle
}else{
// etc ...
}
} // etc ...
那么,它怎么能更好地设计呢?
答案 0 :(得分:5)
这是一个使用双重调度(超出访问者模式的原则)的想法:
基本事实是碰撞函数是对称的。即IsCollision(shapeA, shapeB) = IsCollision(shapeB, shapeA)
。因此,您不需要实现每个n^2
组合(n
是形状类的数量),但只有大约一半:
circle tri rect
circle x x x
tri x x
rec x
因此,假设您有一个形状的顺序,每个形状都会导致与位于它们之前或相等的形状发生碰撞。
在此实现中,将特定于形状的碰撞处理分派给名为CollisionHandler
的对象。以下是接口(为简洁起见而简化):
interface IShape
{
int CollisionPrecedence { get; }
AbstractCollisionHandler CollisionHandler { get; }
void Collide(AbstractCollisionHandler handler);
}
class AbstractCollisionHandler
{
public virtual void Collides(Circle other) { throw new NotImplementedException(); }
public virtual void Collides(Rect other) { throw new NotImplementedException(); }
}
基于这些接口,特定的形状类是:
class CircleCollisionHandler : AbstractCollisionHandler
{
public override void Collides(Circle other)
{
Console.WriteLine("Collision circle-circle");
}
}
class Circle : IShape
{
public int CollisionPrecedence { get { return 0; } }
public AbstractCollisionHandler CollisionHandler { get { return new CircleCollisionHandler(); } }
public void Collide(AbstractCollisionHandler handler) { handler.Collides(this); }
}
class TriCollisionHandler : AbstractCollisionHandler
{
public override void Collides(Circle other)
{
Console.WriteLine("Collision tri-circle");
}
public override void Collides(Tri other)
{
Console.WriteLine("Collision tri-tri");
}
}
class Tri : IShape
{
public int CollisionPrecedence { get { return 1; } }
public AbstractCollisionHandler CollisionHandler { get { return new TriCollisionHandler(); } }
public void Collide(AbstractCollisionHandler handler) { handler.Collides(this); }
}
调用特定碰撞函数的函数是:
static void Collides(IShape a, IShape b)
{
if (a.CollisionPrecedence >= b.CollisionPrecedence)
b.Collide(a.CollisionHandler);
else
a.Collide(b.CollisionHandler);
}
如果你现在想要实现另一个形状Rect
,那么你必须做三件事:
更改AbstractCollisionHandler
以包含矩形
abstract class AbstractCollisionHandler
{
...
public virtual void Collides(Rect other) { throw new NotImplementedException(); }
}
实施碰撞处理程序
class RectCollisionHandler : AbstractCollisionHandler
{
public override void Collides(Circle other)
{
Console.WriteLine("Collision rect-circle");
}
public override void Collides(Tri other)
{
Console.WriteLine("Collision rect-tri");
}
public override void Collides(Rect other)
{
Console.WriteLine("Collision rect-rect");
}
}
并在Rect
类中实现相关的接口方法:
class Rect : IShape
{
public int CollisionPrecedence { get { return 2; } }
public AbstractCollisionHandler CollisionHandler { get { return new RectCollisionHandler(); } }
public void Collide(AbstractCollisionHandler handler) { handler.Collides(this); }
}
这很简单。这是一个显示被调用函数的小测试程序:
Collides(new Circle(), new Tri());
Collides(new Tri(), new Circle());
Collides(new Rect(), new Circle());
输出:
Collision tri-circle
Collision tri-circle
Collision rect-circle
答案 1 :(得分:4)
考虑一下:您需要的是一种根据两个参数(this
和other
)而变化的行为。
换句话说,您需要的是Multiple Dispatch(或更具体地说,Double Dispatch)。起初,像许多其他" OOP"从C ++派生的语言,C#只是为了支持Single Dispatch(就像Java一样,不像Common Lisp,Clojure,Lua这样的语言,它们被设计为支持多重调度)。
有一种经典的方法可以模拟单个调度语言上的多个调度,称为Visitor Pattern。如果您想要遵循该路径,Stack Overflow上已经有an answer(使用C#和访问者模式,以及与您的问题非常相似的问题),所以我不会重复它。 / p>
我可以补充的是,与Java,C#4.0 + 支持多个调度...使用dynamic
关键字,加上通常的方法重载。
所以我们可以这样:
public abstract class Shape
{
private CollisionDetector detector = new CollisionDetector();
public bool IsColliding(Shape that)
{
return detector.IsColliding((dynamic) this, (dynamic) that);
}
}
public class CollisionDetector
{
public bool IsColliding(Circle circle1, Circle circle2)
{
Console.WriteLine("circle x circle");
return true;
}
public bool IsColliding(Circle circle, Rectangle rectangle)
{
Console.WriteLine("circle x rectangle");
return true;
}
public bool IsColliding(Rectangle rectangle, Circle circle)
{
// Just reuse the previous method, it is the same logic:
return IsColliding(circle, rectangle);
}
public bool IsColliding(Rectangle rectangle1, Rectangle rectangle2)
{
Console.WriteLine("rectangle x rectangle");
return true;
}
}
public class Circle : Shape { }
public class Rectangle : Shape { }
是的,这会按预期工作。使用dynamic
将强制后期绑定,因此将在运行时选择实际的方法调用。当然,这会产生性能成本:动态类型分辨率比静态分辨率慢得多。如果这是不可接受的,请使用我在上面引用的答案。
答案 2 :(得分:1)
是的,你是对的。在您当前的方法中,您违反了Open/Closed原则。
任务的第一部分已正确完成。您正在通过为每个形状添加碰撞处理程序来决定如何处理碰撞,例如:您正在使用Rectangle
方法创建类IsColliding
等。
然后你需要做出另一个决定,如何应对这次碰撞。响应方需要关注它。因此,响应此碰撞是other
形状的工作。
我建议在合同中添加新方法RespondToCollision(IShape)
。
在这种情况下,您可以创建以下(伪)方案
Collide(IShape other) {
// do smth with other.Properties
other.RespondToCollision(this);
}
RespondToCollision(IShape other) {
// do smth with this.Properties<>other.Properties
}
如果两个函数的形状都没有足够的参数,则可以使用OneToAnotherCollisionMethod
将静态类更改为策略类(请查看Strategy Pattern)并将这些策略作为参数传递。
考虑到形状是通过它们的坐标检查碰撞的事实,通过将目标侧传递到源侧而反之亦然,建立公式并不困难。
答案 3 :(得分:1)
也许这不是最美丽的解决方案,但你可以写出接受各种形状的方法。
CollisionHandler.Check(Rectangle r = null, Circle c = null, Triangle t = null)
{
if(r != null && c != null
{
return CollisionHandler.CheckRectangleVsCircle(r,c);
}
}
答案 4 :(得分:1)
我真的认为你在这里过度工程。
您的所有形状基本上都是顶点和边的集合,甚至是圆形(只需选择满足精度需求的顶点数)。
一旦你的所有形状都是一个点和边的集合,你只需要在一个地方处理碰撞,它对所涉及的任何形状都有效。
如果你的形状是凸的,你的碰撞算法就像检查一个形状是否包含另一个形状的至少一个顶点一样简单,Contains(Point p)
可以是每个形状覆盖的虚拟方法。