繁体   English   中英

如何为其派生类型的每种可能组合实现基类的方法

[英]How to implement a method of a base class for every possible combination of its derived types

我有以下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 ...

那么,它怎么能更好地设计呢?

这是一个使用双重调度的想法(超出访问者模式的原则):

基本事实是碰撞函数是对称的。 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以包含rect

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

试想一下:你需要的是一个根据两个参数( thisother )改变的行为。

换句话说,你需要的是多分派 (或更具体地说, 双调度 )。 首先,与从C ++派生的许多其他“OOP”语言一样,C#被设计为仅支持单一调度 (如Java,与Common Lisp,Clojure,Lua等旨在支持Multiple Dispatch的语言不同)。

有一种经典的方法可以模拟单个调度语言上的多个调度,称为访问者模式 如果你想要遵循这条路径,那么Stack Overflow上已有答案 (使用C#和访问者模式,以及与你的问题非常相似的问题),所以我不再重复了。

我可以补充的是,与Java不同,C#4.0+ 确实支持Multiple Dispatch ...通过使用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会强制进行后期绑定,因此在运行时将选择实际的方法调用。 当然,这会产生性能成本:动态类型分辨率比静态分辨率慢得多。 如果这是不可接受的,请使用我在上面引用的答案。

是的,你是对的。 在您当前的方法中,您违反了开放/封闭原则。

任务的第一部分正确完成。 您正在通过为每个形状添加碰撞处理程序来决定如何处理碰撞,例如,您正在使用IsColliding方法创建类Rectangle等。

然后你需要做出另一个决定,如何应对这次碰撞。 响应方需要关注它。 因此,应对这次碰撞是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将静态类OneToAnotherCollisionMethod为策略类(查看策略模式 )并将这些策略作为参数传递。

考虑到形状是通过它们的坐标检查碰撞的事实,通过将目标侧传递到源侧而反之亦然来构建公式并不困难。

也许这不是最美丽的解决方案,但你可以写出接受各种形状的方法。

CollisionHandler.Check(Rectangle r = null, Circle c = null, Triangle t = null)
{
   if(r != null && c != null
   {
      return CollisionHandler.CheckRectangleVsCircle(r,c);
   }
}

我真的觉得你在这里过度工程。

您的所有形状基本上都是顶点和边的集合,甚至是圆(只需选择满足精度需求的顶点数)。

一旦你的所有形状都是一个点和边的集合,你只需要在一个地方处理碰撞,它将对任何涉及的形状有效。

如果你的形状是凸的,你的碰撞算法可以像检查一个形状是否包含另一个形状的至少一个顶点一样简单,并且Contains(Point p)可以是由每个形状覆盖的虚拟方法。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM