有什么好的设计习惯可以避免询问子类类型?


11

我已经读到,当您的程序需要知道对象是什么类时,通常表明存在设计缺陷,因此我想知道什么是处理此问题的好方法。我正在实现一个Shape类,该类具有从其继承的不同子类,例如Circle,Polygon或Rectangle,并且我有不同的算法来确定Circle是否与Polygon或Rectangle碰撞。然后假设我们有两个Shape实例,并且想知道一个是否与另一个碰撞,在这种方法中,我必须推断正在碰撞的对象是哪种子类类型,以便知道应该调用哪种算法,但这是一个设计或实践不当?这就是我解决的方法。

abstract class Shape {
  ShapeType getType();
  bool collide(Shape other);
}

class Circle : Shape {
  getType() { return Type.Circle; }

  bool collide(Shape other) {
    if(other.getType() == Type.Rect) {
      collideCircleRect(this, (Rect) other);     
    } else if(other.getType() == Type.Polygon) {
      collideCirclePolygon(this, (Polygon) other);
    }
  }
}

这是一种不良的设计模式吗?如何解决这个问题而不必推断子类类型?


1
最终,每个实例(例如Circle)都知道其他所有Shape-Type。因此它们都以某种方式牢固地连接在一起。一旦添加了诸如Triangle之类的新形状,您最终就会在各处添加Triangles支持。这取决于您要更频繁地更改什么,是否要添加新的Shapes,所以这种设计是不好的。因为您有解决方案蔓延的情况-到处都必须添加对三角形的支持。相反,您应该将Collisiondetection提取到一个单独的Class中,该Class适用于所有类型和委托。
thepacker


IMO这归结为性能要求。代码越具体,代码越优化,运行速度越快。在这种特殊情况下(实现了它,太),检查类型是可以的,因为定制的碰撞检测可以enourmously快于一个通用的解决方案。但是,当运行时性能并不重要时,我将始终采用通用/多态方法。
marstato

归功于所有,在我的情况下性能至关重要,并且我不会添加新形状,也许我会采用CollisionDetection方法,但是我仍然必须知道子类的类型,如果我在其中保留“ Type getType()”方法, Shape还是在CollisionDetection类中对Shape进行某种“实例化”?
亚历杭德罗(Alejandro)

1
抽象Shape对象之间没有有效的碰撞过程。除非您要检查边界点的碰撞bool collide(x, y)(控制点的子集可能是一个很好的折衷方案),否则您的逻辑取决于其他对象的内部。否则,您需要以某种方式检查类型-如果确实需要抽象,那么产生Collision类型(对于当前参与者区域内的对象)应该是正确的方法。
2013年

Answers:


13

多态性

只要使用getType()或类似的东西,就不会使用多态。

我了解您需要知道自己的类型。但是,您要在知道确实应该将其推入课堂的同时进行任何工作。然后,您只需告诉它何时进行即可。

程序代码获取信息然后做出决定。面向对象的代码告诉对象要做事。
—亚历克·夏普

这个原则叫做告诉,不要问。遵循它可以帮助您避免散布诸如打字之类的细节并创建对其起作用的逻辑。这样做可以使一堂课变得面目全非。最好将这种行为保留在类中,以便在类更改时可以更改。

封装形式

您可以告诉我,将不再需要其他形状,但我不相信您,也不应该。

下面封装的一个很好的效果是,它很容易地添加新的类型,因为它们的细节不散开成,他们在出现的代码ifswitch逻辑。一种新类型的代码应全部放在一个地方。

类型无知碰撞检测系统

让我向您展示如何设计一种性能出色且可在任何2D形状下工作的碰撞检测系统,而无需关心它的类型。

在此处输入图片说明

说您应该画那个。看起来很简单。都是圈子 创建一个了解碰撞的圆形类很诱人。问题在于,这使我们陷入了思考的困境,而当我们需要1000个圆时,它就会崩溃。

我们不应该考虑圈子。我们应该考虑像素。

如果我告诉您,用来绘制这些家伙的代码相同,那么您可以用来检测他们何时触摸甚至是用户单击的代码。

在此处输入图片说明

在这里,我为每个圆圈绘制了一种独特的颜色(如果您的眼睛足以看到黑色轮廓,则可以忽略它)。这意味着该隐藏图像中的每个像素都映射回其绘制的位置。哈希图很好地解决了这个问题。您实际上可以通过这种方式进行多态。

您无需显示给用户此图像。使用与绘制第一个相同的代码创建它。只是具有不同的颜色。

当用户单击一个圆圈时,我确切知道哪个圆圈,因为该颜色只有一个圆圈。

当我在另一个圆圈上绘制一个圆圈时,可以将它们倾倒在一组中,从而快速读取要覆盖的每个像素。当我完成设定点时,它碰到了每个圆,现在我只需要呼叫每个圆一次就可以通知碰撞了。

新型:矩形

这一切都是用圆圈完成的,但是我问你:使用矩形是否会有所不同?

没有圈子知识泄漏到检测系统中。它不在乎半径,圆周或中心点。它关心像素和颜色。

这种碰撞系统中唯一需要向下压成单个形状的部分是唯一的颜色。除此之外,形状还可以考虑绘制形状。无论如何,这就是他们擅长的。

现在,当您编写碰撞逻辑时,您不必关心您拥有什么子类型。您告诉它发生碰撞,并且告诉您在假装绘制的形状下找到的内容。无需知道类型。这意味着您可以添加任意多个子类型,而不必更新其他类中的代码。

实施选择

确实,它不必是唯一的颜色。它可以是实际的对象引用,并可以保存一定程度的间接引用。但是,在此答案中得出的结论看起来并不那么好。

这只是一个实现示例。当然还有其他。这意味着要显示的是,您越让这些形状子类型坚持其单一职责,整个系统就越有效。可能会有更快,内存占用较少的解决方案,但是如果它们迫使我在周围传播子类型的知识,即使性能有所提高,我也不愿使用它们。除非我明确需要它们,否则我不会使用它们。

双重派遣

到现在为止,我完全忽略了双重派遣。我这样做是因为我可以。只要冲突逻辑不在乎哪两种类型发生冲突,您就不需要它。如果您不需要它,请不要使用它。如果您认为自己可能需要它,请尽可能推迟处理它。这种态度称为YAGNI

如果您确定确实需要不同种类的碰撞,请问自己是否n个形状子类型确实需要n 种2种碰撞。到目前为止,我已经非常努力地使添加其他形状子类型变得容易。我不想用双重分发实现来破坏它,它迫使圈子知道正方形的存在。

反正有几种碰撞?稍作推测(一种危险的事情)就发明了弹性碰撞(弹力),非弹性(粘性),高能(爆炸)和破坏性(损坏)。可能会有更多,但如果小于n 2,就不要过度设计碰撞。

这意味着当我的鱼雷击中受到伤害的东西时,不必知道它击中了太空飞船。只需说:“哈哈!您受到了5点伤害。”

造成损害的事物向接受损害消息的事物发出损害信息。通过这种方式,您可以添加新形状而无需告知其他形状。您最终只会在新的碰撞类型中散布。

太空船可以将鱼雷发回鱼雷,“哈哈!你受到了100点伤害。” 以及“您现在被困在我的船体上”。然后鱼雷可以发回“好吧,我已经做好了,所以忘记我了”。

谁都不知道确切的含义。他们只是知道如何通过碰撞界面互相交谈。

现在可以肯定,双调度可以让您比这更紧密地控制事物,但是您真的想要吗?

如果您愿意的话,请至少考虑通过抽象接受形状而不是实际形状实现的抽象碰撞来进行双重调度。此外,可以将碰撞行为作为依赖项注入并委托给该依赖项。

性能

性能始终至关重要。但这并不意味着它总是一个问题。测试性能。不要只是猜测。无论如何,以性能为名牺牲其他所有内容通常都不会导致性能良好的代码。



+1表示“您可以告诉我,将不再需要其他形状,但我不相信您,您也不应该。”
TulainsCórdova16年

如果该程序不是关于绘制形状,而是纯粹的数学计算,那么考虑像素问题将无济于事。这个答案意味着您应该牺牲一切以实现面向对象的纯洁性。它还包含一个矛盾:您首先说我们应该基于将来可能需要更多类型的形状的想法来进行整个设计,然后说“ YAGNI”。最后,您忽略了使添加类型更容易的情况通常意味着添加操作变得更加困难,如果类型层次结构相对稳定,但操作却发生了很大变化,则这很糟糕。
Christian Hackl

7

问题的描述听起来像您应该使用Multimethods(aka多重调度),在这种特殊情况下-Double dispatch。第一个答案详尽地讨论了如何在栅格渲染中一般性地处理碰撞形状,但是我相信OP想要“矢量”解决方案,或者整个问题都已经根据Shape进行了重新表述,这是OOP解释中的经典示例。

甚至引用的Wikipedia文章也使用相同的碰撞隐喻,让我引用一下(Python没有像其他语言一样内置的多重方法):

@multimethod(Asteroid, Asteroid)
def collide(a, b):
    """Behavior when asteroid hits asteroid"""
    # ...define new behavior...
@multimethod(Asteroid, Spaceship)
def collide(a, b):
    """Behavior when asteroid hits spaceship"""
    # ...define new behavior...
# ... define other multimethod rules ...

因此,下一个问题是如何在您的编程语言中获得对多方法的支持。



是的,答案中添加了“多重调度又称为多重方法”的特殊情况
Roman Susi

5

此问题需要在两个级别上进行重新设计。

首先,您需要从形状中提取用于检测形状之间的碰撞的逻辑。这样,您就不会在每次需要向模型添加新形状时都违反OCP。假设您已经定义了圆形,正方形和矩形。然后可以这样:

class ShapeCollisionDetector
{
    public void DetectCollisionCircleCircle(Circle firstCircle, Circle secondCircle)
    { 
        //Code that detects collision between two circles
    }

    public void DetectCollisionCircleSquare(Circle circle, Square square)
    {
        //Code that detects collision between circle and square
    }

    public void DetectCollisionCircleRectangle(Circle circle, Rectangle rectangle)
    {
        //Code that detects collision between circle and rectangle
    }

    public void DetectCollisionSquareSquare(Square firstSquare, Square secondSquare)
    {
        //Code that detects collision between two squares
    }

    public void DetectCollisionSquareRectangle(Square square, Rectangle rectangle)
    {
        //Code that detects collision between square and rectangle
    }

    public void DetectCollisionRectangleRectangle(Rectangle firstRectangle, Rectangle secondRectangle)
    { 
        //Code that detects collision between two rectangles
    }
}

接下来,您必须根据调用它的形状安排调用适当的方法。您可以使用多态和Visitor Pattern做到这一点。为了实现这一点,我们必须具有适当的对象模型。首先,所有形状必须遵循相同的界面:

    interface IShape
{
    void DetectCollision(IShape shape);
    void Accept (ShapeVisitor visitor);
}

接下来,我们必须有一个父访问者类:

    abstract class ShapeVisitor
{
    protected ShapeCollisionDetector collisionDetector = new ShapeCollisionDetector();

    abstract public void VisitCircle (Circle circle);

    abstract public void VisitSquare(Square square);

    abstract public void VisitRectangle(Rectangle rectangle);

}

我在这里使用类而不是接口,因为我需要每个访问者对象都具有ShapeCollisionDetector类型的属性。

IShape接口的每种实现都会实例化适当的访问者,并调用Accept与之进行交互的对象的适当方法,如下所示:

    class Circle : IShape
{
    public void DetectCollision(IShape shape)
    {
        CircleVisitor visitor = new CircleVisitor(this);
        shape.Accept(visitor);
    }

    public void Accept(ShapeVisitor visitor)
    {
        visitor.VisitCircle(this);
    }
}

    class Rectangle : IShape
{
    public void DetectCollision(IShape shape)
    {
        RectangleVisitor visitor = new RectangleVisitor(this);
        shape.Accept(visitor);
    }

    public void Accept(ShapeVisitor visitor)
    {
        visitor.VisitRectangle(this);
    }
}

具体的访问者如下所示:

    class CircleVisitor : ShapeVisitor
{
    private Circle Circle { get; set; }

    public CircleVisitor(Circle circle)
    {
        this.Circle = circle;
    }

    public override void VisitCircle(Circle circle)
    {
        collisionDetector.DetectCollisionCircleCircle(Circle, circle);
    }

    public override void VisitSquare(Square square)
    {
        collisionDetector.DetectCollisionCircleSquare(Circle, square);
    }

    public override void VisitRectangle(Rectangle rectangle)
    {
        collisionDetector.DetectCollisionCircleRectangle(Circle, rectangle);
    }
}

    class RectangleVisitor : ShapeVisitor
{
    private Rectangle Rectangle { get; set; }

    public RectangleVisitor(Rectangle rectangle)
    {
        this.Rectangle = rectangle;
    }

    public override void VisitCircle(Circle circle)
    {
        collisionDetector.DetectCollisionCircleRectangle(circle, Rectangle);
    }

    public override void VisitSquare(Square square)
    {
        collisionDetector.DetectCollisionSquareRectangle(square, Rectangle);
    }

    public override void VisitRectangle(Rectangle rectangle)
    {
        collisionDetector.DetectCollisionRectangleRectangle(Rectangle, rectangle);
    }
}

这样,您不需要每次添加新形状时都更改形状类,也不需要检查形状的类型即可调用适当的碰撞检测方法。

该解决方案的缺点是,如果添加新形状,则必须使用该形状的方法(例如VisitTriangle(Triangle triangle))扩展ShapeVisitor类,因此,必须在所有其他访问者中实现该方法。但是,由于这是扩展,因此在某种意义上说,现有方法不会更改,而仅添加新方法,就不会违反OCP,并且代码开销很小。另外,通过使用class ShapeCollisionDetector,可以避免违反SRP的情况,并且可以避免代码冗余。


5

您的基本问题是,在大多数现代OO编程语言中,函数重载不适用于动态绑定(即,函数参数的类型在编译时确定)。您需要的是一个虚拟方法调用,该方法在两个对象而不是一个对象上是虚拟的。这样的方法称为多方法。但是,有多种方法可以在Java,C ++等语言中模拟这种行为。在这种情况下,双调度非常方便。

基本思想是您两次使用了多态性。当两个形状发生碰撞时,您可以通过多态调用其中一个对象的正确碰撞方法,并传递通用形状类型的另一个对象。然后,在被调用的方法中,您将知道对象是圆形,矩形还是其他形状。然后,在传递的shape对象上调用冲撞方法,并将对象传递给对象。然后,第二个调用再次通过多态性找到正确的对象类型。

abstract class Shape {
  bool collide(Shape other);
  bool collide(Rect other);
  bool collide(Circle other);
}

class Circle : Shape {

  bool collide(Shape other) {
    return other.collide(this);
  }

  bool collide(Rect other) {
    // algorithm to detect collision between Circle and Rect
  }

  // ...
}

class Rect : Shape {

  bool collide(Shape other) {
    return other.collide(this);
  }

  bool collide(Circle other) {
    // algorithm to detect collision between Circle and Rect
  }

  // ...
}

但是,此技术的一大缺点是,层次结构中的每个类都必须了解所有同级对象。如果以后添加新的形状,则会带来很高的维护负担。


2

也许这不是解决此问题的最佳方法

数学行为形状冲突特定于形状组合。这意味着您将需要的子例程数量是系统支持的形状数量的平方。形状冲突实际上不是对形状的操作,而是将形状作为参数的操作。

运营商过载策略

如果您不能简化基本的数学问题,我建议使用运算符重载方法。就像是:

 public final class ShapeOp 
 {
     static { ... }

     public static boolean collision( Shape s1, Shape s2 )  { ... }
     public static boolean collision( Point p1, Point p2 ) { ... }
     public static boolean collision( Point p1, Square s1 ) { ... }
     public static boolean collision( Point p1, Circle c1 ) { ... }
     public static boolean collision( Point p1, Line l1 ) { ... }
     public static boolean collision( Square s1, Point p2 ) { ... }
     public static boolean collision( Square s1, Square s2 ) { ... }
     public static boolean collision( Square s1, Circle c1 ) { ... }
     public static boolean collision( Square s1, Line l1 ) { ... }
     (...)

在静态初始化器上,我将使用反射来绘制方法的映射,以在通用碰撞(Shape s1,Shape s2)方法上实现动态变位。静态初始化器还可以具有检测丢失的碰撞函数并报告它们的逻辑,从而拒绝加载类。

这类似于C ++运算符重载。在C ++中,运算符重载非常混乱,因为您有一组可以重载的固定符号。但是,该概念非常有趣,可以用静态函数复制。

我之所以使用这种方法,是因为冲突不是对对象的操作。冲突是一种外部操作,它说出关于两个任意对象的某种关系。另外,静态初始化程序将能够检查我是否错过了一些碰撞功能。

尽可能简化数学问题

如前所述,碰撞函数的数量是形状类型数量的平方。这意味着在只有20个形状的系统中,您将需要400个例程,其中有21个形状441,依此类推。这是不容易扩展的。

但是您可以简化数学。无需扩展碰撞功能,您可以对每个形状进行栅格化或三角剖分。这样,碰撞引擎不需要是可扩展的。碰撞,距离,相交,合并和其他几个功能将是通用的。

三角剖分

您是否注意到大多数3D软件包和游戏对所有内容进行了三角划分?那是简化数学的形式之一。这也适用于2D形状。多边形可以被三角剖分。圆和样条曲线可以近似为多边形。

同样,您将拥有一个碰撞功能。您的班级将变成:

public class Shape 
{
    public Triangle[] triangulate();
}

和您的操作:

public final class ShapeOp
{
    public static boolean collision( Triangle[] shape1, Triangle[] shape2 )
}

不是吗?

栅格化

您可以栅格化形状以具有单个碰撞功能。

栅格化似乎是一种根本的解决方案,但可以负担得起且快速,具体取决于形状碰撞的精确度。如果不需要精确(例如在游戏中),则可能具有低分辨率位图。大多数应用程序不需要数学上的绝对精度。

近似值可能足够好。用于生物学模拟的ANTON超级计算机就是一个例子。它的数学运算舍弃了许多难以计算的量子效应,到目前为止,所进行的模拟与现实世界中的实验相符。游戏引擎和渲染包中使用的PBR计算机图形模型进行了简化,从而降低了渲染每一帧所需的计算机功能。实际上不是物理上准确的,但足够接近以使人凭肉眼相信。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.