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ę Card
tylko odniesienia do czegoś, co jest albo zwykłą kartą, albo połową dwuczęściowej karty (innymi słowy, czymś o tylko jednej nazwie).
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). PartialCard
ma dwie dodatkowe metody: PartialCard otherPart()
i boolean isFirstPart()
.
Przedstawiciele
Jeśli mam talię, powinna ona składać się z WholeCard
s, a nie Card
s, ponieważ Card
moż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 WholeCard
lub dwa PartialCard
s. Wstępnie nazywam ten typ Representative
i Card
mam metodę getRepresentative()
. A Representative
nie 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ć getRepresentative
jako return this;
.
Jeśli chodzi o PartialCards
, nie reprezentują siebie, ale mają zewnętrzny, Representative
któ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ż PartialCard
s i WholeCards
oba 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
, Representative
trzeba by rzucić na albo, WholeCard
albo Composite
, aby uzyskać dostęp do rzeczywistych Card
s, 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.
źródło
Odpowiedzi:
Wydaje mi się, że powinieneś mieć taką klasę
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ć.
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.
źródło
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
Card
istocie zawierać N każdego atrybutu: grywalna nazwa, koszt many, typ, rzadkość, tekst, efekty itp.Opcja 2. Nie wszystko, co różni się od opcji 1, modeluj ją zgodnie z rzeczywistością fizyczną. Masz
Card
byt reprezentujący fizyczną kartę . I celem jest trzymanie NPlayable
rzeczy. TePlayable
„s każdy może mieć odrębną nazwę, koszt many, listę działań, wykaz umiejętności, etc .. A twoja«fizyczny»Card
może mieć swój własny identyfikator (lub nazwę), który jest związkiem o każdyPlayable
” s nazwy, podobnie jak wydaje się, że baza danych MTG działa.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.)
źródło
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.
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:
Card
interfejs, tak jak już to zrobiłeś.ConcreteCard
, który implementujeCard
i definiuje prostą kartę twarzy. Nie wahaj się postawić normalnego zachowania w tej klasie karty.CompositeCard
, który implementujeCard
i ma dwa dodatkowe (i a priori prywatne)Card
. Zadzwońmy do nichleftCard
irightCard
.Elegancja tego podejścia polega na tym, że a
CompositeCard
zawiera dwie karty, które same mogą być kartami betonowymi lub kompozytowymi. W twojej grze,leftCard
irightCard
prawdopodobnie będzie to systematycznieConcreteCard
s, 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.CompositeCard
muszą wdrożyć metody określone wCard
, 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 dlaCompositeCard
samej karty. Na przykład możesz chcieć następujące wdrożenie:Robiąc to, możesz użyć
CompositeCard
dokładnie tego, co robisz dla każdegoCard
, a konkretne zachowanie jest ukryte dzięki polimorfizmowi.Jeśli masz pewność,
CompositeCard
że zawsze będzie zawierać dwa normalneCard
s, możesz zachować pomysł i po prostu użyć goConcreateCard
jako typu dlaleftCard
irightCard
.źródło
Card
wCompositeCard
jesteś realizacji wzorzec dekoratora . Polecam również OP, aby skorzystać z tego rozwiązania, dekorator to droga!CompositeCard
nie ujawnia dodatkowych metod,CompositeCard
jest jedynie dekoratorem.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.
źródło
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,
Card
interfejsu:Card
Interfejs może nieco się zachowywać , ale większość pobierających właściwości przenosi się do nowej klasyCardProperties
:Teraz możemy mieć
SimpleCard
reprezentującą całą kartę z jednym zestawem właściwości:Widzimy, jak zaczynają się układać
CardProperties
te, które jeszcze nie zostały napisaneCardVisitor
. Zróbmy to,CompoundCard
aby przedstawić kartę o dwóch twarzach:CardVisitor
Zaczyna się pojawiać. Spróbujmy teraz napisać ten interfejs:(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ć:
Środowisko wykonawcze obsłuży wysyłkę do poprawnej wersji
#visit
metody poprzez polimorfizm, zamiast próbować go przerwać.Zamiast korzystać z anonimowej klasy, możesz nawet awansować
CardVisitor
do 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
CardVisitor
interfejsu. 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ć
CardVisitor
na 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 twarzachCard
). Możemy również dodawać nowe metody bez zmuszania każdej istniejącej klasy do wdrożenia tych metod lub niepowodzenia kompilacji.źródło