Я работаю над реализацией карточной игры UNO (в основном для запуска симуляции для проверки некоторых домашних правил, но это другая история).
Для тех, кто никогда не играл в нее, это похоже на Crazy Eights. Игроки по очереди играют на карточке на стопке сброса. Карта должна соответствовать номеру или цвету. Также есть карты для розыгрыша, которые заставляют следующего игрока нарисовать две или четыре карты. Это также довольно дружелюбный к дому правила, что делает его довольно интересным.
У меня есть класс Card
со value
и color
карты. Я бы хотел создать DrawCard
который расширяет Card
. Затем, при определении игровой механики, я бы использовал instanceof
чтобы проверить, есть ли у меня DrawCard
.
Я знаю, что этот instanceof
часто проблематичен, но мне кажется, что все в порядке. Обычно карты обрабатываются одинаково, и только несколько специальных карт обрабатываются по-разному, и только в особых обстоятельствах, характерных для типа карты (это похоже на скользкую склонность сказать, хотя...)
Я мог бы просто использовать маркеры в классе Card
(фактически, у каждого типа карты уже есть свое "значение"), но я все равно расширяю Card
, чтобы иметь некоторые методы, которые могут иметь другие типы карт (это не просто средство идентификации). Использование instanceof
представляется мне более общим, поскольку мне не нужно знать, какие значения value
требуют этого особого поведения, и было бы легко, например, добавить карту Draw 8 в текущее содержимое Draw 2 и Draw 4. (Говоря это, меня заставляет задуматься, могу ли я использовать какую-то вложенную переписку или что-то в этом роде, но я не знаю)
Я знаю, что оба решения будут работать (запустите программу). Мне, используя instanceof
чувствует себя лучше, но у меня нет опыта, чтобы понять, все в порядке. Это плохой дизайн?
Короткий ответ: Не совсем приемлемо, так что да - это признак дефицита дизайна. instanceof
почти всегда является запахом, за исключением редких случаев, когда библиотека пытается сделать какую-то помощь, которая является мета или иным образом уродливой, но помогает преодолеть недостаток языка/среды.
С этим мнением этот вопрос является своего рода дубликатом. См. Ответ на:
Когда допустимо использовать instanceof?
Он предлагает использовать шаблон посетителя. Это может быть правдой, если у вас действительно есть два разных типа в системе.
Вам также не нужно использовать шаблон посетителя "явно", а скорее включать его в поток вашей программы. Поэтому рассмотрим интерфейсную карту:
interface Card {
void pick(Player player);
}
class DrawCard implements Card {
void pick(Player player) {
player.draw(value); // assume value passed in e.g. ctor
}
}
onTaken
игрока, в ответ на получение карты, затем «забрать» карту (вы можете переименовать ее в onTaken
или что-то в этом роде, если это имеет больше смысла), но затем заставить реализации карт вызывать методы на игрока, чтобы продолжить ход. Это может означать добавление очков игроку (player.increaseScore) или указание игроку, что он должен делать больше вещей (player.takeMoreCards).
Я бы сказал, вообще лучше избегать instanceof
везде, где это возможно.
Хотя это несколько расплывчато, поскольку вы не публиковали какой-либо фактический код, одним из вариантов в этом типе ситуации было бы реализовать два разных интерфейса, например, ICard
и IDrawCard
а также реализовать класс Card
ICard
а класс DrawCard
оба интерфейса.
Тогда вы сможете дифференцировать класс DrawCard
благодаря своему интерфейсу в сигнатурах функций и т.д. (IDrawCard
на него как на объект типа IDrawCard
).
Редактировать в ответ на комментарий от OP:
Что касается обработки действий после воспроизведения карты, вы можете использовать интерфейсы для переопределения функции, которая обрабатывает действия. Поскольку это контекст для этого, я бы, вероятно, рекомендовал три разных интерфейса:
ICommonCard
ICard
IDrawCard
Где ICommonCard
является универсальным интерфейсом, когда вам нужно то, что, возможно, обрабатывает, имеет некоторые универсальные методы, а два других интерфейса удовлетворяют потребностям этих двух конкретных типов.
Тогда скажите, что у вас есть метод обработчика действий с именем handleAction
, который можно определить следующим образом:
void handleAction(ICard card)
{
// do ICard stuff here
return;
}
void handleAction(IDrawCard card)
{
// do IDrawCard stuff here
return;
}
И затем, когда вы вызываете эту функцию, вы можете передать объект, являющийся типом интерфейса, в перегруженную функцию (один параметр должен иметь переменную-член для каждого типа, и если она будет той или иной, передать тот, который не равен null
в функции).