Як зробити тип даних для чогось, що представляє собою або дві інші речі


16

Фон

Ось актуальна проблема, над якою я працюю: я хочу, щоб представити карти в картковій грі Magic: The Gathering . Більшість карт у грі - це картки звичайного вигляду, але деякі з них розділені на дві частини, кожна зі своєю назвою. Кожна половина цих двоскладових карт трактується як сама картка. Тож для наочності я буду використовувати Cardлише для позначення того, що є або звичайною карткою, або половиною картки з двох частин (іншими словами, щось із лише одним ім’ям).

картки

Отже, у нас є базовий тип, Card. Мета цих об'єктів насправді просто утримувати властивості картки. Вони насправді нічого не роблять самі.

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

Є два підкласи Card, які я дзвоню PartialCard(половина картки з двох частин) та WholeCard(звичайна картка). PartialCardмає два додаткові методи: PartialCard otherPart()і boolean isFirstPart().

Представники

Якщо у мене є колода, вона повинна складатися з WholeCards, а не Cards, як a Cardможе бути PartialCard, і це не мало б сенсу. Тому я хочу об'єкт, який представляє "фізичну карту", тобто те, що може представляти одну WholeCardчи дві PartialCardс. Я орієнтовно називаю цей тип Representativeі Cardмав би метод getRepresentative(). A Representativeне надає майже ніякої прямої інформації на картці (картках), яку вона представляє, вона вказувала б лише на неї / них. Тепер моя блискуча / божевільна / німа ідея (ви вирішите) полягає в тому, що WholeCard успадковує і те, Card і іншеRepresentative . Адже це картки, які представляють себе! WholeCards могли реалізувати getRepresentativeяк return this;.

Що стосується PartialCards, вони не представляють себе, але у них є зовнішній, Representativeякий не є Card, але надає методи доступу до двох PartialCards.

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

Необхідність типового лиття

Оскільки PartialCards і WholeCardsобидва Cards, і зазвичай немає вагомих причин відокремлювати їх, я зазвичай просто працюю Collection<Card>. Тому іноді мені потрібно буде робити це PartialCard, щоб отримати доступ до їх додаткових методів. Зараз я використовую описану тут систему, тому що я дійсно не люблю явні касти. І як Card, Representativeбуло б потрібно бути відлиті або до WholeCardабо Composite, щоб отримати доступ до фактичної CardS , які вони представляють.

Тож просто для підсумків:

  • Тип бази Representative
  • Тип бази Card
  • Тип WholeCard extends Card, Representative(доступ не потрібен, він представляє себе)
  • Введіть PartialCard extends Card(надає доступ до іншої частини)
  • Тип Composite extends Representative(надає доступ до обох частин)

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


1
Чи ефективні картки PartialCards, крім тих, що є дві на фізичну карту? Чи має значення порядок їх відтворення? Не могли б ви просто створити клас "Deck Slot" і "WholeCard" і дозволити DeckSlots мати кілька WholeCards, а потім просто зробити щось на зразок DeckSlot.WholeCards.Play і якщо є 1 або 2, грайте обидва з них, як ніби вони окремі картки? Здається, зважаючи на ваш набагато складніший дизайн, має бути щось, чого мені не вистачає :)
enderland

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

1
Чесно кажучи, я вважаю це смішно складним. Вам здається, що лише модель "це" відносини, що призводить до дуже жорсткої конструкції з жорсткою зв'язкою. Більш цікавим було б, що насправді роблять ці картки?
Даніель Жур

2
Якщо вони "просто об'єкти даних", то моїм інстинктом було б мати один клас, який містить масив об'єктів другого класу, і залишити його при цьому; відсутність успадкування чи інших безглуздих ускладнень. Чи повинні ці два класи бути PlayableCard і DravableCard чи WholeCard та CardPart, я не маю уявлення, тому що я не знаю майже достатньо про те, як працює ваша гра, але я впевнений, що ви можете придумати щось, щоб зателефонувати їм.
Іксрек

1
Яких властивостей вони мають? Чи можете ви навести приклади дій, які використовують ці властивості?
Даніель Жур

Відповіді:


14

Мені здається, вам слід мати такий клас

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

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

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

Не має значення, чи вважаєте ви фізичну та логічну карту тим самим. Не припускайте, що лише тому, що вони є одним фізичним об'єктом, вони повинні бути тим самим об'єктом у вашому коді. Важливим є те, чи прийняття цієї моделі полегшує кодер для читання та запису. Справа в тому, що прийняття простішої моделі, коли кожна фізична карта трактується як колекція логічної картки послідовно, 100% часу, призведе до більш простого коду.


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

2
@KevinKrumwiede, як я бачу, немає жодної концептуальної реальності. Різні люди думають про одне й те саме по-різному, і люди можуть змінити, як вони це думають. Ви можете розглядати фізичні та логічні картки як окремі сутності. Або ви можете розглядати розділені карти як якесь виняток із загального поняття карти. Він не є власне невірним, але той піддається більш спрощеному моделюванню.
Вінстон Еверт

8

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

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

Варіант 1. Поставтеся до неї як до однієї картки, позначеної як Половина A // Половина B , як і на сайті MTG, перелічено Wear // Tear . Але дозвольте своїй Cardорганізації містити N кожного атрибута: ім’я, яке можна відтворити, вартість мани, тип, рідкість, текст, ефекти тощо.

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

Варіант 2. Не всі, що відрізняються від варіанту 1, моделюють його після фізичної реальності. У вас є Cardорганізація, яка представляє фізичну карту . Тоді його мета - утримувати N Playable речей. Цей Playable«s кожен може мати унікальне ім'я, вартість мани, список ефектів, список здібностей і т.д .. А ваш" фізичний " Cardможе мати свій власний ідентифікатор (або ім'я) , який представляє собою з'єднання кожного Playable» s ім'я, так само, як Здається, база даних MTG.

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

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

Я думаю, що будь-який із цих варіантів досить близький до фізичної реальності. І я думаю, що це буде корисно всім, хто дивиться на ваш код. (Як і ваше власне «я» через 6 місяців.)


5

Мета цих об'єктів насправді просто утримувати властивості картки. Вони насправді нічого не роблять самі.

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

Адже це картки, які представляють себе!

ІМХО, це звучить трохи дивно і навіть трохи дивно. Об'єкт типу "Картка" повинен представляти карту. Період.

Я нічого не знаю про Magic: The collection , але я думаю, ви хочете використовувати свої карти аналогічним чином, незалежно від їх фактичної структури: ви хочете відобразити рядкове представлення, ви хочете обчислити значення атаки тощо.

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

  1. Створіть Cardінтерфейс, як ви вже робили.
  2. Створіть ConcreteCard, що реалізує Cardта визначає просту карту обличчя. Не соромтеся ставити поведінку звичайної картки до цього класу.
  3. Створіть CompositeCard, що реалізує Cardта має два додаткові (і апріорі приватні) Card. Давайте називатимемо їх leftCardі rightCard.

Елегантність підходу полягає в тому, що CompositeCardмістить дві картки, які самі по собі можуть бути або ConcreteCard, або CompositeCard. У вашій грі leftCardі rightCard, ймовірно, систематично будуть ConcreteCard, але дизайн шаблону дозволяє безкоштовно проектувати композиції вищого рівня. Ваша маніпуляція з картою не враховуватиме реальний тип ваших карток, і тому вам не потрібні такі речі, як кастинг до підкласу.

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

Якщо ви впевнені, що а CompositeCardзавжди матиме два звичайних Cards, ви можете зберегти цю ідею та просто використовувати ConcreateCardяк тип для leftCardта rightCard.


Ви говорите про композитному малюнку , але насправді , так як ви тримаєте дві посилання з Cardв CompositeCardви реалізуєте шаблон декоратора . Я також рекомендую ОП використовувати це рішення, декоратор - це шлях!
Виявлено

Я не розумію, чому наявність двох екземплярів картки робить мій клас декоратором. Відповідно до вашого власного посилання, декоратор додає функції до одного об’єкта і сам є екземпляром того ж класу / інтерфейсу, що й цей об’єкт. Хоча, за вашим іншим посиланням, композит дозволяє володіти декількома об'єктами одного класу / інтерфейсу. Але в кінцевому рахунку слова не мають значення, і лише ідея чудова.
mgoeminne

@Spotted Це, безумовно, не шаблон декоратора, оскільки цей термін використовується в Java. Шаблон декоратора реалізується шляхом переосмислення методів на окремому об'єкті, створюючи анонімний клас, унікальний для цього об’єкта.
Кевін Крумвієде,

@KevinKrumwiede до тих пір, CompositeCardпоки не виставляє додаткових методів, CompositeCardє лише декоратором.
Виявлено

"... Шаблон дизайну дозволяє безкоштовно розробляти композиції вищого рівня" - ні, це не приходить безкоштовно , навпаки - це стосується ціни на рішення, яке складніше, ніж потрібно для вимог.
Doc Brown

3

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

Можливо, Card і Playable мають ефект.


На жаль, я не граю в ці карти. Вони справді просто об’єкти даних, до яких можна поставити запит. Якщо ви хочете структуру даних колоди, ви хочете Список <WholeCard> або щось подібне. Якщо ви хочете здійснити пошук зелених миттєвих карток, вам потрібно Список <Картка>.
codebreaker

Ага, гаразд. Тож не дуже стосується вашої проблеми. Чи потрібно видалити?
Девіслор

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 and array замість двох параметрів. Це має сенс, якщо багатогранні картки трактуються по-різному, але кількість облич вище однієї обробляється однаково.

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


1
Я думаю, що це може бути легко розширено до іншого виду двох облицьованих карток (спереду та ззаду, а не поруч). ++
RubberDuck

Для подальшої розробки використання Visitor для використання методів, характерних для підкласу, іноді називається Multiple Dispatch. Подвійна відправка може бути цікавим рішенням проблеми.
mgoeminne

1
Я голосую за те, що я вважаю, що модель відвідувачів додає непотрібної складності та зв'язку коду. Оскільки є альтернатива (див. Відповідь mgoeminne), я б не використовував її.
Виявлено

@Spotted Відвідувач - це складний зразок, і я також розглядав складний візерунок під час написання відповіді. Причина, по якій я пішов з відвідувачем, полягає в тому, що ОП хоче ставитися до подібних речей по-різному, а не до різних речей аналогічно. Якщо карти мали більше поведінки і використовувались для гри, то складений зразок дозволяє поєднувати дані для отримання єдиного стату. Вони є лише пакетами даних, хоча, можливо, використовується відображення картки, і в цьому випадку отримання відокремленої інформації видається більш корисним, ніж простий складений агрегатор.
cbojar
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.