#java #state #multiple-inheritance
#java #состояние #множественное наследование
Вопрос:
(Пожалуйста, обратите внимание: знание карточной игры Magic: The Gathering будет здесь плюсом. Извините, я не знаю, как выразить это проще.)
Я столкнулся с проблемой, используя Java, которую я опишу следующим образом… У меня есть фундаментальный класс под названием Card со всеми следующими атрибутами:
public class Card{
String Name;
String RulesText;
String FlavorText;
String Cost;
int ConvertedCost;
String Rarity;
int Number;
}
Постоянный класс расширяет Card и, в свою очередь, расширяется классами Creature, Planeswalker-а, Artifact, Land и Enchantment. На данный момент среди них только первые два имеют собственные поля:
public class Creature extends Permanent{
int Power;
int Toughness;
}
public class Planeswalker extends Permanent{
int Loyalty;
}
Проблемы:
- На некоторые постоянные объекты могут распространяться методы, влияющие на объект его подклассов, например, как на артефакты И существ, так и на артефакты И земли. Я знаю, что могу использовать интерфейсы для расширения поведения, но я не могу сделать это для расширения состояния, если я не использую абстрактные классы, и в некоторых случаях мне нужен объект с состоянием обоих артефактов и существ.
- На большинство постоянных объектов данного подкласса не может повлиять метод, нацеленный на объект другого подкласса. (Т.Е. метод, нацеленный только на объекты зачарования, не может повлиять на объекты существ.)
Ответ №1:
Я думаю, у вас больше проблем, чем вы думаете. Есть разница между картой и перманентом, карта выполняет эффект, например, вызывает существо, в то время как перманент, полученный в результате вызова, является существом в игре. Карта используется в игре для представления обеих вещей для удобства, но во время программирования вы захотите различать их.
В MTG есть случаи, когда перманенты не связаны напрямую с картой (карты, подобные Hive, генерируют существ, представленных только токенами, у них нет представления карты), поэтому ваша иерархия наследования для этого не подходит. Хотя вам нужно будет поддерживать связь между картой и вызванным ею существом (чтобы вы знали, когда отправлять карту обратно в стопку сброса), я не думаю, что конкретное отношение наследования, которое вы изложили, будет полезным. Вам может понадобиться одна иерархия для карточек и другая для постоянных элементов. Не игнорируйте возможность иерархий (потому что существует множество отношений наследования, которые можно использовать с пользой), просто будьте осторожны, чтобы сохранить отдельные вещи отдельными.
Я бы порекомендовал прочитать сообщение в блоге Стива Йегге об Универсальном шаблоне проектирования, где он рассказывает об использовании шаблона свойств для своей игры (который кажется примерно таким же гибким, как MTG). Очевидно, что многие характеристики вещей в игре изменчивы (включая то, что является существом, а что нет, поскольку есть карты, которые меняют земли на существ), и система для обработки свойств и изменений в них (включая истекающие временные изменения) будет необходима. Сама по себе иерархия классов не будет достаточно гибкой, чтобы приспособиться к разнообразию эффектов в MTG.
Что делает MTG интересным, так это то, что у вас есть эта кажущаяся предметная область (состоящая из таких вещей, как существа, заклинания, артефакты, земли и т.д.), Которая кажется понятной и надежной, но правила позволяют этим вещам меняться непредсказуемым образом (тем более, что всегда можно ввести новые карты). Предметная область реализуется через игровой домен (набор правил, которые управляют предметной областью). Если вы жестко закодируете предметную область в своей реализации (выполняя такие действия, как представление земель или артефактов путем расширения суперкласса или реализации интерфейса), всегда будут проблемы, с которыми вы столкнетесь, которые вы не можете сделать, и исправление этих проблем может быть трудным или невозможным. Прочитайте правила, отметив введенную техническую терминологию (карты, перманенты, токены, эффекты и т.д.), Потому что это реальная область, которую вам нужно реализовать.
Комментарии:
1. Вы правы в отношении случая токена, я согласен. Я думал, что смогу найти способ решить эту проблему, как только я разберусь с этим, но, похоже, это будет не так. Я дам почитать сообщение Стива Йегге. Большое спасибо за совет 🙂
Ответ №2:
Рассмотрите возможность использования StateMachine для этого.
У вас будет CreatureStateMachine, а также ArtifactStateMachine. Объект, реализующий оба интерфейса, может быть передан в любую StateMachine. Каждая StateMachine будет отвечать за управление значениями другого набора атрибутов.
Сам объект будет генерировать события и передавать их в StateMachines, которые будут либо игнорировать, либо обрабатывать их соответствующим образом. StateMachine обновляет состояние объекта, которым управляют, и сам объект знает только, в каком состоянии он находится.
Комментарии:
1. Реализовать то, что вы предлагаете на данный момент, выходит за рамки моего опыта, но, по крайней мере, я знаю, что нужно стремиться к чему-то, что могло бы сработать. Спасибо 🙂
Ответ №3:
У вас возникнут проблемы похуже, когда вы начнете иметь дело с артефактными существами или землями артефактов и так далее.
Вам нужно переосмыслить отношения IS-A в вашей модели. Множественное наследование не будет возможным. Но вы могли бы использовать определенные шаблоны проектирования — такие вещи, как шаблон посетителя или шаблон делегата, могут быть полезны. Эффекты могут использовать шаблон Command. Может быть даже полезно, чтобы членом вашего базового класса были «TypeTags» — список всех допустимых правил таргетинга.
Например, вы можете создать существо (псевдокод):
Creature thing = new Creature("My Name", toughness, power, {artifact,creature,enchantment});
Каждая операция «Эффекта» будет принимать объект Card:
Card doEffect(InCard, validTargets) {
if (validTargets.Intersection(Incard.targets).length > 0) //we have valid targets
return actuallyDoEffect(InCard);
else
return InCard;
}
Ответ №4:
Я вообще не знаю эту игру, но просто руководствуюсь тем, что вы здесь говорите:
Возможно, что многоуровневая иерархия решит ваши проблемы. Например, если и артефакты, и земли имеют некоторое общее поведение, то вместо того, чтобы говорить, что Artifact расширяет Permanent, а Land расширяет Permanent, вы могли бы создать другой класс, назовем его Property , а затем сказать, что Property расширяет Permanent, Artifact расширяет Property, Land расширяет Property. (Я имею в виду «свойство» в смысле «реальной собственности» и «личной собственности», а не свойства объекта. Неважно.) Тогда любые функции или данные, общие как для артефакта, так и для земли, могут находиться в собственности.
Но если есть поведение, общее для артефакта и земли, которое не применимо к существу, и другое поведение, общее для артефакта и существа, которое не применимо к земле, этот подход не будет работать.
Класс Java не может расширять более одного другого класса, но, конечно, он может реализовать любое количество классов. Таким образом, вы могли бы создавать интерфейсы для общего поведения для любого заданного набора объектов, например, возможно, подвижный интерфейс, который был бы реализован Artifact и Creature, и интерфейс свойств, который был бы реализован Artifact и Land и т.д. Но, как я уверен, вы понимаете, это наследует только объявления, а не код. Таким образом, вам в конечном итоге придется реализовывать один и тот же код несколько раз.
Одна вещь, которую я иногда делал, — это создать другой класс для реализации поведения, а затем «реальный» класс просто перенаправляет все в класс утилиты. Итак, если — и снова я не знаю игры, поэтому я просто привожу примеры — и артефакту, и земле нужна функция «купить», вы могли бы создать интерфейс свойств, который расширяют и артефакт, и земля, создайте класс PropertyImplementation, который включает в себя фактический код, и тогда у вас было бы что-то вроде:
public class PropertyImplementation
{
public static void buy(Property property, Buyer buyer, int gold)
{
buyer.purse-=gold;
property.owner=buyer;
... etc, whatever ...
}
}
public interface Property
{
public static void buy(Property property, Buyer buyer, int gold);
}
public class Land extends Permanent implements Property
{
public void buy(Buyer buyer, int gold)
{
PropertyImplementation.buy(this, buyer, gold);
}
... other stuff ...
}
public class Artifact extends Permanent implements Property
{
public void buy(Buyer buyer, int gold)
{
PropertyImplementation.buy(this, buyer, gold);
}
... other stuff ...
}
Менее сложный, но иногда вполне практичный подход заключается в том, чтобы просто выдавать ошибки при выполнении неприменимых вызовов. Нравится:
public class Permanent
{
public void buy(Buyer buyer, int gold)
throws InapplicableException
{
buyer.purse-=gold;
this.owner=buyer;
... etc ...
}
}
public class Plainsman extends Permanent
{
// override
public void buy(Buyer buy, int gold)
throws InapplicableException
{
throw new InapplicableException("You can't buy a Plainsman! That would be slavery!");
}
}
Ответ №5:
Редактировать: Хотя это похоже на проблему наследования, которая выиграла бы от простой реструктуризации, я предоставлю это тем, у кого есть волшебный опыт.