Kiedy przestać dziedziczyć?

9

Pewnego razu zadałem pytanie na temat przepełnienia stosu dotyczące dziedziczenia.

Powiedziałem, że projektuję silnik szachowy w stylu OOP. Więc odziedziczyłem wszystkie moje dzieła z klasy Abstract, ale dziedziczenie wciąż trwa. Pokaż mi kod

public abstract class Piece
{
    public void MakeMove();
    public void TakeBackMove();
}

public abstract class Pawn: Piece {}

public class WhitePawn :Pawn {}

public class BlackPawn:Pawn {}

Programiści odkryli, że mój projekt jest nieco przerobiony inżynieryjnie i zasugerowali usunięcie klas kolorowych elementów i zachowanie koloru elementu jako elementu właściwości, jak poniżej.

public abstract class Piece
{
    public Color Color { get; set; }
    public abstract void MakeMove();
    public abstract void TakeBackMove();
}

W ten sposób Piece mogą poznać jego kolor. Po wdrożeniu zauważyłem, że moje wdrożenie przebiega jak poniżej.

public abstract class Pawn: Piece
{
    public override void MakeMove()
    {
        if (this.Color == Color.White)
        {

        }
        else
        {

        }
    }

    public override void TakeBackMove()
    {
        if (this.Color == Color.White)
        {

        }
        else
        {

        }
    }
}

Teraz widzę, że zachowują właściwość koloru powodującą, że instrukcje w implementacjach. Czuję, że potrzebujemy określonych kolorów w hierarchii dziedziczenia.

W takim przypadku czy miałbyś takie klasy jak WhitePawn, BlackPawn czy też zająłby się projektowaniem, zachowując właściwość Color?

Nie widząc takiego problemu, jak chciałbyś zacząć projektowanie? Zachować właściwość Color lub mieć rozwiązanie do dziedziczenia?

Edycja: Chcę określić, że mój przykład może nie pasować całkowicie do przykładu z życia. Więc zanim spróbujesz odgadnąć szczegóły implementacji, skoncentruj się na pytaniu.

Właściwie po prostu pytam, czy użycie właściwości Kolor spowoduje, że jeśli oświadczenia lepiej użyć dziedziczenia?

Świeża krew
źródło
3
Naprawdę nie rozumiem, dlaczego w ogóle modelowałbyś takie kawałki. Po wywołaniu MakeMove () jaki ruch wykona? Powiedziałbym, że tak naprawdę musisz zacząć od modelowania stanu tablicy i zobacz, jakie klasy potrzebujesz do tego
jk.
11
Myślę, że twoim podstawowym błędnym przekonaniem nie jest uświadomienie sobie, że w szachach „kolor” jest naprawdę atrybutem gracza, a nie kawałka. Dlatego mówimy „ruch czarnych” itp. Kolorystyka służy do określenia, który gracz jest właścicielem danego elementu. Pionek podlega tym samym zasadom i ograniczeniom ruchu, bez względu na kolor, jedyną odmianą jest kierunek, który jest „do przodu”.
James Anderson
5
jeśli nie możesz wymyślić dziedziczenia „kiedy przestać”, lepiej całkowicie go upuść. Możesz ponownie wprowadzić dziedziczenie na późniejszym etapie projektowania / wdrażania / konserwacji, kiedy hierarchia stanie się dla ciebie oczywista. Używam tego podejścia, działa jak urok
komara
1
Trudniejszym zagadnieniem jest to, czy utworzyć podklasę dla każdego rodzaju sztuki, czy nie. Typ elementu określa więcej aspektów behawioralnych niż kolor elementu.
NoChance
3
Jeśli masz ochotę rozpocząć projektowanie z ABC (streszczenie podstawowych klas), wyświadcz nam wszystkim przysługę i nie ... Przypadki, w których ABC są faktycznie przydatne, są bardzo rzadkie, a nawet wtedy zwykle bardziej użyteczne jest użycie interfejsu zamiast.
Evan Plaice,

Odpowiedzi:

12

Wyobraź sobie, że projektujesz aplikację zawierającą pojazdy. Czy tworzysz coś takiego?

class BlackVehicle : Vehicle { }
class DarkBlueVehicle : Vehicle { }
class DarkGreenVehicle : Vehicle { }
// hundreds of other classes...

W rzeczywistości kolor jest własnością obiektu (samochodu lub figury szachowej) i musi być reprezentowany przez właściwość.

MakeMovei TakeBackMoveinne dla czarnych i białych? Wcale nie: zasady są takie same dla obu graczy, co oznacza, że ​​te dwie metody będą dokładnie takie same dla czarnych i białych , co oznacza, że ​​dodajesz warunek, w którym go nie potrzebujesz.

Z drugiej strony, jeśli masz WhitePawni BlackPawnnie będę zaskoczony, jeśli prędzej czy później napiszesz:

var piece = (Pawn)this.CurrentPiece;
if (piece is WhitePawn)
{
}
else // The pice is of type BlackPawn
{
}

a nawet coś takiego:

public abstract class Pawn
{
    public Color Player
    {
        get
        {
            return this is WhitePawn ? Color.White : Color.Black;
        }
    }
}

// Now imagine doing the same thing for every other piece.
Arseni Mourzenko
źródło
4
@Freshblood Myślę, że lepiej byłoby mieć directionwłaściwość i użyć jej do przeniesienia, niż użyć właściwości color. Kolor najlepiej używać wyłącznie do renderowania, a nie do logiki.
Gyan alias Gary Buyn
7
Poza tym elementy poruszają się w tym samym kierunku, do przodu, do tyłu, w lewo, w prawo. Różnica polega na tym, od której strony planszy zaczynają. Na drodze samochody na przeciwległych pasach poruszają się do przodu.
slipset
1
@Blrfl Być może masz rację, że directionwłaściwość jest wartością logiczną. Nie rozumiem, co jest z tym nie tak. To, co pomieszało mnie w tym pytaniu aż do powyższego komentarza Freshblood, to dlaczego chciałby zmienić logikę ruchu w oparciu o kolor. Powiedziałbym, że jest to trudniejsze do zrozumienia, ponieważ nie ma sensu, aby kolor wpływał na zachowanie. Jeśli wszystko, co chcesz zrobić, to rozróżnić, który gracz jest właścicielem, możesz umieścić je w dwóch osobnych kolekcjach i uniknąć potrzeby posiadania nieruchomości lub dziedziczenia. Same elementy mogą być błogo nieświadome, do którego gracza należą.
Gyan alias Gary Buyn
1
Myślę, że patrzymy na to samo z dwóch różnych perspektyw. Gdyby obie strony były czerwone i niebieskie, czy ich zachowanie byłoby inne niż w przypadku czerni i bieli? Powiedziałbym „nie”, dlatego mówię, że kolor nie jest przyczyną różnic behawioralnych. To tylko subtelne rozróżnienie, ale dlatego w logice gry użyłbym własności, a directionmoże playerwłasności color.
Gyan alias Gary Buyn
1
@Freshblood white castle's moves differ from black's- jak? Masz na myśli roszadę? Zarówno roszada po stronie króla, jak i roszada po stronie królowej są w 100% symetryczne dla czerni i bieli.
Konrad Morawski
4

Powinieneś zadać sobie pytanie, dlaczego tak naprawdę potrzebujesz tych oświadczeń w swojej klasie pionków:

    if (this.Color == Color.White)
    {

    }
    else
    {
    }

Jestem pewien, że wystarczy mieć mały zestaw funkcji takich jak

public abstract class Piece
{
    bool DoesPieceHaveSameColor(Piece otherPiece) { /*...*/   }

    Color OtherColor{get{/*...*/}}
    // ...
}

w swojej klasie Piece i zaimplementuj wszystkie funkcje w Pawn(i innych pochodnych Piece) za pomocą tych funkcji, bez żadnej z powyższych ifinstrukcji. Jedynym wyjątkiem może być funkcja określająca kierunek ruchu pionka na podstawie jego koloru, ponieważ jest on specyficzny dla pionków, ale w prawie wszystkich innych przypadkach nie będziesz potrzebować żadnego kodu specyficznego dla koloru.

Innym interesującym pytaniem jest to, czy naprawdę powinieneś mieć różne podklasy dla różnych rodzajów elementów. Wydaje mi się to rozsądne i naturalne, ponieważ zasady ruchów dla każdego elementu są różne i na pewno możesz to zaimplementować, używając różnych przeciążonych funkcji dla każdego rodzaju elementu.

Oczywiście, można próbować modelować to w sposób bardziej ogólny, oddzielając właściwości kawałków z ich zasadami ruchu, ale może się to skończyć w abstrakcyjnej klasy MovingRulei podklasy hierarchii MovingRulePawn, MovingRuleQueen... nie będę go uruchomić, że sposób, ponieważ może to łatwo doprowadzić do innej formy nadmiernej inżynierii.

Doktor Brown
źródło
+1 za wskazanie, że kod specyficzny dla koloru jest prawdopodobnie nie-nie. Białe i czarne pionki zachowują się zasadniczo tak samo, jak wszystkie inne pionki.
Konrad Morawski
2

Dziedziczenie nie jest tak przydatne, gdy rozszerzenie nie wpływa na możliwości zewnętrzne obiektu .

Właściwość Kolor nie wpływa, jak jest obiektem Kawałek używany . Na przykład wszystkie elementy mogą wywoływać metodę moveForward lub moveBackwards (nawet jeśli ruch jest niezgodny z prawem), ale logika elementu Piece wykorzystuje właściwość Color do określenia wartości bezwzględnej reprezentującej ruch do przodu lub do tyłu (-1 lub + 1 na „osi y”), jeśli chodzi o interfejs API, twoja nadklasa i podklasy są identycznymi obiektami (ponieważ wszystkie ujawniają te same metody).

Osobiście zdefiniowałbym niektóre stałe reprezentujące każdy typ elementu, a następnie użyłem instrukcji switch do rozgałęzienia się na prywatne metody, które zwracają możliwe ruchy dla „tego” wystąpienia.

Lew
źródło
Ruch do tyłu jest nielegalny tylko w przypadku pionków. I tak naprawdę nie muszą wiedzieć, czy są czarne czy białe - wystarczy (i logicznie), aby rozróżnić do przodu i do tyłu. BoardKlasy (na przykład), może być odpowiedzialny za przekształcenie tych pojęć +1 lub -1 w zależności od koloru w całości.
Konrad Morawski
0

Osobiście w ogóle niczego nie odziedziczyłem Piece, ponieważ nie sądzę, byś coś z tego zyskał. Przypuszczalnie kawałek nie obchodzi, w jaki sposób się porusza, tylko te kwadraty są ważnym miejscem docelowym dla następnego ruchu.

Być może możesz utworzyć prawidłowy kalkulator ruchu, który zna dostępne wzorce ruchu dla danego rodzaju elementu i może na przykład pobrać listę prawidłowych kwadratów docelowych

interface IMoveCalculator
{
    List<Square> GetValidDestinations();
}

class KnightMoveCalculator : IMoveCalculator;
class QueenMoveCalculator : IMoveCalculator;
class PawnMoveCalculator : IMoveCalculator; 
// etc.

class Piece
{
    IMoveCalculator move_calculator;
public:
    Piece(IMoveCalculator mc) : move_calculator(mc) {}
}
Ben Cottrell
źródło
Ale kto decyduje, który kawałek dostaje jaki kalkulator ruchu? Jeśli miałeś klasę bazową na sztukę i miałeś PawnPiece, możesz przyjąć takie założenie. Ale w takim tempie sam utwór mógłby po prostu zaimplementować sposób poruszania się, zgodnie z pierwotnym pytaniem
Steven Striga
@WeekendWarrior - „Osobiście niczego nie odziedziczyłem Piece”. Wydaje się, że Piece są odpowiedzialne za coś więcej niż tylko obliczanie ruchów, co jest przyczyną rozdzielenia ruchu jako odrębnej troski. Ustalenie, który kawałek dostaje, który kalkulator byłby kwestią wstrzyknięcia zależności, np.var my_knight = new Piece(new KnightMoveCalculator())
Ben Cottrell
Byłby to dobry kandydat do wzorca masy ciała. Na szachownicy jest 16 pionków i wszystkie zachowują się tak samo . To zachowanie można łatwo wyodrębnić do pojedynczej wagi muchowej. To samo dotyczy wszystkich innych elementów.
MattDavey,
-2

W tej odpowiedzi zakładam, że przez „silnik szachowy” autor pytania oznacza „szachową AI”, a nie tylko silnik sprawdzania poprawności ruchu.

Myślę, że używanie OOP do elementów i ich prawidłowych ruchów jest złym pomysłem z dwóch powodów:

  1. Siła silnika szachowego zależy w dużej mierze od tego, jak szybko potrafi on manipulować planszami, patrz np . Reprezentacje plansz szachowych . Biorąc pod uwagę poziom optymalizacji opisany w tym artykule, nie sądzę, aby zwykłe wywołanie funkcji było dostępne. Wywołanie metody wirtualnej zwiększyłoby poziom pośredni i pociągnęłoby za sobą jeszcze wyższą karę wydajności. Koszt OOP nie jest tam przystępny.
  2. Korzyści z OOP, takie jak rozszerzalność, nie przydadzą się dla pionków, ponieważ szachy prawdopodobnie nie otrzymają nowych elementów w najbliższym czasie. Realna alternatywa dla hierarchii typów opartej na dziedziczeniu obejmuje stosowanie wyliczeń i przełączania. Bardzo mało zaawansowany technologicznie i podobny do C, ale trudny do pokonania pod względem wydajności.

Odejście od rozważań dotyczących wydajności, w tym koloru elementu w hierarchii typów, jest moim zdaniem złym pomysłem. Znajdziesz się w sytuacjach, w których musisz sprawdzić, czy dwa elementy mają różne kolory, na przykład do robienia zdjęć. Sprawdzanie tego warunku można łatwo wykonać za pomocą właściwości. Robienie tego przy użyciu is WhitePiece-testów byłoby brzydsze w porównaniu.

Istnieje również inny praktyczny powód, dla którego należy unikać generowania ruchów ruchomych do metod specyficznych dla kawałków, a mianowicie, że różne pionki dzielą wspólne ruchy, np. Wieże i królowe. Aby uniknąć powielania kodu, należy rozłożyć wspólny kod na metodę podstawową i uzyskać kolejne kosztowne wywołanie.

Biorąc to pod uwagę, jeśli jest to pierwszy silnik szachowy, który piszesz, zdecydowanie radzę stosować czyste zasady programowania i unikać sztuczek optymalizacyjnych opisanych przez autora Crafty'ego. Zajmowanie się specyfiką szachowych zasad (castling, promocja, en-passant) i skomplikowanymi technikami sztucznej inteligencji (tablice skrótów, cięcie alfa-beta) jest dość trudne.

Joh
źródło
1
Może nie być celem zbyt szybkiego pisania silnika szachowego.
Freshblood
5
-1. Sądy takie jak „hipotetycznie może stać się problemem wydajności, więc jest źle”, są prawie zawsze czystym przesądem IMHO. Dziedziczenie to nie tylko „rozszerzalność” w sensie, o którym wspominacie. Różne zasady szachowe mają zastosowanie do różnych elementów, a każdy rodzaj elementu mający inną implementację metody podobnej WhichMovesArePossiblejest rozsądnym podejściem.
Doc Brown
2
Jeśli nie potrzebujesz rozszerzalności, preferowane jest podejście oparte na zamianie / wyliczaniu, ponieważ jest szybsze niż wysyłanie metody wirtualnej. Silnik szachowy jest obciążony procesorem, a operacje, które są wykonywane najczęściej (a zatem krytyczne pod względem wydajności), wykonują ruchy i cofają je. Tylko jeden poziom powyżej znajduje się lista wszystkich możliwych ruchów. Spodziewam się, że będzie to również miało krytyczne znaczenie dla czasu. Faktem jest, że zaprogramowałem silniki szachowe w C. Nawet bez OOP optymalizacje niskiego poziomu na tablicy były znaczące. Jakie doświadczenie musisz odrzucić moje?
Joh
2
@Joh: Nie zlekceważę twoich wrażeń, ale jestem pewien, że nie o to chodziło w OP. I chociaż optymalizacje niskiego poziomu mogą być znaczące dla kilku problemów, uważam, że błędem jest uczynienie ich podstawą do decyzji projektowych na wysokim poziomie - szczególnie, gdy do tej pory nie napotyka się żadnych problemów z wydajnością. Z mojego doświadczenia wynika, że ​​Knuth miał rację, mówiąc: „Przedwczesna optymalizacja jest źródłem wszelkiego zła”.
Doc Brown
1
-1 Muszę się zgodzić z Doc. Faktem jest, że ten „silnik”, o którym mówi OP, może być po prostu mechanizmem sprawdzania poprawności gry w szachy dla dwóch graczy. W takim przypadku myślenie o wydajności OOP w porównaniu z jakimkolwiek innym stylem jest całkowicie absurdalne, proste walidacje momentów zajęłyby milisekundy nawet na niskim ARM. Nie czytaj więcej na temat wymagań, niż jest to faktycznie stwierdzone.
Timothy Baldridge