Vererbung vs Zusammensetzung für Schachfiguren

9

Eine schnelle Suche in diesem Stapelaustausch zeigt, dass die Komposition im Allgemeinen als flexibler als die Vererbung angesehen wird, aber wie immer vom Projekt usw. abhängt und es Zeiten gibt, in denen die Vererbung die bessere Wahl ist. Ich möchte ein 3D-Schachspiel machen, bei dem jede Figur ein Netz hat, möglicherweise unterschiedliche Animationen und so weiter. In diesem konkreten Beispiel scheint es so, als könnten Sie für beide Ansätze argumentieren. Bin ich falsch?

Die Vererbung würde ungefähr so ​​aussehen (mit dem richtigen Konstruktor usw.)

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

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

was sicherlich dem "is-a" -Prinzip gehorcht, während die Komposition ungefähr so ​​aussehen würde

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
 }

Ist es eher eine Frage der persönlichen Präferenz oder ist ein Ansatz hier eindeutig eine klügere Wahl als der andere?

EDIT: Ich glaube, ich habe aus jeder Antwort etwas gelernt, deshalb fühle ich mich schlecht, wenn ich nur eine auswähle. Meine endgültige Lösung wird von einigen der Beiträge hier inspiriert sein (die noch daran arbeiten). Vielen Dank an alle, die sich die Zeit genommen haben zu antworten.

CanISleepYet
quelle
Ich glaube, beide können nebeneinander existieren, diese beiden dienen unterschiedlichen Zwecken, aber sie schließen sich nicht gegenseitig aus. Das Schach besteht aus verschiedenen Figuren und Brettern, und die Figuren sind von unterschiedlicher Art. Diese Stücke teilen einige grundlegende Eigenschaften und Verhaltensweisen und haben auch spezifische Eigenschaften und Verhaltensweisen. Meiner Meinung nach sollte die Komposition über Brett und Stücke angewendet werden, während die Vererbung über Arten von Stücken verfolgt werden sollte.
Hitesh Gaur
Während einige Leute behaupten, dass die gesamte Vererbung schlecht ist, ist die allgemeine Idee, dass die Vererbung der Schnittstelle gut / in Ordnung ist und dass die Vererbung der Implementierung nützlich ist, aber problematisch sein kann. Alles, was über eine einzelne Vererbungsstufe hinausgeht, ist meiner Erfahrung nach fraglich. Darüber hinaus kann es in Ordnung sein, aber es ist nicht einfach, ohne ein kompliziertes Durcheinander durchzuziehen.
JimmyJames
Das Strategiemuster ist in der Spieleprogrammierung aus einem bestimmten Grund üblich. var Pawn = new Piece(howIMove, howITake, whatILookLike)scheint mir viel einfacher, überschaubarer und wartbarer zu sein als eine Vererbungshierarchie.
Ameise P
@AntP Das ist ein guter Punkt, danke!
CanISleepYet
@AntP Machen Sie eine Antwort! ... Das hat viel zu bieten!
Svidgen

Antworten:

4

Auf den ersten Blick gibt Ihre Antwort vor, dass die "Kompositions" -Lösung keine Vererbung verwendet. Aber ich denke, Sie haben einfach vergessen, Folgendes hinzuzufügen:

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

(und mehr dieser Ableitungen, eine für jeden der sechs Stücktypen). Dies ähnelt nun eher dem klassischen "Strategie" -Muster und nutzt auch die Vererbung.

Leider weist diese Lösung einen Fehler auf: Jede Lösung enthält Piecejetzt zweimal redundant ihre Typinformationen:

  • innerhalb der Mitgliedsvariablen type

  • innen movementComponent(dargestellt durch den Subtyp)

Diese Redundanz könnte Sie wirklich in Schwierigkeiten bringen - eine einfache Klasse wie eine Piecesollte eine "einzige Quelle der Wahrheit" liefern, nicht zwei Quellen.

Natürlich können Sie versuchen, die Typinformationen nur in zu speichern typeund auch keine untergeordneten Klassen von zu erstellen MovementComponent. Aber dieses Design würde höchstwahrscheinlich zu einer großen " switch(type)" Aussage bei der Implementierung von führen GetValidMoveSquares. Und das ist definitiv ein starkes Indiz dafür, dass Vererbung hier die bessere Wahl ist .

Hinweis in der „Vererbung“ Design, ist es ganz einfach , das Feld „Typ“ zu schaffen , in einer nicht-redundanten Art und Weise: in eine virtuelle Methode GetType()zu BasePieceund setzen es entsprechend in jeder Basisklasse.

In Bezug auf "Werbung": Ich bin hier mit @svidgen, ich finde die Argumente in der Antwort von @ TheCatWhisperer umstritten.

"Werbung" als physischen Austausch von Stücken zu interpretieren, anstatt sie als Änderung des Typs desselben Stücks zu interpretieren, fühlt sich für mich ganz natürlicher an. Wenn Sie dies also auf ähnliche Weise umsetzen - indem Sie eine Figur gegen eine andere eines anderen Typs austauschen -, werden Sie höchstwahrscheinlich keine großen Probleme haben - zumindest nicht für den speziellen Fall des Schachs.

Doc Brown
quelle
Vielen Dank, dass Sie mir den Namen des Musters mitgeteilt haben. Ich wusste nicht, dass es Strategie heißt. Sie haben auch Recht, ich habe die Vererbung der Bewegungskomponente in meiner Frage verpasst. Ist der Typ wirklich redundant? Wenn ich alle Königinnen auf dem Brett beleuchten möchte, erscheint es seltsam zu überprüfen, welche Bewegungskomponente sie hatten, und ich müsste die Bewegungskomponente wirken, um zu sehen, um welchen Typ es sich handelt, der super hässlich wäre, nein?
CanISleepYet
@CanISleepYet: Das Problem mit Ihrem vorgeschlagenen "Typ" -Feld ist, dass es vom Subtyp von abweichen kann movementComponent. Siehe meine Bearbeitung, wie ein Typfeld auf sichere Weise im Entwurf "Vererbung" bereitgestellt wird.
Doc Brown
4

Ich würde wahrscheinlich die einfachere Option bevorzugen, es sei denn, Sie haben explizite, gut formulierte Gründe oder starke Bedenken hinsichtlich Erweiterbarkeit und Wartung.

Die Vererbungsoption ist weniger Codezeile und weniger Komplexität. Jeder Stücktyp hat eine "unveränderliche" 1: 1-Beziehung zu seinen Bewegungseigenschaften. (Im Gegensatz zu der Darstellung, die 1 zu 1, aber nicht unbedingt unveränderlich ist, möchten Sie möglicherweise verschiedene "Skins" anbieten.)

Wenn Sie diese unveränderliche 1: 1-Beziehung zwischen Teilen und ihrem Verhalten / ihrer Bewegung in mehrere Klassen aufteilen, fügen Sie wahrscheinlich nur Komplexität hinzu - und sonst nicht viel. Wenn ich diesen Code überprüfen oder erben würde, würde ich gute, dokumentierte Gründe für die Komplexität erwarten.

Eine noch einfachere Option , IMO, ist die Erstellung einer Piece Schnittstelle , die Ihre einzelnen Teile implementieren . In den meisten Sprachen unterscheidet sich dies stark von der Vererbung , da eine Schnittstelle Sie nicht daran hindert, eine andere Schnittstelle zu implementieren . ... In diesem Fall erhalten Sie einfach kein kostenloses Verhalten der Basisklasse: Sie möchten das gemeinsame Verhalten an einer anderen Stelle ablegen.

svidgen
quelle
Ich kaufe nicht, dass die Vererbungslösung "weniger Codezeilen" ist. Vielleicht in dieser einen Klasse, aber nicht insgesamt.
JimmyJames
@ JimmyJames Wirklich? ... Zählen Sie einfach die Codezeilen im OP ... zugegebenermaßen nicht die gesamte Lösung, aber kein schlechter Indikator dafür, welche Lösung mehr LOC und Komplexität aufweist.
Svidgen
Ich möchte das nicht zu genau sagen, weil es an dieser Stelle alles fiktiv ist, aber ja, ich denke wirklich. Meine Behauptung ist, dass dieses Stück Code im Vergleich zur gesamten Anwendung vernachlässigbar ist. Wenn dies die Implementierung der Regeln vereinfacht (umstritten), können Sie Dutzende oder sogar Hunderte von Codezeilen speichern.
JimmyJames
Danke, ich habe nicht an die Benutzeroberfläche gedacht, aber Sie haben vielleicht Recht, dass dies der beste Weg ist. Einfacher ist fast immer besser.
CanISleepYet
2

Die Sache mit Schach ist, dass das Spiel und die Spielregeln vorgeschrieben und festgelegt sind. Jedes Design, das funktioniert, ist in Ordnung - verwenden Sie, was Sie wollen! Experimentieren Sie und probieren Sie sie alle aus.

In der Geschäftswelt ist jedoch nichts so streng vorgeschrieben - Geschäftsregeln und -anforderungen ändern sich im Laufe der Zeit, und Programme müssen sich im Laufe der Zeit ändern, um sie zu berücksichtigen. Hier macht is-a vs. has-a vs. data einen Unterschied. Einfachheit erleichtert die Wartung komplexer Lösungen im Laufe der Zeit und bei sich ändernden Anforderungen. Im Allgemeinen müssen wir uns im Geschäftsleben auch mit der Persistenz befassen, die auch eine Datenbank umfassen kann. Wir haben also Regeln wie nicht mehrere Klassen verwenden, wenn eine einzelne Klasse die Arbeit erledigt, und keine Vererbung verwenden, wenn die Komposition ausreicht. Diese Regeln zielen jedoch alle darauf ab, den Code angesichts sich ändernder Regeln und Anforderungen langfristig wartbar zu machen - was beim Schach einfach nicht der Fall ist.

Beim Schach ist der wahrscheinlichste langfristige Wartungspfad, dass Ihr Programm immer intelligenter werden muss, was letztendlich bedeutet, dass Geschwindigkeits- und Speicheroptimierungen dominieren. Dafür müssen Sie im Allgemeinen Kompromisse eingehen, die die Lesbarkeit für die Leistung beeinträchtigen, und so bleibt selbst das beste OO-Design irgendwann auf der Strecke.

Erik Eidt
quelle
Beachten Sie, dass es viele erfolgreiche Spiele gab, die ein Konzept aufgreifen und dann einige neue Dinge in die Mischung einbringen. Wenn Sie dies in Zukunft mit Ihrem Schachspiel tun möchten, sollten Sie mögliche zukünftige Änderungen tatsächlich berücksichtigen.
Flater
2

Ich würde damit beginnen, einige Beobachtungen über die Problemdomäne (dh Schachregeln) zu machen:

  • Die Menge der gültigen Züge für eine Figur hängt nicht nur von der Art der Figur ab, sondern auch vom Zustand des Bretts (der Bauer kann sich vorwärts bewegen, wenn das Quadrat leer ist / kann sich diagonal bewegen, wenn er eine Figur erfasst).
  • Bestimmte Aktionen umfassen das Entfernen eines vorhandenen Stücks aus dem Spiel (Beförderung / Erfassen eines Stücks) oder das Verschieben mehrerer Stücke (Rochade).

Es ist umständlich, diese als Verantwortung des Stücks selbst zu modellieren, und weder Vererbung noch Komposition fühlen sich hier großartig an. Es wäre natürlicher, diese Regeln als erstklassige Bürger zu modellieren:

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

MovementRuleist eine einfache, aber flexible Schnittstelle, mit der Implementierungen den Board-Status auf jede Weise aktualisieren können, die zur Unterstützung komplexer Bewegungen wie Castling und Promotion erforderlich ist. Für Standardschach hätten Sie eine Implementierung für jede Art von Figur, aber Sie können auch verschiedene Regeln in eine GameInstanz einfügen, um verschiedene Schachvarianten zu unterstützen.

casablanca
quelle
Interessanter Ansatz - Gute Denkanstöße
CanISleepYet
1

Ich würde in diesem Fall denken, dass Vererbung die sauberere Wahl wäre. Die Komposition mag etwas eleganter sein, aber sie scheint mir etwas gezwungener zu sein.

Wenn Sie vorhatten, andere Spiele zu entwickeln, die sich bewegende Teile verwenden, die sich anders verhalten, ist die Komposition möglicherweise die bessere Wahl, insbesondere wenn Sie ein Fabrikmuster verwendet haben, um die für jedes Spiel erforderlichen Teile zu generieren.

Kurt Haldeman
quelle
2
Wie würden Sie mit Werbung durch Vererbung umgehen?
TheCatWhisperer
4
@TheCatWhisperer Wie gehst du damit um, wenn du das Spiel physisch spielst ? (Hoffentlich ersetzen Sie das Stück - Sie schnitzen den Bauern nicht neu, oder?)
svidgen
0

was sicherlich dem "ist-ein" -Prinzip gehorcht

Richtig, Sie verwenden also die Vererbung. Sie können immer noch innerhalb Ihrer Piece-Klasse komponieren, aber zumindest haben Sie ein Rückgrat. Die Komposition ist flexibler, aber die Flexibilität wird im Allgemeinen überbewertet, und in diesem Fall sicherlich, weil die Wahrscheinlichkeit, dass jemand eine neue Art von Stück einführt, bei der Sie Ihr starres Modell aufgeben müssen, gleich Null ist. Das Schachspiel wird sich nicht so schnell ändern. Vererbung gibt Ihnen ein Leitmodell, etwas, das Sie auch in Beziehung setzen können, Lehrcode, Richtung. Zusammensetzung nicht so sehr, die wichtigsten Domain-Entitäten fallen nicht auf, es ist rückgratlos, man muss das Spiel verstehen, um den Code zu verstehen.

Martin Maat
quelle
Vielen
0

Bitte verwenden Sie hier keine Vererbung. Während die anderen Antworten sicherlich viele Juwelen der Weisheit enthalten, wäre die Verwendung der Vererbung hier sicherlich ein Fehler. Die Verwendung von Komposition hilft Ihnen nicht nur bei der Bewältigung von Problemen, die Sie nicht erwarten, sondern ich sehe hier bereits ein Problem, mit dem die Vererbung nicht ordnungsgemäß umgehen kann.

Beförderung, wenn ein Bauer in ein wertvolleres Stück umgewandelt wird, könnte ein Problem mit der Vererbung sein. Technisch gesehen könnten Sie damit umgehen, indem Sie ein Teil durch ein anderes ersetzen. Dieser Ansatz weist jedoch Einschränkungen auf. Eine dieser Einschränkungen besteht darin, Statistiken pro Stück zu verfolgen, bei denen Sie die Stückinformationen bei der Promotion möglicherweise nicht zurücksetzen möchten (oder den zusätzlichen Code schreiben, um sie zu kopieren).

Was Ihr Kompositionsdesign in Ihrer Frage betrifft, halte ich es für unnötig kompliziert. Ich vermute nicht, dass Sie für jede Aktion eine separate Komponente benötigen und sich an eine Klasse pro Stücktyp halten könnten. Der Typ enum wird möglicherweise auch nicht benötigt.

Ich würde eine PieceKlasse (wie Sie bereits haben) sowie eine PieceTypeKlasse definieren. Die PieceTypeKlasse definiert alle Verfahren , die eine Schachfigur, die Königin, ect definieren müssen, wie zum Beispiel, CanMoveTo, GetPieceTypeName, ect. Dann würden die Klassen Pawnund Queen, ect praktisch von der PieceTypeKlasse erben .

TheCatWhisperer
quelle
3
Die Probleme, die Sie bei der Beförderung und Ersetzung sehen, sind trivial, IMO. Die Trennung von Bedenken erfordert so ziemlich, dass eine solche "Verfolgung pro Stück" (oder was auch immer) sowieso unabhängig gehandhabt wird . ... Alle potenziellen Vorteile, die Ihre Lösung bietet, werden durch die imaginären Bedenken verdunkelt, die Sie ansprechen möchten, IMO.
Svidgen
5
Zur Verdeutlichung: Ihre vorgeschlagene Lösung hat zweifellos einige Vorteile. Sie sind jedoch schwer in Ihrer Antwort zu finden, die sich hauptsächlich auf Probleme konzentriert , die in der "Vererbungslösung" wirklich sehr einfach zu lösen sind. ... Diese Art von Beeinträchtigung (für mich jedenfalls) von allem, was Sie verdienen, um über Ihre Meinung zu kommunizieren.
Svidgen
1
Wenn Piecees nicht virtuell ist, stimme ich dieser Lösung zu. Während das Klassendesign auf den ersten Blick etwas komplexer erscheint, denke ich, dass die Implementierung insgesamt einfacher sein wird. Die wichtigsten Dinge, die mit dem Pieceund nicht mit dem verbunden wären, PieceTypesind seine Identität und möglicherweise seine Bewegungshistorie.
JimmyJames
1
@svidgen Eine Implementierung, die ein zusammengesetztes Teil verwendet, und eine Implementierung, die ein vererbungsbasiertes Teil verwendet, sehen abgesehen von den in dieser Lösung beschriebenen Details grundsätzlich identisch aus. Diese Kompositionslösung fügt eine einzelne Indirektionsebene hinzu. Ich sehe das nicht als etwas, das es wert ist, vermieden zu werden.
JimmyJames
2
@ JimmyJames Richtig. Die Bewegungshistorie mehrerer Teile muss jedoch verfolgt und korreliert werden - IMO sollte nicht für ein einzelnes Teil verantwortlich sein. Es ist ein "separates Anliegen", wenn Sie sich für feste Dinge interessieren.
Svidgen
0

Aus meiner Sicht ist es angesichts der bereitgestellten Informationen nicht ratsam zu beantworten, ob Vererbung oder Zusammensetzung der richtige Weg sein sollten.

  • Vererbung und Komposition sind nur Werkzeuge im Werkzeuggürtel des objektorientierten Designers.
  • Mit diesen Tools können Sie viele verschiedene Modelle für die Problemdomäne entwerfen.
  • Da es viele solcher Modelle gibt, die eine Domäne darstellen können, können Sie Vererbung und Zusammensetzung auf verschiedene Arten kombinieren, um funktional äquivalente Modelle zu erhalten.

Ihre Modelle sind völlig unterschiedlich, in der ersten haben Sie sich um das Konzept der Schachfiguren gekümmert, in der zweiten um das Konzept der Bewegung der Figuren. Sie könnten funktional gleichwertig sein, und ich würde diejenige auswählen, die die Domain besser darstellt und mir hilft, leichter darüber nachzudenken.

Auch in Ihrem zweiten Entwurf zeigt die Tatsache, dass Ihre PieceKlasse ein typeFeld hat, deutlich, dass hier ein Entwurfsproblem vorliegt, da das Stück selbst von mehreren Typen sein kann. Klingt es nicht so, dass dies wahrscheinlich eine Art Vererbung verwenden sollte, bei der das Stück selbst der Typ ist?

Sie sehen, es ist sehr schwer, über Ihre Lösung zu streiten. Entscheidend ist nicht, ob Sie Vererbung oder Komposition verwendet haben, sondern ob Ihr Modell den Bereich des Problems genau widerspiegelt und ob es nützlich ist, darüber nachzudenken und eine Implementierung bereitzustellen.

Es erfordert einige Erfahrung, um Vererbung und Komposition richtig zu verwenden, aber es handelt sich um völlig unterschiedliche Werkzeuge, und ich glaube nicht, dass man das eine durch das andere "ersetzen" kann, obwohl ich zustimmen kann, dass ein System vollständig nur mit Komposition entworfen werden kann.

edalorzo
quelle
Sie machen einen guten Punkt, dass es das Modell ist, das wichtig ist und nicht das Werkzeug. Hilft die Vererbung in Bezug auf den redundanten Typ wirklich? Wenn ich zum Beispiel alle Bauern hervorheben oder prüfen möchte, ob ein Stück ein König ist, brauche ich dann kein Casting?
CanISleepYet
-1

Nun, ich schlage weder vor.

Während die Objektorientierung ein nützliches Werkzeug ist und oft zur Vereinfachung beiträgt, ist sie nicht das einzige Werkzeug im Werkzeugschuppen.

Erwägen Sie stattdessen die Implementierung einer Board-Klasse, die ein einfaches Bytearray enthält.
Das Interpretieren und Berechnen erfolgt in den Elementfunktionen mithilfe von Bitmaskierung und Umschaltung.

Verwenden Sie das MSB für den Besitzer und lassen Sie 7 Bits für das Stück übrig, reservieren Sie jedoch Null für leer.
Alternativ Null für leer, Zeichen für den Besitzer, absoluter Wert für das Stück.

Wenn Sie möchten, können Sie Bitfelder verwenden, um eine manuelle Maskierung zu vermeiden, obwohl diese nicht portierbar sind:

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
quelle
Interessant ... und passend, wenn man bedenkt, dass es sich um eine C ++ - Frage handelt. Aber da ich kein C ++ - Programmierer bin, wäre dies nicht mein erster (oder zweiter oder dritter ) Instinkt! ... Das vielleicht ist die meist C ++ Antwort hier aber!
Svidgen
7
Dies ist eine Mikrooptimierung, der keine echte Anwendung folgen sollte.
Lie Ryan
@LieRyan Wenn du nur OOP hast, riecht alles nach einem Objekt. Und ob es wirklich das "Mikro" ist, ist auch eine interessante Frage. Je nachdem, was Sie damit machen , kann dies von großer Bedeutung sein.
Deduplikator
Heh ... das heißt , C ++ ist objektorientiert. Ich nehme an, es gibt einen Grund, warum das OP C ++ anstelle von nur C verwendet .
Svidgen
3
Hier mache ich mir weniger Sorgen um das Fehlen von OOP, sondern verschleiere den Code mit ein bisschen Twiddling. Sofern Sie keinen Code für 8-Bit-Nintendo schreiben oder Algorithmen schreiben, die den Umgang mit Millionen von Kopien des Board-Status erfordern, ist dies eine vorzeitige Mikrooptimierung und nicht ratsam. In den normalen Szenarien wird es wahrscheinlich sowieso nicht schneller sein, da der nicht ausgerichtete Bitzugriff zusätzliche Bitoperationen erfordert.
Lie Ryan