Я читал, что когда вашей программе нужно знать, к какому классу относится объект, обычно это указывает на недостаток дизайна, поэтому я хочу знать, что такое хорошая практика для этого. Я реализую класс 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);
}
}
}
Это плохой дизайн шаблона? Как я могу решить эту проблему без вывода типов подкласса?
источник
Shape
объектами. Ваша логика зависит от внутренних объектов другого объекта, если вы не проверяете столкновение на наличие пограничных точекbool collide(x, y)
(подмножество контрольных точек может быть хорошим компромиссом). В противном случае вам нужно как-то проверить тип - если действительно есть необходимость в абстракциях, тогда созданиеCollision
типов (для объектов в области текущего актера) должно быть правильным подходом.Ответы:
Полиморфизм
Пока вы используете
getType()
или что-то подобное, вы не используете полиморфизм.Я понимаю, что тебе нужно знать, какой у тебя тип. Но любую работу, которую вы хотели бы выполнять, зная, что это действительно нужно отодвинуть в класс. Тогда просто скажи, когда это делать.
Этот принцип называется сказать, не спрашивайте . Следуя этому, вы не будете распространять детали типа текста и создавать логику, которая на них действует. Это превращает класс наизнанку. Лучше хранить это поведение внутри класса, чтобы оно могло меняться при изменении класса.
Инкапсуляция
Вы можете сказать мне, что никакие другие формы никогда не понадобятся, но я не верю вам и вам не следует.
Приятным эффектом последующей инкапсуляции является то, что легко добавлять новые типы, потому что их детали не распространяются на код, в котором они отображаются,
if
и наswitch
логику. Код для нового типа должен быть в одном месте.Система обнаружения столкновений невежественного типа
Позвольте мне показать вам, как я спроектировал бы систему обнаружения столкновений, которая бы работала и работала с любой 2D-формой, не заботясь о типе.
Скажи, что ты должен был это нарисовать. Кажется простым Это все круги. Соблазнительно создать класс круга, который понимает столкновения. Проблема в том, что это заставляет нас задуматься, когда нам нужно 1000 кругов.
Мы не должны думать о кругах. Мы должны думать о пикселях.
Что если я скажу вам, что тот же код, который вы используете для рисования этих парней, - это то, что вы можете использовать, чтобы определить, когда они касаются или даже те, на которые нажимает пользователь.
Здесь я нарисовал каждый круг уникальным цветом (если ваши глаза достаточно хороши, чтобы увидеть черный контур, просто игнорируйте это). Это означает, что каждый пиксель в этом скрытом изображении соответствует тому, что нарисовало его. Хешмап позаботится об этом красиво. Вы действительно можете сделать полиморфизм таким образом.
Это изображение вам никогда не придется показывать пользователю. Вы создаете его с тем же кодом, который нарисовал первый. Просто с разными цветами.
Когда пользователь нажимает на круг, я точно знаю, какой круг, потому что этот цвет имеет только один круг.
Когда я рисую круг поверх другого, я могу быстро прочитать каждый пиксель, который я собираюсь перезаписать, поместив их в набор. Когда я закончил, установил точки для каждого круга, с которым он столкнулся, и теперь мне нужно всего лишь позвонить каждому из них, чтобы уведомить его о столкновении.
Новый тип: прямоугольники
Все это было сделано с кругами, но я спрашиваю вас: с прямоугольниками это будет работать иначе?
Никакие круговые знания не просочились в систему обнаружения. Его не волнует радиус, окружность или центральная точка. Это заботится о пикселях и цвете.
Единственная часть этой системы столкновений, которую необходимо придавить отдельным формам, - это уникальный цвет. Кроме этого формы могут просто думать о рисовании своих форм. Это то, что они хороши в любом случае.
Теперь, когда вы пишете логику столкновения, вам все равно, какой у вас подтип. Вы говорите ему, что он сталкивается, и он говорит вам, что он нашел под формой, которую притворяется рисовать. Не нужно знать тип. А это значит, что вы можете добавлять столько подтипов, сколько вам нужно, без необходимости обновлять код в других классах.
Варианты реализации
Действительно, это не обязательно должен быть уникальный цвет. Это могут быть реальные ссылки на объекты и сохранить уровень косвенности. Но те не выглядят так хорошо, когда нарисованы в этом ответе.
Это всего лишь один пример реализации. Там, безусловно, есть другие. Это должно было показать, что чем ближе вы позволяете этим подтипам формы придерживаться своей единственной ответственности, тем лучше работает вся система. Вероятно, существуют более быстрые и менее ресурсоемкие решения, но если они заставят меня распространять знания о подтипах вокруг, я бы не хотел использовать их даже при повышении производительности. Я бы не использовал их, если бы я явно не нуждался в них.
Двойная отправка
До сих пор я полностью игнорировал двойную отправку . Я сделал это, потому что мог. Пока логике столкновения не важно, какие два типа столкнулись, вам это не нужно. Если вам это не нужно, не используйте его. Если вы думаете, что вам это может понадобиться, отложите заниматься этим как можно дольше. Такое отношение называется ЯГНИ .
Если вы решили, что вам действительно нужны разные виды столкновений, спросите себя, действительно ли n подтипам формы нужно n 2 видов столкновений. До сих пор я работал очень усердно, чтобы упростить добавление другого подтипа формы. Я не хочу портить это реализацией двойной отправки, которая заставляет круги знать, что квадраты существуют.
Сколько видов столкновений существует? Немного спекулируя (опасная вещь) изобретает упругие столкновения (упругие), неупругие (липкие), энергичные (взрывные) и разрушительные (разрушительные). Их может быть больше, но если это меньше, чем n 2, давайте не будем чрезмерно проектировать наши столкновения.
Это означает, что когда моя торпеда поражает что-то, что получает урон, оно не должно ЗНАТЬ, что оно поражает космический корабль. Надо только сказать: «Ха-ха! Ты получил 5 единиц урона».
Вещи, которые наносят ущерб, отправляют сообщения об ущербе вещам, которые принимают сообщения о повреждении. Сделав это, вы можете добавлять новые фигуры, не рассказывая другим фигурам о новой фигуре. В итоге вы распространяетесь только вокруг новых типов столкновений.
Космический корабль может отправить обратно в оцепенение "Ха-ха! Вы получили 100 единиц урона". а также "Вы сейчас застряли в моем корпусе". И торп может отправить назад: «Ну, я готов, так что забудь обо мне».
Ни в коем случае не знает точно, что каждый из них. Они просто знают, как разговаривать друг с другом через интерфейс столкновения.
Конечно, двойная диспетчеризация позволяет вам контролировать вещи более тщательно, чем это, но вы действительно этого хотите ?
Если вы, пожалуйста, хотя бы подумайте о том, чтобы выполнить двойную диспетчеризацию через абстракции того, какие виды столкновений принимает фигура, а не о фактической реализации фигуры. Кроме того, поведение при столкновении - это то, что вы можете внедрить в качестве зависимости и делегировать этой зависимости.
Производительность
Производительность всегда важна. Но это не значит, что это всегда проблема. Тест производительности. Не просто спекулировать. Пожертвование всем остальным во имя исполнения обычно не приводит к коду исполнения.
источник
Описание проблемы звучит так, как будто вы должны использовать Multimethods (иначе Multiple dispatch), в данном конкретном случае - Double dispatch . В первом ответе подробно говорилось о том, как в целом иметь дело со встречными фигурами при растровом рендеринге, но я считаю, что OP хотел «векторное» решение, или, возможно, вся проблема была переформулирована в терминах Shapes, что является классическим примером в объяснениях ООП.
Даже в цитируемой статье в википедии используется та же самая метафора о столкновениях, позвольте мне привести только цитату (Python не имеет встроенных мультиметодов, как некоторые другие языки):
Итак, следующий вопрос - как получить поддержку мультиметодов в вашем языке программирования.
источник
Эта проблема требует редизайна на двух уровнях.
Во-первых, вам нужно извлечь логику для обнаружения столкновения между фигурами из фигур. Это сделано для того, чтобы вы не нарушали OCP каждый раз, когда вам нужно добавить новую фигуру в модель. Представьте, что вы уже определили круг, квадрат и прямоугольник. Затем вы можете сделать это так:
Затем вы должны организовать вызов соответствующего метода в зависимости от формы, которая его вызывает. Вы можете сделать это, используя полиморфизм и шаблон посетителя . Чтобы достичь этого, у нас должна быть соответствующая объектная модель. Во-первых, все фигуры должны соответствовать одному интерфейсу:
Далее у нас должен быть родительский класс посетителя:
Я использую класс вместо интерфейса, потому что мне нужно, чтобы каждый объект посетителя имел атрибут
ShapeCollisionDetector
типа.Каждая реализация
IShape
интерфейса будет создавать экземпляр соответствующего посетителя и вызывать соответствующийAccept
метод объекта, с которым взаимодействует вызывающий объект, например:И конкретные посетители будут выглядеть так:
Таким образом, вам не нужно менять классы фигур каждый раз, когда вы добавляете новую форму, и вам не нужно проверять тип формы, чтобы вызвать соответствующий метод обнаружения столкновений.
Недостаток этого решения заключается в том, что если вы добавляете новую фигуру, вам необходимо расширить класс ShapeVisitor с помощью метода для этой фигуры (например
VisitTriangle(Triangle triangle)
), и, следовательно, вам придется реализовать этот метод для всех других посетителей. Однако, поскольку это расширение, в том смысле, что никакие существующие методы не изменены, а добавлены только новые, это не нарушает OCP , а накладные расходы кода минимальны. Кроме того, используя классShapeCollisionDetector
, вы избегаете нарушения SRP и избегаете избыточности кода.источник
Ваша основная проблема заключается в том, что в большинстве современных языков программирования ОО перегрузка функций не работает с динамическим связыванием (то есть тип аргументов функции определяется во время компиляции). Вам потребуется вызов виртуального метода, который является виртуальным для двух объектов, а не только для одного. Такие методы называются мульти-методами . Однако есть способы эмулировать это поведение в таких языках, как Java, C ++ и т. Д. Именно здесь двойная диспетчеризация оказывается очень полезной.
Основная идея заключается в том, что вы используете полиморфизм дважды. Когда две фигуры сталкиваются, вы можете вызвать правильный метод столкновения одного из объектов посредством полиморфизма и передать другой объект универсального типа фигуры. В вызываемом методе вы узнаете, является ли этот объект кругом, прямоугольником или чем-то еще. Затем вы вызываете метод столкновения для переданного объекта формы и передаете ему объект this . Этот второй вызов затем снова находит правильный тип объекта посредством полиморфизма.
Однако большим недостатком этого метода является то, что каждый класс в иерархии должен знать обо всех братьях и сестрах. Это создает большую нагрузку на обслуживание, если новая форма добавляется позже.
источник
Может быть, это не лучший способ подойти к этой проблеме
Математическое столкновение формы behing является определенным для комбинаций формы. Это означает, что количество необходимых вам подпрограмм является квадратом числа фигур, поддерживаемых системой. Столкновения с фигурами - это не операции с фигурами, а операции, принимающие фигуры в качестве параметров.
Стратегия перегрузки оператора
Если вы не можете упростить основную математическую задачу, я бы порекомендовал подход перегрузки оператора. Что-то типа:
На статическом инициализаторе я бы использовал отражение, чтобы составить карту методов для реализации динамического диспетчера по методу общего столкновения (Shape s1, Shape s2). Статический инициализатор также может иметь логику, чтобы обнаруживать отсутствующие функции столкновения и сообщать о них, отказываясь загружать класс.
Это похоже на перегрузку оператора C ++. В C ++ оператор перегрузки очень запутан, потому что у вас есть фиксированный набор символов, которые вы можете перегрузить. Тем не менее, концепция очень интересна и может быть воспроизведена с помощью статических функций.
Причина, по которой я бы использовал этот подход, состоит в том, что столкновение не является операцией над объектом. Столкновение - это внешняя операция, которая говорит о некотором отношении двух произвольных объектов. Кроме того, статический инициализатор сможет проверить, не пропущена ли какая-либо функция столкновения.
Упростите вашу математическую задачу, если это возможно
Как я уже говорил, количество функций столкновения является квадратом числа типов фигур. Это означает, что в системе только с 20 фигурами вам потребуется 400 процедур, с 21 формой 441 и так далее. Это не просто расширяемо.
Но вы можете упростить свою математику . Вместо расширения функции столкновения вы можете растеризовать или триангулировать каждую фигуру. Таким образом, двигатель столкновения не должен быть расширяемым. Столкновение, Расстояние, Пересечение, Слияние и некоторые другие функции будут универсальными.
Triangulate
Вы заметили, что большинство 3d-пакетов и игр триангулируют все? Это одна из форм упрощения математики. Это относится и к 2D фигурам. Полис можно триангулировать. Круги и сплайны могут быть приближены к полигонам.
Опять же ... у вас будет одна функция столкновения. Ваш класс стал тогда:
И ваши операции:
Проще это не так?
Rasterize
Вы можете растеризовать свою форму, чтобы иметь единственную функцию столкновения.
Растеризация может показаться радикальным решением, но она может быть доступной и быстрой в зависимости от того, насколько точными должны быть ваши столкновения с фигурами. Если они не должны быть точными (как в игре), у вас могут быть изображения с низким разрешением. Большинство приложений не нуждается в абсолютной точности по математике.
Аппроксимации могут быть достаточно хорошими. Суперкомпьютер ANTON для моделирования биологии является примером. Его математика отбрасывает множество квантовых эффектов, которые трудно рассчитать, и до сих пор проведенное моделирование соответствовало экспериментам, проведенным в реальном мире. Модели компьютерной графики PBR, используемые в игровых движках и пакетах рендеринга, делают упрощения, которые уменьшают мощность компьютера, необходимую для рендеринга каждого кадра. На самом деле физически не точен, но достаточно близок, чтобы убедить его невооруженным глазом.
источник