Как создать тип данных для чего-то, что представляет собой или две другие вещи

16

Фон

Вот настоящая проблема, над которой я работаю: я хочу представить карты в карточной игре Magic: The Gathering . Большинство карт в игре выглядят нормально, но некоторые из них разделены на две части, каждая со своим именем. Каждая половина этих двухкомпонентных карт рассматривается как сама карта. Поэтому для ясности я буду использовать Cardтолько для обозначения чего-то, что является либо обычной картой, либо половиной карты, состоящей из двух частей (другими словами, чем-то, имеющим только одно имя).

карты

Итак, у нас есть базовый тип, карта. Цель этих объектов на самом деле просто держать свойства карты. Они на самом деле ничего не делают сами.

interface Card {
    String name();
    String text();
    // etc
}

Есть два подкласса Card, которые я называю PartialCard(половина карты из двух частей) и WholeCard(обычная карта). PartialCardимеет два дополнительных метода: PartialCard otherPart()и boolean isFirstPart().

Представители

Если у меня есть колода, она должна состоять из WholeCards, а не Cards, поскольку a Cardможет быть a PartialCard, и это не имеет смысла. Поэтому я хочу, чтобы объект представлял «физическую карту», ​​то есть что-то, что может представлять один WholeCardили два PartialCardэлемента s. Я предварительно вызываю этот тип Representative, и у Cardменя будет метод getRepresentative(). A Representativeпочти не предоставит прямой информации о картах, которые он представляет, он только укажет на нее / них. Теперь моя блестящая / сумасшедшая / глупая идея (вы решаете), что WholeCard наследует от обоих Card и Representative. В конце концов, это карты, которые представляют себя! WholeCards может реализовать getRepresentativeкак return this;.

Что касается PartialCards, они не представляют себя, но у них есть внешнее Representative, которое не является Card, но предоставляет методы для доступа к двум PartialCards.

Я думаю, что эта иерархия типов имеет смысл, но она сложная. Если мы рассматриваем Cards как «концептуальные карты», а Representatives как «физические карты», то большинство карт оба! Я думаю, вы могли бы поспорить, что физические карты на самом деле содержат концептуальные карты, и что это не одно и то же , но я бы сказал, что это так.

Необходимость литья типов

Поскольку PartialCards и WholeCardsесть оба Cards, и обычно нет веских причин для их разделения, я бы обычно просто работал с ними Collection<Card>. Поэтому иногда мне нужно приводить PartialCards, чтобы получить доступ к их дополнительным методам. Прямо сейчас я использую систему, описанную здесь, потому что я действительно не люблю явные приведения. И, как Card, Representativeбудет необходимо привести либо к WholeCardили Composite, чтобы получить доступ к фактическим Cards, которые они представляют.

Так что просто для резюме:

  • Базовый тип Representative
  • Базовый тип Card
  • Тип WholeCard extends Card, Representative(доступ не требуется, он представляет себя)
  • Тип PartialCard extends Card(дает доступ к другой части)
  • Тип Composite extends Representative(дает доступ к обеим частям)

Это безумие? Я думаю, что это действительно имеет большой смысл, но я, честно говоря, не уверен.

нарушитель закона
источник
1
Являются ли карты PartialCards эффективными, кроме двух карт на физическую карту? Имеет ли значение порядок их воспроизведения? Не могли бы вы просто создать классы «Слот для колоды» и «WholeCard» и позволить DeckSlots иметь несколько карточек WholeCards, а затем просто сделать что-то вроде DeckSlot.WholeCards.Play, и, если их 1 или 2, сыграйте их обоих, как если бы они были отдельными карты? Кажется, учитывая ваш гораздо более сложный дизайн, я должен что-то упустить :)
enderland
Я действительно не чувствую, что могу использовать полиморфизм между целыми картами и частичными картами, чтобы решить эту проблему. Да, если бы я хотел функцию «игры», то я мог бы реализовать это легко, но проблема в том, что неполные карты и целые карты имеют разные наборы атрибутов. Мне действительно нужно иметь возможность рассматривать эти объекты как то, что они есть, а не только как их базовый класс. Если нет лучшего решения этой проблемы. Но я обнаружил, что полиморфизм плохо сочетается с типами, которые имеют общий базовый класс, но имеют разные методы, если это имеет смысл.
код
1
Честно говоря, я думаю, что это нелепо сложно. Вы, кажется, только модель "это" отношения, что приводит к очень жесткой конструкции с жесткой связью. Интересно было бы, что на самом деле делают эти карты?
Даниэль Жур
2
Если они «просто объекты данных», то мой инстинкт должен иметь один класс, который содержит массив объектов второго класса, и оставить все как есть; нет наследства или других бессмысленных осложнений. Должны ли эти два класса быть PlayableCard и DrawableCard или WholeCard и CardPart, я понятия не имею, потому что я не знаю достаточно о том, как работает ваша игра, но я уверен, что вы можете придумать, как их вызвать.
Иксрек,
1
Какие свойства они держат? Можете ли вы привести примеры действий, которые используют эти свойства?
Даниэль Жур

Ответы:

14

Мне кажется, у вас должен быть такой класс

class PhysicalCard {
    List<LogicalCard> getLogicalCards();
}

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

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

Неважно, думаете ли вы, что физическая и логическая карты - это одно и то же. Не думайте, что только потому, что они являются одним и тем же физическим объектом, они должны быть одним и тем же объектом в вашем коде. Важно то, облегчит ли принятие этой модели кодировщик для чтения и записи. Дело в том, что принятие более простой модели, в которой каждая физическая карта обрабатывается как набор логических карт последовательно, 100% времени, приведет к более простому коду.

Уинстон Эверт
источник
2
Проголосовал, потому что это лучший ответ, хотя и не по указанной причине. Это лучший ответ не потому, что он прост, а потому, что физические карты и концептуальные карты не одно и то же, и это решение правильно моделирует их взаимосвязь. Это правда, что хорошая модель ООП не всегда отражает физическую реальность, но она всегда должна отражать концептуальную реальность. Простота, конечно, хороша, но она отходит на задний план к концептуальной правильности.
Кевин Крумвиде,
2
@KevinKrumwiede, на мой взгляд, нет единой концептуальной реальности. Разные люди думают об одном и том же по-разному, и люди могут изменить свое мнение об этом. Вы можете думать о физических и логических картах как об отдельных объектах. Или вы можете думать о разделенных картах как о каком-то исключении из общего понятия карты. Ни один из них не является по сути неправильным, но он поддается простому моделированию.
Уинстон Эверт
8

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

Я бы предложил одну из двух альтернатив:

Вариант 1. Рассматривайте его как отдельную карту, обозначенную как Half A // Half B , как списки сайтов MTG Wear // Tear . Но разрешите вашей Cardсущности содержать N каждого атрибута: имя для игры, стоимость маны, тип, редкость, текст, эффекты и т. Д.

interface Card {
  List<String> Names();
  List<ManaCost> Costs();
  List<CardTypes> Types();
  /* etc. */
}

Вариант 2. Не все, что отличается от варианта 1, смоделируйте его в соответствии с физической реальностью. У вас есть Cardобъект, который представляет физическую карту . И его цель состоит в том, чтобы держать N Playable вещей. У Playableкаждого из них может быть свое имя, мана-стоимость, список эффектов, список способностей и т. Д. А у вашего «физического» Cardможет быть свой собственный идентификатор (или имя), который является составной частью имени каждого Playable, во многом как база данных MTG, кажется, делает.

interface Card {
  String Name();
  List<Playable> Playables();
}

interface Playable {
  String Name();
  ManaCost Cost();
  CardType Type();
  /* etc. */
}

Я думаю, что любой из этих вариантов довольно близок к физической реальности. И я думаю, что это будет полезно для всех, кто смотрит на ваш код. (Как и ты сам через 6 месяцев.)

svidgen
источник
5

Цель этих объектов на самом деле просто держать свойства карты. Они на самом деле ничего не делают сами.

Это предложение является признаком того, что в вашем дизайне что-то не так: в ООП каждый класс должен иметь ровно одну роль, а отсутствие поведения выявляет потенциальный класс данных , который является неприятным запахом в коде.

В конце концов, это карты, которые представляют себя!

ИМХО, это звучит немного странно, и даже немного странно. Объект типа «Карта» должен представлять собой карту. Период.

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

Для описываемой проблемы я бы порекомендовал Composit Design Pattern , несмотря на то, что этот DP обычно представлен для решения более общей проблемы:

  1. Создайте Cardинтерфейс, как вы уже сделали.
  2. Создайте ConcreteCard, который реализует Cardи определяет простую лицевую карту. Не стесняйтесь ставить поведение обычной карты в этом классе.
  3. Создайте объект CompositeCard, который реализует Cardи имеет два дополнительных (и априорных частных) Card. Позвоним им leftCardи rightCard.

Элегантность этого подхода состоит в том, что a CompositeCardсодержит две карты, которые могут быть либо ConcreteCard, либо CompositeCard. В вашей игре, leftCardи rightCard, вероятно, будут систематически ConcreteCard, но Design Pattern позволяет вам создавать композиции более высокого уровня бесплатно, если хотите. Ваши манипуляции с картами не будут учитывать реальный тип ваших карт, и поэтому вам не нужны такие вещи, как приведение к подклассу.

CompositeCardCardКонечно, необходимо реализовать методы, указанные в , и делать это, принимая во внимание тот факт, что такая карта состоит из 2 карт (плюс, если хотите, что-то специфическое для самой CompositeCardкарты. Например, вы можете захотеть следующая реализация:

public class CompositeCard implements Card
{ 
   private final Card leftCard, rightCard;
   private final double factor;

   @Override // Defined in Card
   public double attack(Player p){
      return factor * (leftCard.attack(p) + rightCard.attack(p));
   }

   @Override // idem
   public String name()
   {
       return leftCard.name() + " combined with " + rightCard.name();
   }

   ...
}

Делая это, вы можете использовать CompositeCardточно так же, как вы делаете для любого Card, и конкретное поведение скрыто благодаря полиморфизму.

Если вы уверены, что a CompositeCardвсегда будет содержать два нормальных Cards, вы можете сохранить идею и просто использовать ConcreateCardкак тип для leftCardи rightCard.

mgoeminne
источник
Вы говорите о составном шаблоне , но на самом деле, поскольку вы держите две ссылки CardвCompositeCard вы реализуете шаблон декоратора . Я также рекомендую ОП использовать это решение, декоратор это путь!
Пятница,
Я не понимаю, почему наличие двух экземпляров Card делает мой класс декоратором. Согласно вашей собственной ссылке, декоратор добавляет функции к одному объекту и сам является экземпляром того же класса / интерфейса, что и этот объект. В то время как, согласно вашей другой ссылке, композит позволяет владеть несколькими объектами одного класса / интерфейса. Но в конечном счете слова не имеют значения, и только идея хороша.
mgoeminne
@Spotted Это определенно не шаблон декоратора, так как этот термин используется в Java. Шаблон декоратора реализуется путем переопределения методов для отдельного объекта, создавая анонимный класс, уникальный для этого объекта.
Кевин Крумвиде,
@KevinKrumwiede, пока CompositeCardне предоставляет дополнительные методы, CompositeCardявляется только декоратором.
Пятнистый
«... Design Pattern позволяет вам создавать композиции более высокого уровня бесплатно» - нет, это не бесплатно , а наоборот - это цена решения, которое сложнее, чем необходимо для требований.
Док Браун
3

Может быть, все это Карта, когда она находится в колоде или на кладбище, и когда вы разыгрываете ее, вы создаете Существо, Землю, Чары и т. Д. Из одного или нескольких объектов Карты, каждый из которых реализует или расширяет Воспроизводимый. Затем составной становится одиночным играбельным, конструктор которого берет две частичные карты, а карта с кикером становится играбельным, конструктор которого принимает аргумент маны. Тип отражает то, что вы можете сделать с ним (нарисовать, заблокировать, рассеять, нажать) и что может повлиять на это. Или играбельная - это просто карта, которую нужно аккуратно вернуть (потерять все бонусы и фишки, разбить ее), когда вывести из игры, если действительно полезно использовать тот же интерфейс для вызова карты и прогнозирования ее действия.

Возможно, карты и играбельные имеют эффект.

Davislor
источник
К сожалению, я не играю в эти карты. На самом деле это просто объекты данных, к которым можно обращаться. Если вам нужна структура данных колоды, вам нужен List <WholeCard> или что-то еще. Если вы хотите выполнить поиск зеленых мгновенных карт, вам нужен список <Card>.
код
Ах хорошо. Не очень важно для вашей проблемы. Должен ли я удалить?
Дэвислор
3

Шаблон посетителя классический метод для извлечения скрытой информации типа. Мы можем использовать это (небольшое изменение здесь) здесь, чтобы различать два типа, даже когда они хранятся в переменных более высокой абстракции.

Давайте начнем с этой более высокой абстракции, Cardинтерфейса:

public interface Card {
    public void accept(CardVisitor visitor);
}

Там может быть немного больше поведения на Card интерфейсе , но большинство методов получения свойств переходят в новый класс CardProperties:

public class CardProperties {
    // property methods, constructors, etc.

    String name();
    String text();
    // ...
}

Теперь мы можем иметь SimpleCard представить целую карту с одним набором свойств:

public class SimpleCard implements Card {
    private CardProperties properties;

    // Constructors, ...

    @Override
    public void accept(CardVisitor visitor) {
        visitor.visit(properties);
    }
}

Мы видим, как CardPropertiesи что еще предстоит написатьCardVisitor . Давайте сделаем так, CompoundCardчтобы представить карту с двумя лицами:

public class CompoundCard implements Card {
    private CardProperties firstFaceProperties;
    private CardProperties secondFaceProperties;

    // Constructors, ...

    public void accept(CardVisitor visitor) {
        visitor.visit(firstFaceProperties, secondFaceProperties);
    }
}

CardVisitorНачинает появляться. Давайте попробуем написать этот интерфейс сейчас:

public interface CardVisitor {
    public void visit(CardProperties properties);
    public void visit(CardProperties firstFaceProperties, CardProperties secondFaceProperties);
}

(Это первая версия интерфейса на данный момент. Мы можем внести улучшения, которые будут обсуждаться позже.)

Теперь мы уточнили все части. Теперь нам просто нужно собрать их вместе:

List<Card> cards = new LinkedList<>();
cards.add(new SimpleCard(new CardProperties(/* ... */)));
cards.add(new CompoundCard(new CardProperties(/* ... */), new CardProperties(/* ... */)));

 for(Card card : cards) {
     card.accept(new CardVisitor() {
         @Override
         public void visit(CardProperties properties) {
             // Do something for simple cards with a single face
         }

         public void visit(CardProperties firstFaceProperties, CardProperties secondFaceProperties) {
             // Do something else for compound cards with two faces
         }
     });
 }

Среда выполнения будет обрабатывать отправку к правильной версии #visitметода посредством полиморфизма, а не пытаться его сломать.

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


Мы можем использовать классы такими, какие они есть сейчас, но мы можем внести некоторые улучшения в CardVisitor интерфейс. Например, может наступить момент, когда у Cards может быть три, четыре или пять лиц. Вместо того, чтобы добавлять новые методы для реализации, мы могли бы просто использовать второй метод take и array вместо двух параметров. Это имеет смысл, если к многогранным картам относятся по-разному, но количество лиц выше одной считается одинаковым.

Мы могли бы также преобразовать CardVisitorв абстрактный класс вместо интерфейса, и иметь пустые реализации для всех методов. Это позволяет нам реализовывать только интересующее нас поведение (возможно, нас интересуют только односторонние Card). Мы также можем добавлять новые методы, не заставляя каждый существующий класс реализовывать эти методы или не компилировать их.

cbojar
источник
1
Я думаю, что это может быть легко расширено на другой вид двухсторонней карты (спереди и сзади вместо бок о бок). ++
RubberDuck
Для дальнейшего исследования использование Visitor для использования методов, специфичных для подкласса, иногда называется Multiple Dispatch. Двойная отправка может быть интересным решением проблемы.
mgoeminne
1
Я отказываюсь, потому что я думаю, что шаблон посетителя добавляет ненужную сложность и связь с кодом. Поскольку альтернатива доступна (см. Ответ mgoeminne), я бы не стал ее использовать.
Пятно,
@Spotted Посетитель - сложный шаблон, и я также учитывал составной шаблон при написании ответа. Причина, по которой я пошел с посетителем, состоит в том, что ОП хочет по-разному относиться к сходным вещам, а не к разным. Если карты имели большее поведение и использовались для игрового процесса, то комбинированный шаблон позволяет объединять данные для получения единой статистики. Это всего лишь пакеты данных, которые, возможно, используются для отображения карты, и в этом случае получение отдельной информации кажется более полезным, чем простой составной агрегатор.
cbojar