Яка хороша практика дизайну, щоб не задавати тип підкласу?


11

Я читав, що, коли вашій програмі потрібно знати, що таке клас об'єкта, зазвичай вказують на недолік дизайну, тому я хочу знати, яка хороша практика для цього. Я реалізую клас Shape з різними підкласами, успадкованими від нього, як 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
Ви закінчуєте, що кожен екземпляр, наприклад, Коло, знає всі інші типи форм. Тому всі вони якось міцно пов'язані. І як тільки ви додасте нову форму, як, наприклад, Трикутник, ви в кінцевому підсумку додаєте підтримку трикутників скрізь. Це залежить від того, що ви хочете частіше змінювати, додаватимете нові форми, такий дизайн поганий. Оскільки у вас розповсюдження розчину - ваша підтримка трикутників повинна додаватися скрізь. Натомість слід витягнути своє колізійне виявлення в окремий клас, який може працювати з усіма типами та делегувати.
thepacker


IMO це зводиться до вимог щодо продуктивності. Чим конкретніший код, тим більш оптимізованим він може бути і швидше він буде працювати. В даному конкретному випадку (реалізується це теж), перевірка типу КІ , тому що адаптовані перевірки зіткнень можуть бути enourmously швидше , ніж загальне рішення. Але коли продуктивність роботи не є критичною, я завжди працюю із загальним / поліморфним підходом.
marstato

Дякуючи всім, у моєму випадку продуктивність є критичною, і я не додаватиму нових форм, можливо, я використовую підхід CollisionDetection, однак мені все ж довелося знати тип підкласу, чи повинен я зберігати метод "Type getType ()" у Форма або замість цього робиться якийсь "екземпляр" з Shape у класі CollisionDetection?
Алехандро

1
Не існує ефективної процедури зіткнення абстрактних Shapeоб'єктів. Ваша логіка залежить від внутрішніх внутрішніх об'єктів, якщо ви не перевіряєте зіткнення для граничних точок bool collide(x, y)(підмножина контрольних точок може бути хорошим компромісом). В іншому випадку вам потрібно перевірити тип - якщо справді потрібні абстракції, то створення Collisionтипів (для об'єктів у зоні поточного актора) має бути правильним підходом.
здригаються

Відповіді:


13

Поліморфізм

Поки ви використовуєте getType()чи щось подібне, ви не використовуєте поліморфізм.

Я розумію, відчуваю, що вам потрібно знати, який у вас тип. Але будь-яку роботу, яку ви хотіли б зробити, знаючи, що її справді слід відсунути в клас. Тоді ви просто скажете, коли це зробити.

Процедурний код отримує інформацію, після чого приймає рішення. Об'єктно-орієнтований код повідомляє об'єктам робити речі.
- Алек Шарп

Цей принцип називається скажи, не питай . Дотримуючись це, ви допомагаєте не поширювати деталі на зразок типу навколо та створювати логіку, яка діє на них. Це перетворює клас назовні. Краще зберігати таку поведінку всередині класу, щоб вона могла змінюватися, коли клас змінювався.

Інкапсуляція

Ви можете сказати мені, що інші форми ніколи не знадобляться, але я не вірю вам, а також вам.

Приємним ефектом після інкапсуляції є те, що легко додавати нові типи, оскільки їх деталі не поширюються в код, де вони відображаються, ifі switchлогіка. Код нового типу повинен бути в одному місці.

Невідома система виявлення зіткнень

Дозвольте мені показати вам, як я б спроектував систему виявлення зіткнень, яка працює та працює з будь-якою двовимірною формою, не піклуючись про тип.

введіть тут опис зображення

Скажіть, ви повинні це намалювати. Здається, просто. Це всі кола. Заманливо створити клас гуртка, який розуміє зіткнення. Проблема полягає в тому, що це направляє нас вниз по лінії мислення, яка розпадається, коли нам потрібно 1000 кіл.

Ми не повинні думати про кола. Ми повинні думати про пікселі.

Що робити, якщо я сказав вам, що той самий код, який ви використовуєте для малювання цих хлопців, - це те, що ви можете використовувати для виявлення, коли вони торкаються або навіть того, на який користувач натискає.

введіть тут опис зображення

Тут я намалював кожне коло унікальним кольором (якщо ваші очі досить гарні, щоб побачити чорний контур, просто проігноруйте це). Це означає, що кожен піксель у цьому прихованому зображенні відображається до того, що його намалювало. Хешмап добре піклується про це. Насправді можна зробити поліморфізм таким чином.

Це зображення ви ніколи не повинні показувати користувачеві. Ви створюєте його з тим самим кодом, який намалював перший. Просто з різними кольорами.

Коли користувач натискає коло, я точно знаю, яке коло, тому що лише одне коло - це колір.

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

Новий тип: Прямокутники

Це все робилося за допомогою кіл, але я запитаю вас: чи не працювало б воно з прямокутниками?

Жодна інформація про коло не просочилася в систему виявлення. Це не хвилює радіус, окружність або центральну точку. Це піклується про пікселі та кольорі.

Єдина частина цієї системи зіткнень, яку потрібно відсунути в окремі форми, - це унікальний колір. Крім того, фігури можуть просто думати про малювання своїх фігур. Це все, що їм добре.

Тепер, коли ви пишете логіку зіткнення, вам не байдуже, який у вас підтип. Ви кажете йому зіткнутися, і він розповість вам, що він знайшов під формою, яку прикидається малювати. Не потрібно знати тип. А це означає, що ви можете додати скільки завгодно підтипів, не потребуючи оновлення коду в інших класах.

Реалізація варіантів

Дійсно, це не потрібно, щоб це був унікальний колір. Це можуть бути фактичні посилання на об'єкт і зберегти рівень непрямості. Але це не виглядало б так приємно, коли звертається у цій відповіді.

Це лише один приклад реалізації. Звичайно, є й інші. Що це означало показати, що чим ближче ви дозволяєте цим підтипам форми дотримуватися своєї єдиної відповідальності, тим краще працює вся система. Ймовірно, є швидші та менш об'ємні оперативні пам’яті, але якщо вони змусять мене поширювати знання про підтипи навколо, я б ненавиджу використовувати їх навіть із збільшенням продуктивності. Я б не користувався ними, якщо я їх явно не потребував.

Подвійна відправка

До цих пір я повністю ігнорував подвійну розсилку . Я це зробив, бо міг. До тих пір, поки логіку зіткнення не хвилює, які два типи зіткнулися, вам це не потрібно. Якщо він вам не потрібен, не використовуйте його. Якщо ви думаєте, що вам це може знадобитися, відкладіть справу, поки зможете. Таке ставлення називається ЯГНІ .

Якщо ви вирішили, що вам справді потрібні різні види зіткнень, то запитайте себе, чи потрібні підтипи n форми справді n 2 видів зіткнень. Поки я дуже наполегливо працював, щоб легко додати ще один підтип форми. Я не хочу зіпсувати це подвійною диспетчерською реалізацією, яка змушує кола знати, що квадрати існують.

Скільки видів зіткнень все-таки є? Трохи міркуючи (небезпечна річ) вигадує пружні зіткнення (підстрибування), нееластичні (липкі), енергійні (вибухові) та руйнівні (пошкодження). Може бути і більше, але якщо це менше n 2, то не можемо сконструювати наші зіткнення.

Це означає, що коли моя торпеда потрапляє на щось, що сприймає шкоду, не потрібно її знати, коли вона потрапила в космічний корабель. Треба лише сказати: "Ха-ха! Ти забрав 5 балів шкоди".

Речі, які завдають шкоди, надсилають повідомлення про пошкодження речам, які приймають повідомлення про пошкодження. Зроблено таким чином, ви можете додавати нові форми, не повідомляючи іншим про нову форму. Ви лише поширюєтесь навколо нових типів зіткнень.

Космічний корабель може відправити назад на торп "Ха-ха! Ти забрав 100 очок шкоди". а також "Ти зараз причепився до мого корпусу". І торп може відправити назад "Ну, я готовий, щоб забути про мене".

Жоден момент не знає, що саме таке кожен. Вони просто знають, як спілкуватися один з одним через інтерфейс зіткнення.

Тепер впевнене, що подвійна відправка дозволяє контролювати речі більш інтимно, ніж це, але чи справді ви цього хочете ?

Якщо ви хочете, подумайте, принаймні, подумайте про подвійну диспетчеризацію абстракцій про те, які види зіткнень приймає форма, а не над реальною реалізацією форми. Також поведінка зіткнення - це те, що можна ввести як залежність і делегувати цю залежність.

Продуктивність

Продуктивність завжди критична. Але це не означає, що це завжди проблема. Виконання тесту. Не просто спекулюйте. Жертвоприношення всього іншого в ім'я продуктивності зазвичай не призводить до коду виконання.



+1 за "Ви можете сказати мені, що інші форми ніколи не знадобляться, але я не вірю вам і вам, і ні."
Tulains Córdova

Думка про пікселі нікуди не дінеться, якщо ця програма не стосується малювання фігур, а чисто математичних обчислень. Ця відповідь означає, що ви повинні принести в жертву все, щоб сприйняти об'єктно-орієнтовану чистоту. Він також містить протиріччя: спочатку ви говорите, що ми повинні базувати весь наш дизайн на думці, що нам може знадобитися більше типів фігур у майбутньому, потім ви говорите "ЯГНІ". Нарешті, ви нехтуєте тим, що полегшувати додавання типів часто означає, що складніше додавати операції, що погано, якщо ієрархія типів відносно стабільна, але операції сильно змінюються.
Крістіан

7

В описі проблеми звучить так, як ви повинні використовувати мультиметоди (також багаторазові розсилки), у цьому конкретному випадку - подвійну диспетчеризацію . Перша відповідь була детально розказана про те, як по-справжньому поводитися зі стикаються фігурами в растровому візуалізації, але я вважаю, що ОП хотів «векторного» рішення або, можливо, вся проблема була переформульована з точки зору Shapes, що є класичним прикладом у поясненнях 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 ...

Отже, наступне питання - як отримати підтримку багатометодів у мові програмування.



Так, особливий випадок множинної диспетчери aka Multimethods, доданий до відповіді
Роман Сузі

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

Далі ви повинні домовитись про виклик відповідного методу залежно від форми, яка його викликає. Це можна зробити, використовуючи поліморфізм та шаблон відвідувачів . Для цього ми повинні мати відповідну об'єктну модель. По-перше, всі форми повинні дотримуватися одного інтерфейсу:

    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);
    }
}

Таким чином, вам не потрібно змінювати класи фігур щоразу, коли ви додаєте нову форму, і вам не потрібно перевіряти тип фігури, щоб викликати відповідний метод виявлення зіткнень.

Недолік цього рішення полягає в тому, що якщо ви додасте нову форму, вам доведеться розширити клас ShapeVisitor методом цієї форми (наприклад VisitTriangle(Triangle triangle)), і, отже, вам доведеться реалізувати цей метод у всіх інших відвідувачів. Однак, оскільки це розширення, в тому сенсі, що не змінюються існуючі методи, а додаються лише нові, це не порушує OCP , а накладні витрати коду мінімальні. Також, використовуючи клас ShapeCollisionDetector, ви уникаєте порушення SRP і уникаєте надмірності коду.


5

Ваша основна проблема полягає в тому, що в більшості сучасних мов програмування OO функція перевантаження не працює з динамічним зв'язуванням (тобто тип аргументів функції визначається під час компіляції). Вам знадобиться виклик віртуального методу, який є віртуальним для двох об'єктів, а не лише одного. Такі методи називаються багатопроцесорними . Однак є способи емуляції такої поведінки на таких мовах, як Java, C ++ тощо. Ось тут подвійна відправка дуже зручна.

Основна ідея полягає в тому, що ви використовуєте поліморфізм двічі. Коли стикаються дві форми, ви можете викликати правильний метод зіткнення одного з об'єктів через поліморфізм і передавати інший об’єкт загального типу фігури. У названому методі ви знаєте, чи є цей об'єкт колом, прямокутником чи іншим. Потім ви викликаєте метод зіткнення на об'єкт переданої форми і передаєте йому цей об'єкт. Потім другий виклик знову знаходить правильний тип об'єкта через поліморфізм.

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 ) { ... }
     (...)

На статичному intializer я б використав роздуми, щоб скласти карту методів для здійснення динамічного розпаду методу загального зіткнення (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, що використовуються в ігрових двигунах та пакетах рендерінгу, спрощують, що зменшує потужність комп'ютера, необхідну для візуалізації кожного кадру. Насправді не є фізично точним, але є досить близьким, щоб бути переконливим неозброєним оком.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.