Dziedziczenie vs Kompozycja do kawałków szachowych

9

Szybkie wyszukiwanie tej wymiany stosów pokazuje, że ogólnie kompozycja jest ogólnie uważana za bardziej elastyczną niż dziedziczenie, ale jak zawsze zależy od projektu itp. I są chwile, kiedy dziedziczenie jest lepszym wyborem. Chcę stworzyć grę w szachy 3D, w której każdy element ma siatkę, być może różne animacje i tak dalej. W tym konkretnym przykładzie wydaje się, że możesz argumentować za obiema metodami, czy się mylę?

Dziedziczenie wyglądałoby mniej więcej tak (z odpowiednim konstruktorem itp.)

class BasePiece
{
    virtual Squares GetValidMoveSquares() = 0;
    Mesh* mesh;
    // Other fields
}

class Pawn : public BasePiece
{
   Squares GetValidMoveSquares() override;
}

co z pewnością jest zgodne z zasadą „jest”, podczas gdy kompozycja wyglądałaby mniej więcej tak

class MovementComponent
{
    virtual Squares GetValidMoveSquares() = 0;
}

class PawnMovementComponent
{
     Squares GetValidMoveSquares() override;
}

enum class Type
{
     PAWN,
     BISHOP, //etc
}


class Piece
{
    MovementComponent* movementComponent;
    MeshComponent* mesh;
    Type type;
    // Other fields
 }

Czy to bardziej kwestia osobistych preferencji, czy jedno podejście jest zdecydowanie mądrzejszym wyborem niż inne tutaj?

EDYCJA: Myślę, że nauczyłem się czegoś z każdej odpowiedzi, więc źle się czuję, wybierając tylko jedną. Moje ostateczne rozwiązanie zaczerpnie inspirację z kilku postów tutaj (wciąż nad tym pracuję). Dziękujemy wszystkim, którzy poświęcili czas na odpowiedź.

CanISleepYet
źródło
Wierzę, że oba mogą istnieć obok siebie, te dwa są do różnych celów, ale nie wykluczają się one wzajemnie. Szachy składają się z różnych elementów i plansz, a elementy są różnego rodzaju. Te elementy mają wspólną podstawową właściwość i zachowanie, a także mają określoną właściwość i zachowanie. Według mnie więc kompozycję należy nakładać na deskę i kawałki, a dziedziczenie powinno następować po rodzajach kawałków.
Hitesh Gaur
Chociaż może się zdarzyć, że niektórzy twierdzą, że całe dziedziczenie jest złe, myślę, że ogólną ideą jest to, że dziedziczenie interfejsu jest dobre / w porządku, a dziedziczenie implementacji jest przydatne, ale może być problematyczne. Z mojego doświadczenia wynika, że ​​wszystko poza jednym poziomem dziedziczenia jest wątpliwe. Poza tym może być OK, ale nie jest to łatwe do zrobienia bez zawiłego bałaganu.
JimmyJames
Wzorzec strategii jest często stosowany w programowaniu gier. var Pawn = new Piece(howIMove, howITake, whatILookLike)wydaje mi się znacznie prostsze, łatwiejsze w zarządzaniu i łatwiejsze w utrzymaniu niż hierarchia dziedziczenia.
Ant P
@AntP To dobra uwaga, dzięki!
CanISleepYet
@AntP Uczyń to odpowiedzią! ... Jest w tym wiele zasług!
svidgen

Odpowiedzi:

4

Na pierwszy rzut oka twoja odpowiedź udaje, że rozwiązanie „kompozycja” nie wykorzystuje dziedziczenia. Ale chyba po prostu zapomniałeś dodać to:

class PawnMovementComponent : MovementComponent
{
     Squares GetValidMoveSquares() override;
}

(i więcej tych pochodnych, po jednym dla każdego z sześciu typów sztuk). Teraz wygląda to bardziej na klasyczny wzorzec „strategii” i wykorzystuje również dziedziczenie.

Niestety, to rozwiązanie ma wadę: każde z nich Pieceprzechowuje nadmiarowo dwukrotnie informację o typie:

  • wewnątrz zmiennej członka type

  • wewnątrz movementComponent(reprezentowane przez podtyp)

Ta nadmiarowość może naprawdę wpędzić cię w kłopoty - prosta klasa taka jak Piecepowinna zapewniać „pojedyncze źródło prawdy”, a nie dwa źródła.

Oczywiście możesz spróbować przechowywać informacje o typie tylko w typei nie tworzyć żadnych klas podrzędnych MovementComponent. Ale ten projekt najprawdopodobniej doprowadziłby do ogromnego switch(type)oświadczenia podczas implementacji GetValidMoveSquares. I to zdecydowanie wskazuje na to, że dziedziczenie jest tutaj lepszym wyborem .

Notatka w „spadku” projektu, jest to dość łatwe do zapewnienia „typ” pola w nieredundantnej sposób: dodać wirtualną metodę GetType()do BasePiecei wdrożyć go odpowiednio w każdej klasie bazowej.

Jeśli chodzi o „promocję”: jestem tutaj z @svidgen, argumenty przedstawione w odpowiedzi @ TheCatWhisperer są dyskusyjne.

Interpretowanie „promocji” jako fizycznej wymiany utworów zamiast interpretowania ich jako zmiany rodzaju tego samego utworu wydaje mi się bardziej naturalne. Tak więc wdrożenie tego jak w podobny sposób - poprzez zamianę jednego kawałka na inny innego typu - najprawdopodobniej nie spowoduje żadnych poważnych problemów - przynajmniej nie dla konkretnego przypadku szachów.

Doktor Brown
źródło
Dzięki, że podałeś mi nazwę wzoru, nie zdawałem sobie sprawy, że nazywa się Strategia. Masz również rację. W moim pytaniu przeoczyłem dziedziczenie komponentu ruchu. Czy ten typ jest jednak naprawdę zbędny? Jeśli chcę podświetlić wszystkie królowe na planszy, dziwnie byłoby sprawdzić, jaki element ruchu mają, a ponadto musiałbym rzucić element ruchu, aby zobaczyć, jaki to typ, który byłby super brzydki, nie?
CanISleepYet
@CanISleepYet: problem z proponowanym polem „typ” polega na tym, że może on różnić się od podtypu movementComponent. Zobacz moją edycję, w jaki sposób bezpiecznie podać pole typu w projekcie „dziedziczenia”.
Doc Brown
4

Prawdopodobnie wolałbym prostszą opcję, chyba że masz wyraźne, dobrze wyartykułowane powody lub poważne obawy związane z rozszerzalnością i utrzymaniem.

Opcja dziedziczenia to mniej linii kodu i mniej złożoności. Każdy rodzaj elementu ma „niezmienny” stosunek 1 do 1 z jego charakterystyką ruchu. (W przeciwieństwie do reprezentacji, która jest równa 1 do 1, ale niekoniecznie niezmienna - możesz zaoferować różne „skórki”).

Jeśli rozbijesz tę niezmienną relację 1-do-1 między elementami i ich zachowaniem / ruchem na wiele klas, prawdopodobnie tylko zwiększysz złożoność - i niewiele więcej. Gdybym więc przeglądał lub dziedziczył ten kod, oczekiwałbym dobrych, udokumentowanych przyczyn złożoności.

Wszystko, co powiedziałem, jeszcze prostszą opcją , IMO, jest stworzenie Piece interfejsu, który będą wdrażane przez poszczególne elementy . W większości języków jest to w rzeczywistości bardzo różni się od dziedziczenia , ponieważ interfejs nie będzie Cię ograniczał do implementacji innego interfejsu . ... W takim przypadku po prostu nie dostajesz żadnych zachowań klasy podstawowej: chciałbyś umieścić wspólne zachowanie gdzie indziej.

svidgen
źródło
Nie kupuję tego, że rozwiązaniem dziedziczenia jest „mniej linii kodu”. Być może w tej jednej klasie, ale nie ogólnie.
JimmyJames
@JimmyJames Naprawdę? ... Wystarczy policzyć wiersze kodu w PO ... co prawda nie całe rozwiązanie, ale niezły wskaźnik tego, które rozwiązanie jest bardziej LOC i bardziej skomplikowane do utrzymania.
svidgen
Nie chcę zbytnio na to zwracać uwagi, ponieważ w tym momencie wszystko jest hipotetyczne, ale tak, naprawdę tak myślę. Uważam, że ten fragment kodu jest znikomy w porównaniu z całą aplikacją. Jeśli upraszcza to implementację reguł (dyskusyjne), możesz zaoszczędzić dziesiątki, a nawet setki linii kodu.
JimmyJames
Dzięki, nie myślałem o interfejsie, ale być może masz rację, że to najlepsza droga. Prostsze jest prawie zawsze lepsze.
CanISleepYet
2

Szachy polegają na tym, że zasady gry i zasady gry są określone i naprawione. Każdy projekt, który działa, jest w porządku - używaj, co chcesz! Eksperymentuj i wypróbuj je wszystkie.

Jednak w świecie biznesu nie ma tak ściśle określonych przepisów - zasady i wymagania biznesowe zmieniają się z czasem, a programy muszą się zmieniać w miarę upływu czasu. To jest różnica między danymi typu a-a-kontra-a. Tutaj prostota sprawia, że ​​złożone rozwiązania są łatwiejsze w utrzymaniu w miarę upływu czasu i zmieniających się wymagań. Ogólnie rzecz biorąc, w biznesie musimy również radzić sobie z wytrwałością, która może obejmować również bazę danych. Mamy więc reguły takie jak: nie używaj wielu klas, gdy jedna klasa wykona zadanie, i nie używaj dziedziczenia, gdy skład jest wystarczający. Ale wszystkie te zasady mają na celu uczynienie kodu łatwym do utrzymania na dłuższą metę w obliczu zmieniających się zasad i wymagań - co nie jest tak w przypadku szachów.

W przypadku szachów najbardziej prawdopodobną ścieżką długoterminowej konserwacji jest to, że Twój program musi stać się mądrzejszy, co ostatecznie oznacza, że ​​będą dominować optymalizacje szybkości i pamięci. W tym celu generalnie będziesz musiał dokonać kompromisów, które poświęcają czytelność dla wydajności, więc nawet najlepszy projekt OO ostatecznie pójdzie na marne.

Erik Eidt
źródło
Zauważ, że pojawiło się wiele udanych gier, które zakładają koncepcję, a następnie dodają nowe rzeczy do miksu. Jeśli chcesz to zrobić w przyszłości w swojej grze w szachy, powinieneś wziąć pod uwagę możliwe przyszłe zmiany.
Flater
2

Zacznę od kilku uwag na temat problematycznej dziedziny (tj. Zasad szachowych):

  • Zestaw prawidłowych ruchów dla pionka zależy nie tylko od rodzaju pionka, ale również od stanu planszy (pionek może poruszać się do przodu, jeśli kwadrat jest pusty / może przesuwać się po przekątnej, jeśli chwyta kawałek).
  • Niektóre działania polegają na usunięciu z gry istniejącego elementu (awans / złapanie) lub przeniesieniu wielu elementów (roszowanie).

Nie są one łatwe do modelowania jako odpowiedzialność za sam utwór i ani dziedziczenie, ani kompozycja nie czują się tutaj dobrze. Bardziej naturalne byłoby modelowanie tych zasad jako obywateli pierwszej klasy:

// pseudo-code, not pure C++

interface MovementRule {
  set<Square> getValidMoves(Board board, Square from); // what moves can I make from the given square?
  void makeMove(Board board, Square from, Square to); // update board state to reflect a specific move
}

class Game {
  Board board;
  map<PieceType, MovementRule> rules;
}

MovementRuleto prosty, ale elastyczny interfejs, który pozwala implementacjom aktualizować stan tablicy w jakikolwiek sposób wymagany do obsługi złożonych ruchów, takich jak castling i promocja. W przypadku standardowych szachów miałbyś jedną implementację dla każdego rodzaju elementu, ale możesz też podłączyć różne reguły w Gameinstancji, aby obsługiwać różne warianty szachów.

Casablanka
źródło
Ciekawe podejście - Dobre jedzenie do przemyślenia
CanISleepYet
1

Wydaje mi się, że w tym przypadku dziedziczenie byłoby czystszym wyborem. Kompozycja może być nieco bardziej elegancka, ale wydaje mi się, że jest trochę bardziej wymuszona.

Jeśli planujesz opracować inne gry, które wykorzystują ruchome elementy, które zachowują się inaczej, kompozycja może być lepszym wyborem, szczególnie jeśli zastosowałeś wzór fabryczny do wygenerowania elementów potrzebnych do każdej gry.

Kurt Haldeman
źródło
2
Jak poradziłbyś sobie z promocją za pomocą dziedziczenia?
TheCatWhisperer
4
@TheCatWhisperer Jak sobie z tym poradzić, gdy fizycznie grasz w grę? (Mam nadzieję, że zastąpisz ten kawałek - nie wyrzeźbisz pionka, prawda !?)
svidgen
0

co z pewnością jest zgodne z zasadą „jest-a”

Racja, więc korzystasz z dziedziczenia. Nadal możesz komponować w klasie Piece, ale przynajmniej będziesz miał kręgosłup. Kompozycja jest bardziej elastyczna, ale elastyczność jest przereklamowana, ogólnie rzecz biorąc, i na pewno w tym przypadku, ponieważ szanse, że ktoś wprowadzi nowy rodzaj elementu, który wymagałby porzucenia sztywnego modelu, wynoszą zero. Gra w szachy nie zmieni się w najbliższym czasie. Dziedziczenie daje model przewodni, coś do odniesienia, nauczenie kodu, kierunek. Kompozycja nie tyle, najważniejsze jednostki domeny nie wyróżniają się, jest pozbawiona spinów, musisz zrozumieć grę, aby zrozumieć kod.

Martin Maat
źródło
dzięki za dodanie uwagi, że w przypadku kompozycji trudniej jest wnioskować o kodzie
CanISleepYet
0

Nie używaj tutaj dziedziczenia. Podczas gdy inne odpowiedzi z pewnością zawierają wiele klejnotów mądrości, użycie tutaj dziedzictwa z pewnością byłoby błędem. Używanie kompozycji nie tylko pomaga poradzić sobie z problemami, których nie przewidujesz, ale już widzę tutaj problem, którego dziedziczenie nie jest w stanie z wdziękiem sobie poradzić.

Awans, kiedy pionek zostanie zamieniony na bardziej wartościowy element, może stanowić problem z dziedziczeniem. Technicznie możesz sobie z tym poradzić, zastępując jeden element innym, ale to podejście ma ograniczenia. Jednym z tych ograniczeń byłoby śledzenie statystyk poszczególnych sztuk, w przypadku których możesz nie chcieć resetować informacji o sztukach podczas promocji (lub pisać dodatkowy kod, aby je skopiować).

Teraz, jeśli chodzi o projekt kompozycji w twoim pytaniu, myślę, że jest to niepotrzebnie skomplikowane. Nie podejrzewam, że musisz mieć osobny komponent dla każdej akcji i mógłbym trzymać się jednej klasy na typ elementu. Wyliczenie typu może być również niepotrzebne.

Zdefiniowałbym Pieceklasę (jak już macie), a także PieceTypeklasę. W PieceTypeklasa definiuje wszystkie metody, które pionkiem, królowej, ect musi zdefiniować, takie jak CanMoveTo, GetPieceTypeName, ect. Wówczas klasy Pawni Queenect praktycznie dziedziczyłyby po PieceTypeklasie.

TheCatWhisperer
źródło
3
Problemy, które widzisz przy promocji i wymianie są trywialne, IMO. Oddzielenie obawy dość dużo mandatów, że takie „za sztukę śledzenia” (lub cokolwiek) być obsługiwane niezależnie w każdym razie . ... Wszelkie potencjalne korzyści, jakie oferuje twoje rozwiązanie, są przyćmione przez wyimaginowane obawy, które próbujesz rozwiązać, IMO.
svidgen
5
Wyjaśnienie: niewątpliwie istnieją pewne zalety proponowanego rozwiązania. Trudno je jednak znaleźć w odpowiedzi, która koncentruje się przede wszystkim na problemach, które są naprawdę bardzo łatwe do rozwiązania w „rozwiązaniu dotyczącym dziedziczenia”. ... Ten rodzaj (w każdym razie dla mnie) umniejsza wszelkie zasługi, które próbujesz przekazać na temat swojej opinii.
svidgen
1
Jeśli Piecenie jest wirtualny, zgadzam się z tym rozwiązaniem. Podczas gdy projektowanie klas może wydawać się nieco bardziej skomplikowane na powierzchni, myślę, że implementacja będzie ogólnie prostsza. Głównymi rzeczami, które można by skojarzyć z, Piecea nie z PieceTypeich tożsamością i potencjalnie historią ruchów.
JimmyJames
1
@svidgen Implementacja korzystająca ze skomponowanego elementu i implementacja korzystająca z elementu opartego na dziedziczeniu będzie wyglądać zasadniczo identycznie oprócz szczegółów opisanych w tym rozwiązaniu. To rozwiązanie kompozycji dodaje jeden poziom pośredni. Nie uważam tego za coś, czego warto unikać.
JimmyJames
2
@JimmyJames Right. Ale historia ruchów wielu elementów musi być śledzona i skorelowana - nie jest to coś, za co każdy element powinien być odpowiedzialny, IMO. Jest to „osobna sprawa”, jeśli lubisz rzeczy SOLIDNE.
svidgen
0

Z mojego punktu widzenia, z dostarczonymi informacjami, nie jest mądrze odpowiadać, czy dziedziczenie czy kompozycja powinny być właściwą drogą.

  • Dziedziczenie i kompozycja to tylko narzędzia w zorientowanym obiektowo pasku narzędzi projektanta.
  • Za pomocą tych narzędzi można zaprojektować wiele różnych modeli dla dziedziny problemu.
  • Ponieważ istnieje wiele takich modeli, które mogą reprezentować domenę, w zależności od punktu widzenia projektanta wymagań, można łączyć dziedziczenie i kompozycję na wiele sposobów, aby uzyskać funkcjonalnie równoważne modele.

Twoje modele są zupełnie inne, w pierwszym modelujesz wokół koncepcji szachy, w drugim - koncepcja ruchu elementów. Mogą być funkcjonalnie równoważne, a ja wybrałbym ten, który lepiej reprezentuje domenę i pomaga mi łatwiej o tym myśleć.

Również w twoim drugim projekcie fakt, że twoja Pieceklasa ma typepole, wyraźnie pokazuje, że istnieje tutaj problem projektowy, ponieważ sam element może być wielu typów. Czy to nie brzmi tak, że prawdopodobnie należy użyć jakiegoś dziedziczenia, gdzie sam utwór jest typem?

Widzisz, bardzo trudno jest spierać się o twoje rozwiązanie. Liczy się nie to, czy zastosowałeś dziedziczenie, czy kompozycję, ale czy Twój model dokładnie odzwierciedla dziedzinę problemu i czy warto go uzasadnić i podać jego implementację.

Właściwe wykorzystanie dziedziczenia i kompozycji wymaga pewnego doświadczenia, ale są to zupełnie inne narzędzia i nie sądzę, że można je „zastąpić” innym, chociaż zgadzam się, że system można zaprojektować w całości przy użyciu samej kompozycji.

edalorzo
źródło
Dobrze robisz, że to model jest ważny, a nie narzędzie. W odniesieniu do typu zbędnego dziedziczenie naprawdę pomaga? Na przykład, jeśli chcę podświetlić wszystkie pionki lub sprawdzić, czy pionek jest królem, to czy nie potrzebuję rzucić?
CanISleepYet
-1

Cóż, nie sugeruję żadnego.

Orientacja obiektowa jest przyjemnym narzędziem i często pomaga uprościć sprawy, ale nie jest jedynym narzędziem w zestawie narzędzi.

Zastanów się, czy zamiast tego zaimplementować klasę planszy zawierającą prostą tablicę.
Interpretowanie tego i obliczanie tego odbywa się w funkcjach składowych za pomocą maskowania bitów i przełączania.

Użyj MSB dla właściciela, pozostawiając 7 bitów na sztukę, choć rezerwę zero na puste.
Alternatywnie zero dla pustych, znak dla właściciela, wartość bezwzględna dla sztuki.

Jeśli chcesz, możesz użyć pól bitów, aby uniknąć ręcznego maskowania, chociaż nie można ich przenosić:

class field {
    signed char data = 0;
public:
    constexpr field() = default;
    constexpr field(bool black, piece x) noexcept
    : data(x < piece::pawn || x > piece::king ? 0 : (black << 7) | x))
    {}
    constexpr bool is_black() noexcept { return data < 0; }
    constexpr bool is_white() noexcept { return data > 0; }
    constexpr bool empty() noexcept { return data == 0; }
    constexpr piece piece() noexcept { return piece(data & 0x7f); }
};
Deduplikator
źródło
Ciekawe ... i apt rozważa to pytanie C ++. Ale nie będąc programistą C ++, nie byłby to mój pierwszy (drugi lub trzeci ) instynkt! ... To może być najbardziej odpowiedź C ++ tutaj!
svidgen
7
Jest to mikrooptymalizacja, której nie powinna podążać żadna prawdziwa aplikacja.
Lie Ryan
@LieRyan Jeśli wszystko, co masz, to OOP, wszystko pachnie jak obiekt. I czy to naprawdę „mikro” również jest interesującym pytaniem. W zależności od tego, co z nim zrobisz , może to być dość znaczące.
Deduplicator
Heh ... to powiedziawszy , C ++ jest zorientowany obiektowo. Zakładam, że istnieje powód PO jest za pomocą C ++ zamiast tylko C .
svidgen
3
Tutaj jestem mniej zaniepokojony brakiem OOP, ale raczej zaciemniam kod nieco kręcąc. O ile nie piszesz kodu do 8-bitowego Nintendo lub piszesz algorytmy, które wymagają radzenia sobie z milionami kopii stanu płyty, jest to przedwczesna mikrooptymalizacja i niewskazana. W normalnych scenariuszach i tak prawdopodobnie nie będzie szybszy, ponieważ niezaangażowany dostęp do bitów wymaga dodatkowych operacji bitowych.
Lie Ryan