Jak utworzyć typ danych dla czegoś, co reprezentuje siebie lub dwie inne rzeczy

16

tło

Oto faktyczny problem, nad którym pracuję: chcę sposób na reprezentowanie kart w grze karcianej Magic: The Gathering . Większość kart w grze to normalnie wyglądające karty, ale niektóre z nich są podzielone na dwie części, każda z własną nazwą. Każda połowa tych dwuczęściowych kart jest traktowana jak karta. Dla jasności użyję Cardtylko odniesienia do czegoś, co jest albo zwykłą kartą, albo połową dwuczęściowej karty (innymi słowy, czymś o tylko jednej nazwie).

karty

Mamy więc typ podstawowy, Card. Celem tych obiektów jest po prostu zachowanie właściwości karty. Tak naprawdę nic nie robią.

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

Istnieją dwie podklasy Card, które nazywam PartialCard(połowa dwuczęściowej karty) i WholeCard(zwykła karta). PartialCardma dwie dodatkowe metody: PartialCard otherPart()i boolean isFirstPart().

Przedstawiciele

Jeśli mam talię, powinna ona składać się z WholeCards, a nie Cards, ponieważ Cardmoże to być a PartialCard, i to nie miałoby sensu. Chcę więc obiektu reprezentującego „fizyczną kartę”, czyli coś, co może reprezentować jeden WholeCardlub dwa PartialCards. Wstępnie nazywam ten typ Representativei Cardmam metodę getRepresentative(). A Representativenie podałby prawie żadnych bezpośrednich informacji na temat karty, którą reprezentuje, wskazywałby tylko na niego / nich. Teraz moim genialnym / szalonym / głupim pomysłem (ty decydujesz) jest to, że WholeCard dziedziczy po obu Card i Representative. W końcu są to karty, które przedstawiają się! WholeCards mogą implementować getRepresentativejako return this;.

Jeśli chodzi o PartialCards, nie reprezentują siebie, ale mają zewnętrzny, Representativektóry nie jest Card, ale zapewnia metody dostępu do dwóch PartialCard.

Myślę, że ta hierarchia typów ma sens, ale jest skomplikowana. Jeśli myślimy o Card„kartach pojęciowych” i Representative„kartach fizycznych”, cóż, większość kart to oba! Myślę, że można argumentować, że karty fizyczne faktycznie zawierają karty koncepcyjne i że to nie to samo , ale twierdzę, że tak.

Konieczność odlewania typu

Ponieważ PartialCards i WholeCardsoba są Card, i zwykle nie ma dobrego powodu, aby je rozdzielić, normalnie po prostu pracowałbym Collection<Card>. Czasami więc musiałbym przesyłać PartialCard, aby uzyskać dostęp do ich dodatkowych metod. Obecnie używam opisanego tutaj systemu, ponieważ tak naprawdę nie lubię jawnych rzutów. I tak Card, Representativetrzeba by rzucić na albo, WholeCardalbo Composite, aby uzyskać dostęp do rzeczywistych Cards, które reprezentują.

Podsumowując:

  • Typ podstawowy Representative
  • Typ podstawowy Card
  • Wpisz WholeCard extends Card, Representative(nie wymaga dostępu, reprezentuje się)
  • Typ PartialCard extends Card(daje dostęp do innej części)
  • Typ Composite extends Representative(daje dostęp do obu części)

Czy to jest szalone? Myślę, że to naprawdę ma sens, ale szczerze mówiąc, nie jestem pewien.

łamacz kodów
źródło
1
Czy karty częściowe skutecznie działają inaczej niż na dwie karty fizyczne? Czy kolejność grania ma znaczenie? Czy nie możesz po prostu stworzyć klasy „Slot na talię” i „WholeCard” i pozwolić, aby DeckSlots posiadało wiele WholeCards, a następnie po prostu zrobić coś takiego jak DeckSlot.WholeCards.Play, a jeśli jest ich 1 lub 2, zagraj obie z nich tak, jakby były osobne karty? Wydaje się, że biorąc pod uwagę twój znacznie bardziej skomplikowany projekt, coś musi mi brakować :)
enderland
Naprawdę nie wydaje mi się, żebym mógł użyć polimorfizmu między całymi i częściowymi kartami, aby rozwiązać ten problem. Tak, gdybym chciał funkcji „grać”, to mógłbym ją łatwo wdrożyć, ale problemem są częściowe karty, a całe karty mają różne zestawy atrybutów. Naprawdę muszę widzieć te obiekty jako takie, jakie są, a nie tylko ich klasę podstawową. Chyba że istnieje lepsze rozwiązanie tego problemu. Odkryłem jednak, że polimorfizm nie miesza się dobrze z typami, które mają wspólną klasę podstawową, ale mają różne metody, jeśli ma to sens.
codebreaker
1
Szczerze mówiąc, myślę, że jest to absurdalnie skomplikowane. Wydaje się, że modelujesz tylko relacje „to”, co prowadzi do bardzo sztywnej konstrukcji z ciasnym połączeniem. Bardziej interesujące byłoby to, co faktycznie robią te karty?
Daniel Jour
2
Jeśli są to „tylko obiekty danych”, to moim instynktem byłoby posiadanie jednej klasy zawierającej tablicę obiektów drugiej klasy i pozostawienie tego w tym miejscu; bez dziedziczenia lub innych bezcelowych komplikacji. Czy te dwie klasy powinny być PlayableCard i DrawableCard lub WholeCard i CardPart, nie mam pojęcia, ponieważ nie wiem wystarczająco dużo o tym, jak działa twoja gra, ale jestem pewien, że możesz wymyślić coś, co można by nazwać.
Ixrec
1
Jakie posiadają właściwości? Czy możesz podać przykłady działań korzystających z tych właściwości?
Daniel Jour

Odpowiedzi:

14

Wydaje mi się, że powinieneś mieć taką klasę

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

Kod dotyczący karty fizycznej może radzić sobie z klasą karty fizycznej, a kod dotyczący karty logicznej może sobie z tym poradzić.

Myślę, że można argumentować, że karty fizyczne faktycznie zawierają karty konceptualne i że to nie to samo, ale twierdzę, że tak.

Nie ma znaczenia, czy uważasz, że karta fizyczna i logiczna są tym samym. Nie zakładaj, że tylko dlatego, że są tym samym obiektem fizycznym, powinny być tym samym obiektem w twoim kodzie. Ważne jest, czy przyjęcie tego modelu ułatwia koderowi czytanie i pisanie. Faktem jest, że przyjęcie prostszego modelu, w którym każda karta fizyczna jest konsekwentnie traktowana jako zbiór kart logicznych, w 100% przypadków spowoduje uproszczenie kodu.

Winston Ewert
źródło
2
Głosowano, ponieważ jest to najlepsza odpowiedź, choć nie z podanego powodu. To najlepsza odpowiedź nie dlatego, że jest prosta, ale dlatego, że karty fizyczne i karty koncepcyjne nie są tym samym, a to rozwiązanie poprawnie modeluje ich związek. To prawda, że ​​dobry model OOP nie zawsze odzwierciedla rzeczywistość fizyczną, ale zawsze powinien odzwierciedlać rzeczywistość konceptualną. Prostota jest oczywiście dobra, ale ustępuje miejsca poprawności pojęciowej.
Kevin Krumwiede,
2
@KevinKrumwiede, jak widzę, nie ma jednej rzeczywistości konceptualnej. Różni ludzie myślą o tym samym na różne sposoby, a ludzie mogą zmienić sposób, w jaki o tym myślą. Karty fizyczne i logiczne można traktować jako osobne jednostki. Lub możesz pomyśleć o podzielonych kartach jako pewnego rodzaju wyjątku od ogólnego pojęcia karty. Żadna z nich nie jest z natury niepoprawna, ale umożliwia prostsze modelowanie.
Winston Ewert,
8

Mówiąc wprost, uważam, że proponowane rozwiązanie jest zbyt restrykcyjne, zbyt zniekształcone i niepowiązane z rzeczywistością fizyczną to modele, z niewielką przewagą.

Sugerowałbym jedną z dwóch alternatyw:

Opcja 1. Traktuj ją jako pojedynczą kartę, oznaczoną jako Połowa A // Połowa B , jak na stronach MTG Wear / Tear . Ale pozwól swojej Cardistocie zawierać N każdego atrybutu: grywalna nazwa, koszt many, typ, rzadkość, tekst, efekty itp.

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

Opcja 2. Nie wszystko, co różni się od opcji 1, modeluj ją zgodnie z rzeczywistością fizyczną. Masz Cardbyt reprezentujący fizyczną kartę . I celem jest trzymanie N Playable rzeczy. Te Playable„s każdy może mieć odrębną nazwę, koszt many, listę działań, wykaz umiejętności, etc .. A twoja«fizyczny» Cardmoże mieć swój własny identyfikator (lub nazwę), który jest związkiem o każdy Playable” s nazwy, podobnie jak wydaje się, że baza danych MTG działa.

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

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

Myślę, że jedna z tych opcji jest bardzo zbliżona do rzeczywistości fizycznej. I myślę, że będzie to korzystne dla każdego, kto spojrzy na twój kod. (Jak twoje własne ja za 6 miesięcy.)

svidgen
źródło
5

Celem tych obiektów jest po prostu zachowanie właściwości karty. Tak naprawdę nic nie robią.

To zdanie jest znakiem, że coś jest nie tak w twoim projekcie: w OOP każda klasa powinna mieć dokładnie jedną rolę, a brak zachowania ujawnia potencjalną klasę danych , która jest nieprzyjemnym zapachem w kodzie.

W końcu są to karty, które przedstawiają się!

IMHO, to brzmi trochę dziwnie, a nawet trochę dziwnie. Obiekt typu „Karta” powinien reprezentować kartę. Kropka.

Nic nie wiem o Magic: the gathering , ale myślę, że chcesz używać swoich kart w podobny sposób, niezależnie od ich faktycznej struktury: chcesz wyświetlić reprezentację ciągu, chcesz obliczyć wartość ataku itp.

W przypadku opisanego problemu zaleciłbym wzorzec projektowania kompozytu , mimo że ten DP jest zazwyczaj przedstawiany w celu rozwiązania bardziej ogólnego problemu:

  1. Stwórz Card interfejs, tak jak już to zrobiłeś.
  2. Utwórz ConcreteCard, który implementuje Cardi definiuje prostą kartę twarzy. Nie wahaj się postawić normalnego zachowania w tej klasie karty.
  3. Utwórz CompositeCard, który implementuje Cardi ma dwa dodatkowe (i a priori prywatne) Card. Zadzwońmy do nich leftCardi rightCard.

Elegancja tego podejścia polega na tym, że a CompositeCardzawiera dwie karty, które same mogą być kartami betonowymi lub kompozytowymi. W twojej grze, leftCardi rightCardprawdopodobnie będzie to systematycznie ConcreteCards, ale Wzorzec Projektu pozwala ci projektować kompozycje wyższego poziomu za darmo, jeśli chcesz. Manipulacja kartami nie będzie uwzględniać prawdziwego rodzaju kart, dlatego nie potrzebujesz takich rzeczy, jak rzutowanie do podklasy.

CompositeCardmuszą wdrożyć metody określone w Card, oczywiście, i zrobią to, biorąc pod uwagę fakt, że taka karta składa się z 2 kart (plus, jeśli chcesz, coś specyficznego dla CompositeCardsamej karty. Na przykład możesz chcieć następujące wdrożenie:

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();
   }

   ...
}

Robiąc to, możesz użyć CompositeCarddokładnie tego, co robisz dla każdego Card, a konkretne zachowanie jest ukryte dzięki polimorfizmowi.

Jeśli masz pewność, CompositeCardże zawsze będzie zawierać dwa normalne Cards, możesz zachować pomysł i po prostu użyć go ConcreateCardjako typu dla leftCardi rightCard.

mgoeminne
źródło
Mówisz o kompozytowej strukturze , ale w rzeczywistości, ponieważ jesteś trzyma dwa odniesienia Cardw CompositeCardjesteś realizacji wzorzec dekoratora . Polecam również OP, aby skorzystać z tego rozwiązania, dekorator to droga!
Zauważył
Nie rozumiem, dlaczego posiadanie dwóch instancji kart czyni moją klasę dekoratorem. Według własnego łącza dekorator dodaje funkcje do pojedynczego obiektu i sam jest instancją tej samej klasy / interfejsu niż ten obiekt. Podczas gdy, zgodnie z twoim drugim linkiem, kompozyt pozwala na posiadanie wielu obiektów tej samej klasy / interfejsu. Ale ostatecznie słowa nie mają znaczenia i tylko pomysł jest świetny.
mgoeminne,
@Spotted To zdecydowanie nie jest wzorzec dekoratora, ponieważ termin ten jest używany w Javie. Wzorzec dekoratora jest implementowany przez nadpisywanie metod na pojedynczym obiekcie, tworząc anonimową klasę, która jest unikalna dla tego obiektu.
Kevin Krumwiede,
@KevinKrumwiede, o ile CompositeCardnie ujawnia dodatkowych metod, CompositeCardjest jedynie dekoratorem.
Zauważył
„... Wzorzec projektowy pozwala projektować kompozycje wyższego poziomu za darmo” - nie, nie przychodzi za darmo , wręcz przeciwnie - przychodzi za cenę rozwiązania, które jest bardziej skomplikowane niż jest to wymagane.
Doc Brown,
3

Może wszystko jest Kartą, gdy znajduje się w talii lub na cmentarzu, a kiedy ją zagrywasz, budujesz Stworzenie, Krainę, Zaklęcie itp. Z jednego lub więcej obiektów Karty, z których wszystkie implementują lub rozszerzają Grywalne. Następnie kompozyt staje się pojedynczym Grywalnym, którego konstruktor bierze dwie częściowe Karty, a karta z kopaczem staje się Grywalnym, którego konstruktor bierze argument many. Typ odzwierciedla, co możesz z nim zrobić (rysować, blokować, rozwiewać, stukać) i co może na to wpłynąć. Lub Grywalna to po prostu Karta, którą należy ostrożnie cofnąć (utratę wszelkich bonusów i liczników, podział) po wyjściu z gry, jeśli naprawdę użyteczne jest użycie tego samego interfejsu do wywołania karty i przewidzenia, co ona zrobi.

Być może karty i grywalne mają efekt.

Davislor
źródło
Niestety nie gram tych kart. Są to tak naprawdę obiekty danych, które można przeszukiwać. Jeśli chcesz mieć strukturę danych talii, chcesz List <WholeCard> lub coś takiego. Jeśli chcesz przeprowadzić wyszukiwanie zielonych kart Instant, potrzebujesz Listy <Card>.
codebreaker
Ah, dobrze. Zatem niezbyt istotne dla twojego problemu. Czy powinienem usunąć?
Davislor,
3

Odwiedzający to klasyczna technika odzyskiwania ukrytych informacji typu. Możemy go tutaj użyć (tutaj jego niewielka odmiana), aby rozróżnić dwa typy, nawet jeśli są one przechowywane w zmiennych o wyższej abstrakcji.

Zacznijmy od tej wyższej abstrakcji, Cardinterfejsu:

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

CardInterfejs może nieco się zachowywać , ale większość pobierających właściwości przenosi się do nowej klasy CardProperties:

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

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

Teraz możemy mieć SimpleCardreprezentującą całą kartę z jednym zestawem właściwości:

public class SimpleCard implements Card {
    private CardProperties properties;

    // Constructors, ...

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

Widzimy, jak zaczynają się układać CardPropertieste, które jeszcze nie zostały napisane CardVisitor. Zróbmy to, CompoundCardaby przedstawić kartę o dwóch twarzach:

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

    // Constructors, ...

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

CardVisitorZaczyna się pojawiać. Spróbujmy teraz napisać ten interfejs:

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

(Na razie jest to pierwsza wersja interfejsu. Możemy wprowadzić ulepszenia, które zostaną omówione później).

Opracowaliśmy teraz wszystkie części. Teraz musimy je tylko połączyć:

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
         }
     });
 }

Środowisko wykonawcze obsłuży wysyłkę do poprawnej wersji #visitmetody poprzez polimorfizm, zamiast próbować go przerwać.

Zamiast korzystać z anonimowej klasy, możesz nawet awansować CardVisitordo klasy wewnętrznej lub nawet pełnej, jeśli zachowanie jest wielokrotnego użytku lub jeśli chcesz mieć możliwość zamiany zachowania w czasie wykonywania.


Możemy korzystać z klas takimi, jakimi są teraz, ale możemy wprowadzić pewne ulepszenia CardVisitorinterfejsu. Na przykład może nadejść czas, kiedyCard s może mieć trzy, cztery lub pięć twarzy. Zamiast dodawać nowe metody do wdrożenia, moglibyśmy po prostu wziąć drugą metodę take i tablicę zamiast dwóch parametrów. Ma to sens, jeśli karty o wielu twarzach są traktowane inaczej, ale liczba twarzy powyżej jednej jest traktowana podobnie.

Możemy również przekonwertować CardVisitorna klasę abstrakcyjną zamiast interfejsu i mieć puste implementacje dla wszystkich metod. To pozwala nam wdrażać tylko te zachowania, którymi jesteśmy zainteresowani (być może interesują nas tylko osoby o pojedynczych twarzach Card). Możemy również dodawać nowe metody bez zmuszania każdej istniejącej klasy do wdrożenia tych metod lub niepowodzenia kompilacji.

Cbojar
źródło
1
Myślę, że można to łatwo rozszerzyć na inny rodzaj kart o dwóch twarzach (przód i tył zamiast obok siebie). ++
RubberDuck
W celu dalszej eksploracji użycie Odwiedzającego do wykorzystania metod specyficznych dla podklasy jest czasami nazywane Wysyłaniem wielokrotnym. Podwójna wysyłka może być interesującym rozwiązaniem problemu.
mgoeminne,
1
Głosuję za odrzuceniem, ponieważ myślę, że wzorzec odwiedzających powoduje niepotrzebną złożoność i sprzężenie z kodem. Ponieważ alternatywa jest dostępna (patrz odpowiedź mgoeminne), nie użyłbym jej.
Zauważył
@Spotted Odwiedzający jest złożonym wzorem, a także wziąłem pod uwagę wzór złożony podczas pisania odpowiedzi. Powodem, dla którego poszedłem z gościem, jest to, że OP chce traktować podobne rzeczy inaczej, niż różne rzeczy podobnie. Jeśli karty miały więcej zachowań i były używane do rozgrywki, wówczas złożony wzór pozwala na łączenie danych w celu uzyskania jednolitej statystyki. Są to po prostu worki danych, które mogą być użyte do renderowania wyświetlacza karty, w którym to przypadku oddzielenie informacji wydawało się bardziej przydatne niż zwykły agregator złożony.
cbojar