Wann soll die Vererbung gestoppt werden?

9

Vor einiger Zeit habe ich eine Frage zu Stack Overflow über die Vererbung gestellt.

Ich habe gesagt, ich entwerfe Schachmaschine in OOP-Mode. Also erbe ich alle meine Stücke von Piece Abstract Class, aber die Vererbung geht immer noch. Lassen Sie mich durch Code zeigen

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

public abstract class Pawn: Piece {}

public class WhitePawn :Pawn {}

public class BlackPawn:Pawn {}

Programmierer haben mein Design ein wenig über das Engineering hinaus gefunden und vorgeschlagen, farbige Stückklassen zu entfernen und Stückfarbe als Eigenschaftsmitglied wie unten zu halten.

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

Auf diese Weise kann Piece seine Farbe erkennen. Nach der Implementierung habe ich gesehen, dass meine Implementierung wie folgt verläuft.

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
        {

        }
    }
}

Jetzt sehe ich, dass die Farbeigenschaft if-Anweisungen in Implementierungen verursacht. Ich habe das Gefühl, wir brauchen bestimmte Farbstücke in der Vererbungshierarchie.

Hätten Sie in einem solchen Fall Klassen wie WhitePawn, BlackPawn oder würden Sie zum Design gehen, indem Sie die Color-Eigenschaft beibehalten?

Wie würden Sie ohne ein solches Problem mit dem Design beginnen wollen? Farbeigenschaft behalten oder Vererbungslösung haben?

Bearbeiten: Ich möchte angeben, dass mein Beispiel möglicherweise nicht vollständig mit dem Beispiel aus dem wirklichen Leben übereinstimmt. Bevor Sie also versuchen, Implementierungsdetails zu erraten, konzentrieren Sie sich einfach auf die Frage.

Ich frage eigentlich nur, ob die Verwendung der Color-Eigenschaft dazu führt, dass Anweisungen die Vererbung besser verwenden.

Frisches Blut
quelle
3
Ich verstehe nicht wirklich, warum Sie die Stücke überhaupt so modellieren würden. Wenn Sie MakeMove () aufrufen, welchen Zug wird es machen? Ich würde sagen, Sie müssen zunächst den Zustand des Boards modellieren und sehen, welche Klassen Sie dafür benötigen
jk.
11
Ich denke, Ihr grundlegendes Missverständnis besteht nicht darin, zu erkennen, dass "Farbe" im Schach eher ein Attribut des Spielers als der Figur ist. Deshalb sagen wir "Schwarze bewegen sich" usw. Die Farbe dient dazu, festzustellen, welchem ​​Spieler das Stück gehört. Ein Bauer folgt den gleichen Bewegungsregeln und -beschränkungen, egal welche Farbe er hat. Die einzige Variation ist, welche Richtung "vorwärts" ist.
James Anderson
5
Wenn Sie nicht herausfinden können, wann die Vererbung beendet werden soll, lassen Sie sie besser vollständig fallen. Sie können die Vererbung in einer späteren Entwurfs-, Implementierungs- und Wartungsphase wieder einführen, wenn die Hierarchie für Sie offensichtlich wird. Ich benutze diesen Ansatz, funktioniert wie ein Zauber
Mücke
1
Die schwierigere Frage ist, ob für jeden Stücktyp eine Unterklasse erstellt werden soll oder nicht. Der Stücktyp bestimmt mehr Verhaltensaspekte als die Stückfarbe.
NoChance
3
Wenn Sie versucht sind, ein Design mit ABCs (Abstract Base Classes) zu beginnen, tun Sie uns allen einen Gefallen und tun Sie es nicht ... Die Fälle, in denen ABCs tatsächlich nützlich sind, sind sehr selten und selbst dann ist es normalerweise nützlicher, eine Schnittstelle zu verwenden stattdessen.
Evan Plaice

Antworten:

12

Stellen Sie sich vor, Sie entwerfen eine Anwendung, die Fahrzeuge enthält. Schaffst du so etwas?

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

Im wirklichen Leben ist Farbe eine Eigenschaft eines Objekts (entweder eines Autos oder einer Schachfigur) und muss durch eine Eigenschaft dargestellt werden.

Sind MakeMoveund TakeBackMoveanders für Schwarze und Weiße? Überhaupt nicht: Die Regeln sind für beide Spieler gleich, was bedeutet, dass diese beiden Methoden für Schwarze und Weiße genau gleich sind , was bedeutet, dass Sie eine Bedingung hinzufügen, bei der Sie sie nicht benötigen.

Auf der anderen Seite, wenn Sie haben WhitePawnund BlackPawn, werde ich nicht überrascht sein, wenn Sie bald oder später schreiben werden:

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

oder sogar so etwas wie:

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
quelle
4
@Freshblood Ich denke, es wäre besser, eine directionEigenschaft zu haben und diese zum Verschieben zu verwenden, als die Farbeigenschaft zu verwenden. Farbe sollte idealerweise nur zum Rendern verwendet werden, nicht für die Logik.
Gyan alias Gary Buyn
7
Außerdem bewegen sich die Teile in die gleiche Richtung, vorwärts, rückwärts, links, rechts. Der Unterschied besteht darin, von welcher Seite des Boards sie starten. Auf einer Straße bewegen sich Autos auf gegenüberliegenden Fahrspuren vorwärts.
Slipset
1
@Blrfl Sie haben möglicherweise Recht, dass die directionEigenschaft ein Boolescher Wert ist. Ich sehe nicht, was daran falsch ist. Was mich bis zu Freshbloods obigem Kommentar verwirrt hat, ist, warum er die Bewegungslogik basierend auf der Farbe ändern möchte. Ich würde sagen, das ist schwieriger zu verstehen, weil es keinen Sinn macht, dass Farbe das Verhalten beeinflusst. Wenn Sie nur unterscheiden möchten, welcher Spieler welches Stück besitzt, können Sie diese in zwei separate Sammlungen einteilen und die Notwendigkeit einer Eigenschaft oder Vererbung vermeiden. Die Figuren selbst könnten selig nicht wissen, zu welchem ​​Spieler sie gehören.
Gyan alias Gary Buyn
1
Ich denke, wir betrachten dasselbe aus zwei verschiedenen Perspektiven. Wenn die beiden Seiten rot und blau wären, würde sich ihr Verhalten von dem von Schwarz und Weiß unterscheiden? Ich würde nein sagen, was der Grund ist, warum ich sage, dass die Farbe nicht die Ursache für Verhaltensunterschiede ist. Es ist nur eine subtile Unterscheidung, aber deshalb würde ich eine directionoder vielleicht eine playerEigenschaft anstelle coloreiner in der Spiellogik verwenden.
Gyan alias Gary Buyn
1
@Freshblood white castle's moves differ from black's- wie? Meinst du Rochade? Sowohl die königliche als auch die königliche Rochade sind für Schwarz und Weiß zu 100% symmetrisch.
Konrad Morawski
4

Was Sie sich fragen sollten, ist, warum Sie diese Aussagen in Ihrer Bauernklasse wirklich brauchen:

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

    }
    else
    {
    }

Ich bin mir ziemlich sicher, dass es ausreichen wird, einen kleinen Satz von Funktionen wie zu haben

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

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

in Ihrer Piece-Klasse und implementieren Sie alle Funktionen in Pawn(und die anderen Ableitungen von Piece) unter Verwendung dieser Funktionen, ohne jemals eine der ifobigen Anweisungen zu haben. Einzige Ausnahme kann die Funktion sein, die Bewegungsrichtung für einen Bauern anhand seiner Farbe zu bestimmen, da dies für Bauern spezifisch ist. In fast allen anderen Fällen benötigen Sie jedoch keinen farbspezifischen Code.

Die andere interessante Frage ist, ob Sie wirklich verschiedene Unterklassen für verschiedene Arten von Stücken haben sollten. Das erscheint mir vernünftig und natürlich, da die Regeln für die Bewegungen für jedes Stück unterschiedlich sind und Sie dies sicherlich implementieren können, indem Sie für jede Art von Stück unterschiedliche überladene Funktionen verwenden.

Natürlich könnte man versuchen , dies in einer allgemeinere Art und Weise zu modellieren, indem Sie die Eigenschaften der Stücke aus ihrem bewegten Regeln zu trennen, aber das kann in einer abstrakten Klasse beenden MovingRuleund eine Unterklasse Hierarchie MovingRulePawn, MovingRuleQueen... würde ich anfangen es nicht , dass Weg, da es leicht zu einer anderen Form der Überentwicklung führen könnte.

Doc Brown
quelle
+1 für den Hinweis, dass farbspezifischer Code wahrscheinlich ein Nein-Nein ist. Weiße und schwarze Bauern verhalten sich im Wesentlichen genauso wie alle anderen Teile.
Konrad Morawski
2

Die Vererbung ist nicht so nützlich, wenn die Erweiterung die externen Funktionen des Objekts nicht beeinträchtigt .

Die Color-Eigenschaft hat keinen Einfluss darauf, wie ein Piece-Objekt verwendet wird . Beispielsweise könnten alle Teile möglicherweise eine moveForward- oder moveBackwards-Methode aufrufen (auch wenn die Verschiebung nicht zulässig ist), aber die gekapselte Logik des Stücks verwendet die Color-Eigenschaft, um den Absolutwert zu bestimmen, der eine Vorwärts- oder Rückwärtsbewegung darstellt (-1 oder +) 1 auf der "y-Achse") Für Ihre API sind Ihre Ober- und Unterklassen identische Objekte (da sie alle dieselben Methoden verfügbar machen).

Persönlich würde ich einige Konstanten definieren, die jeden Stücktyp darstellen, und dann eine switch-Anweisung verwenden, um in private Methoden zu verzweigen, die mögliche Bewegungen für "diese" Instanz zurückgeben.

Löwe
quelle
Rückwärtsbewegung ist nur bei Bauern illegal. Und sie müssen nicht wirklich wissen, ob sie schwarz oder weiß sind - es ist ausreichend (und logisch), um sie vorwärts und rückwärts unterscheiden zu lassen. Die BoardKlasse (zum Beispiel) könnte dafür verantwortlich sein, diese Konzepte je nach Farbe des Stücks in -1 oder +1 umzuwandeln.
Konrad Morawski
0

Persönlich würde ich überhaupt nichts erben Piece, weil ich nicht glaube, dass Sie dadurch etwas gewinnen. Vermutlich ist es einem Stück eigentlich egal, wie es sich bewegt, nur welche Felder ein gültiges Ziel für seinen nächsten Zug sind.

Vielleicht könnten Sie einen gültigen Bewegungsrechner erstellen, der die verfügbaren Bewegungsmuster für einen Stücktyp kennt und beispielsweise eine Liste gültiger Zielquadrate abrufen kann

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
quelle
Aber wer bestimmt, welches Stück welchen Bewegungsrechner bekommt? Wenn Sie eine Stückbasisklasse und PawnPiece hätten, könnten Sie diese Annahme machen. Aber dann könnte das Stück selbst einfach umsetzen, wie es sich bewegt, wie in der ursprünglichen Frage entworfen
Steven Striga
@WeekendWarrior - "Persönlich würde ich überhaupt nichts erben Piece". Es scheint, dass Piece nicht nur für die Berechnung von Bewegungen verantwortlich ist, sondern auch für die Aufteilung von Bewegungen als eigenständiges Anliegen. Zu bestimmen, welches Stück welchen Taschenrechner bekommt, wäre eine Frage der Abhängigkeitsinjektion, z. B.var my_knight = new Piece(new KnightMoveCalculator())
Ben Cottrell,
Dies wäre ein guter Kandidat für das Fliegengewichtsmuster. Auf einem Schachbrett befinden sich 16 Bauern, die sich alle gleich verhalten . Dieses Verhalten könnte leicht in ein einzelnes Fliegengewicht extrahiert werden. Gleiches gilt für alle anderen Stücke.
MattDavey
-2

In dieser Antwort gehe ich davon aus, dass der Autor der Frage unter "Schach-Engine" "Schach-KI" versteht, nicht nur eine Bewegungsvalidierungs-Engine.

Ich denke, die Verwendung von OOP für Stücke und ihre gültigen Bewegungen ist aus zwei Gründen eine schlechte Idee:

  1. Die Stärke einer Schachengine hängt stark davon ab, wie schnell sie Bretter manipulieren kann, siehe z. B. Darstellungen von Schachprogrammbrettern . Angesichts des in diesem Artikel beschriebenen Optimierungsgrades halte ich einen regulären Funktionsaufruf nicht für erschwinglich. Ein Aufruf einer virtuellen Methode würde eine Indirektionsebene hinzufügen und eine noch höhere Leistungseinbuße verursachen. Die Kosten für OOP sind dort nicht erschwinglich.
  2. Die Vorteile von OOP wie die Erweiterbarkeit sind für Figuren nicht von Nutzen, da Schach wahrscheinlich nicht bald neue Figuren bekommt. Eine praktikable Alternative zur vererbungsbasierten Typhierarchie ist die Verwendung von Enums und Switch. Sehr Low-Tech und C-ähnlich, aber in Bezug auf die Leistung kaum zu übertreffen.

Es ist meiner Meinung nach eine schlechte Idee, sich von Leistungsüberlegungen zu entfernen, einschließlich der Farbe des Stücks in der Typhierarchie. Sie befinden sich in Situationen, in denen Sie überprüfen müssen, ob zwei Teile unterschiedliche Farben haben, beispielsweise zum Erfassen. Das Überprüfen dieses Zustands kann einfach mithilfe einer Eigenschaft durchgeführt werden. Dies mit is WhitePiece-tests zu tun, wäre im Vergleich hässlicher.

Es gibt noch einen weiteren praktischen Grund, um zu vermeiden, dass die Bewegungsgenerierung auf stückspezifische Methoden verschoben wird, nämlich dass verschiedene Teile gemeinsame Bewegungen haben, z. B. Türme und Königinnen. Um doppelten Code zu vermeiden, müssten Sie allgemeinen Code in eine Basismethode zerlegen und einen weiteren kostspieligen Aufruf durchführen.

Abgesehen davon, wenn es die erste Schach-Engine ist, die Sie schreiben, würde ich definitiv empfehlen, saubere Programmierprinzipien zu verwenden und die von Craftys Autor beschriebenen Optimierungstricks zu vermeiden. Der Umgang mit den Besonderheiten von Schachregeln (Castling, Promotion, En-Passant) und komplexen KI-Techniken (Hashing Boards, Alpha-Beta-Cut) ist schwierig genug.

Joh
quelle
1
Es ist möglicherweise nicht sinnvoll, die Schachengine zu schnell zu schreiben.
Freshblood
5
-1. Urteile wie "hypothetisch könnte es zu einem Leistungsproblem werden, also ist es schlecht" sind meiner Meinung nach fast immer reiner Aberglaube. Und bei der Vererbung geht es nicht nur um "Erweiterbarkeit" in dem Sinne, wie Sie es erwähnen. Für verschiedene Figuren gelten unterschiedliche Schachregeln, und jede Art von Figur mit einer anderen Implementierung einer Methode wie WhichMovesArePossibleist ein vernünftiger Ansatz.
Doc Brown
2
Wenn Sie keine Erweiterbarkeit benötigen, ist ein auf switch / enum basierender Ansatz vorzuziehen, da er schneller ist als ein Versand mit virtuellen Methoden. Eine Schachengine ist CPU-schwer, und die Operationen, die am häufigsten ausgeführt werden (und daher effizient sind), führen Züge aus und machen sie rückgängig. Nur eine Ebene darüber werden alle möglichen Züge aufgelistet. Ich gehe davon aus, dass dies auch zeitkritisch sein wird. Tatsache ist, dass ich Schach-Engines in C programmiert habe. Auch ohne OOP waren Optimierungen auf niedriger Ebene in der Board-Darstellung von Bedeutung. Welche Art von Erfahrung müssen Sie meine entlassen?
Joh
2
@Joh: Ich werde deine Erfahrung nicht missachten, aber ich bin mir ziemlich sicher, dass das OP nicht danach war. Und obwohl Optimierungen auf niedriger Ebene für mehrere Probleme von Bedeutung sein können, halte ich es für einen Fehler, sie als Grundlage für Entwurfsentscheidungen auf hoher Ebene zu verwenden - insbesondere dann, wenn bisher keine Leistungsprobleme auftreten. Ich habe die Erfahrung gemacht, dass Knuth sehr Recht hatte, als er sagte: "Vorzeitige Optimierung ist die Wurzel allen Übels."
Doc Brown
1
-1 Ich muss Doc zustimmen. Tatsache ist, dass diese "Engine", von der das OP spricht, einfach die Validierungs-Engine für ein 2-Spieler-Schachspiel sein könnte. In diesem Fall ist es völlig lächerlich, über die Leistung von OOP im Vergleich zu anderen Stilen nachzudenken. Einfache Validierungen von Momenten würden selbst auf einem Low-End-ARM-Gerät Millisekunden dauern. Lesen Sie nicht mehr in die Anforderungen als tatsächlich angegeben.
Timothy Baldridge