Как реализовать метод базового класса для каждой возможной комбинации его производных типов

У меня есть следующий интерфейс Shape, который реализуется несколькими другими классами, такими как Rectangle, Circle, Triangle...

interface IShape{
    bool IsColliding(IShape other);
}

Метод IsColliding должен проверять, сталкивается ли Shape с другим или нет, независимо от их конкретного типа. Однако каждая пара фигур (Rectangle/Rectangle, Rectangle/Circle, Circle/Triangle и т.д.) Имеет свою собственную реализацию для этой проверки столкновения.

Я пытаюсь найти хорошее решение для решения этой проблемы.

Наивным методом было бы переключить тип "другой" формы, чтобы вызвать правильную реализацию:

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 ...

Итак, как его лучше настроить?

Ответ 1

Вот идея, использующая двойную отправку (принцип за пределами шаблона посетителя):

Основной факт состоит в том, что функция столкновения симметрична. То есть 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

Ответ 2

Подумайте об этом: вам нужно поведение, которое изменяется в соответствии с обоими параметрами (this и other).

Другими словами, вам нужно Multiple Dispatch (или, более конкретно, Double Dispatch). Сначала, как и многие другие языки "ООП", полученные из С++, С# был разработан только для поддержки Single Dispatch (например, Java, и в отличие от языков, таких как как Common Lisp, Clojure, Lua, которые были предназначены для поддержки множественной отправки).

Существует классический способ эмулировать множественную отправку на отдельных языках рассылки, называемый шаблон посетителя. Если вы хотите следовать этому пути, здесь уже есть ответ в разделе "Переполнение стека" (с использованием как С#, так и шаблона посетителя, а также для проблемы, очень похожей на вашу) поэтому я не буду повторять его.

Я могу добавить, что, в отличие от, например, Java, С# 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 приведет к позднему связыванию, поэтому фактический вызов метода будет выбран во время выполнения. Конечно, это будет стоить производительности: разрешение динамического типа намного медленнее, чем статическое. Если это неприемлемо, используйте ответ, на который я ссылался выше.

Ответ 3

Да, вы правы. В вашем текущем подходе вы нарушаете принцип 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) и передать эти стратегии в качестве аргументов.

Учитывая тот факт, что формы проверяются на столкновение, возможно, по их координатам, будет сложно построить формулу, передав целевую сторону стороне источника и наоборот.

Ответ 4

Возможно, это не самое красивое решение, но вы можете написать метод, принимающий все виды формы.

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

Ответ 5

Я действительно думаю, что вы здесь слишком сложны.

Все ваши фигуры по существу представляют собой набор вершин и ребер, даже кругов (просто выберите, сколько вершин удовлетворяет вашим потребностям).

Как только все ваши фигуры представляют собой набор точек и краев, вам нужно только обрабатывать коллизии в одном месте, и оно будет действительным для любых задействованных фигур.

Если ваши формы выпуклые, ваш алгоритм столкновения может быть простым, если проверить, содержит ли одна фигура хотя бы одна вершина другой формы, а Contains(Point p) может быть виртуальным методом, переопределенным каждой фигурой.