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.
quelle
var Pawn = new Piece(howIMove, howITake, whatILookLike)
scheint mir viel einfacher, überschaubarer und wartbarer zu sein als eine Vererbungshierarchie.Antworten:
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:
(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
Piece
jetzt 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
Piece
sollte eine "einzige Quelle der Wahrheit" liefern, nicht zwei Quellen.Natürlich können Sie versuchen, die Typinformationen nur in zu speichern
type
und auch keine untergeordneten Klassen von zu erstellenMovementComponent
. Aber dieses Design würde höchstwahrscheinlich zu einer großen "switch(type)
" Aussage bei der Implementierung von führenGetValidMoveSquares
. 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()
zuBasePiece
und 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.
quelle
movementComponent
. Siehe meine Bearbeitung, wie ein Typfeld auf sichere Weise im Entwurf "Vererbung" bereitgestellt wird.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.quelle
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.
quelle
Ich würde damit beginnen, einige Beobachtungen über die Problemdomäne (dh Schachregeln) zu machen:
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:
MovementRule
ist 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 eineGame
Instanz einfügen, um verschiedene Schachvarianten zu unterstützen.quelle
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.
quelle
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.
quelle
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
Piece
Klasse (wie Sie bereits haben) sowie einePieceType
Klasse definieren. DiePieceType
Klasse definiert alle Verfahren , die eine Schachfigur, die Königin, ect definieren müssen, wie zum Beispiel,CanMoveTo
,GetPieceTypeName
, ect. Dann würden die KlassenPawn
undQueen
, ect praktisch von derPieceType
Klasse erben .quelle
Piece
es 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 demPiece
und nicht mit dem verbunden wären,PieceType
sind seine Identität und möglicherweise seine Bewegungshistorie.Aus meiner Sicht ist es angesichts der bereitgestellten Informationen nicht ratsam zu beantworten, ob Vererbung oder Zusammensetzung der richtige Weg sein sollten.
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
Piece
Klasse eintype
Feld 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.
quelle
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:
quelle