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ź.
źródło
var Pawn = new Piece(howIMove, howITake, whatILookLike)
wydaje mi się znacznie prostsze, łatwiejsze w zarządzaniu i łatwiejsze w utrzymaniu niż hierarchia dziedziczenia.Odpowiedzi:
Na pierwszy rzut oka twoja odpowiedź udaje, że rozwiązanie „kompozycja” nie wykorzystuje dziedziczenia. Ale chyba po prostu zapomniałeś dodać to:
(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
Piece
przechowuje 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
Piece
powinna zapewniać „pojedyncze źródło prawdy”, a nie dwa źródła.Oczywiście możesz spróbować przechowywać informacje o typie tylko w
type
i nie tworzyć żadnych klas podrzędnychMovementComponent
. Ale ten projekt najprawdopodobniej doprowadziłby do ogromnegoswitch(type)
oświadczenia podczas implementacjiGetValidMoveSquares
. 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()
doBasePiece
i 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.
źródło
movementComponent
. Zobacz moją edycję, w jaki sposób bezpiecznie podać pole typu w projekcie „dziedziczenia”.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.źródło
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.
źródło
Zacznę od kilku uwag na temat problematycznej dziedziny (tj. Zasad szachowych):
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:
MovementRule
to 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 wGame
instancji, aby obsługiwać różne warianty szachów.źródło
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.
źródło
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.
źródło
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
Piece
klasę (jak już macie), a takżePieceType
klasę. WPieceType
klasa definiuje wszystkie metody, które pionkiem, królowej, ect musi zdefiniować, takie jakCanMoveTo
,GetPieceTypeName
, ect. Wówczas klasyPawn
iQueen
ect praktycznie dziedziczyłyby poPieceType
klasie.źródło
Piece
nie 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,Piece
a nie zPieceType
ich tożsamością i potencjalnie historią ruchów.Z mojego punktu widzenia, z dostarczonymi informacjami, nie jest mądrze odpowiadać, czy dziedziczenie czy kompozycja powinny być właściwą drogą.
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
Piece
klasa matype
pole, 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.
źródło
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ć:
źródło