Quand arrêter l'héritage?

9

Il y a une fois, j'ai posé une question sur Stack Overflow sur l'héritage.

J'ai dit que je conçois un moteur d'échecs de façon OOP. J'hérite donc tous mes morceaux de la classe abstraite Piece mais l'héritage continue. Permettez-moi de montrer par code

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

public abstract class Pawn: Piece {}

public class WhitePawn :Pawn {}

public class BlackPawn:Pawn {}

Les programmeurs ont trouvé ma conception un peu trop technique et ont suggéré de supprimer les classes de pièces colorées et de conserver la couleur des pièces en tant que membre de la propriété, comme ci-dessous.

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

Ainsi, Piece peut connaître sa couleur. Après la mise en œuvre, j'ai vu ma mise en œuvre comme ci-dessous.

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
        {

        }
    }
}

Maintenant, je vois que la propriété keep color provoque des instructions if dans les implémentations. Cela me fait sentir que nous avons besoin de pièces de couleur spécifiques dans la hiérarchie d'héritage.

Dans un tel cas, auriez-vous des classes comme WhitePawn, BlackPawn ou iriez-vous à la conception en conservant la propriété Color?

Sans avoir vu un tel problème, comment voudriez-vous commencer la conception? Conserver la propriété Couleur ou avoir une solution d'héritage?

Edit: je veux spécifier que mon exemple peut ne pas correspondre complètement à l'exemple de la vie réelle. Donc, avant d'essayer de deviner les détails de la mise en œuvre, concentrez-vous simplement sur la question.

En fait, je demande simplement si l'utilisation de la propriété Color entraînera une meilleure utilisation de l'héritage par les instructions?

Sang frais
la source
3
Je ne comprends pas vraiment pourquoi vous modéliseriez les pièces comme ça du tout. Lorsque vous appelez MakeMove () quel mouvement cela fera-t-il? Je dirais que ce dont vous avez vraiment besoin pour commencer, c'est de modéliser l'état de la carte et de voir de quelles classes vous avez besoin pour cela
jk.
11
Je pense que votre idée fausse de base n'est pas de réaliser que dans les échecs, la "couleur" est vraiment un attribut du joueur plutôt que la pièce. C'est pourquoi nous disons "les noirs se déplacent", etc., la coloration est là pour identifier quel joueur possède la pièce. Un pion suit les mêmes règles et restrictions de mouvement, quelle que soit la couleur, la seule variation est la direction "vers l'avant".
James Anderson
5
quand vous ne pouvez pas comprendre l'héritage «quand arrêter», mieux vaut le supprimer complètement. Vous pouvez réintroduire l'héritage à une étape ultérieure de conception / mise en œuvre / maintenance, lorsque la hiérarchie deviendra évidente pour vous. J'utilise cette approche, fonctionne comme un charme
moucheron
1
La question la plus difficile consiste à créer ou non une sous-classe pour chaque type de pièce. Le type de pièce détermine plus d'aspects comportementaux que la couleur de la pièce.
NoChance
3
Si vous êtes tenté de commencer une conception avec des ABC (classes de base abstraites), faites-nous tous une faveur et ne le faites pas ... Les cas où les ABC sont réellement utiles sont très rares et même dans ce cas, il est généralement plus utile d'utiliser une interface au lieu.
Evan Plaice

Réponses:

12

Imaginez que vous conceviez une application qui contient des véhicules. Créez-vous quelque chose comme ça?

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

Dans la vraie vie, la couleur est une propriété d'un objet (une voiture ou une pièce d'échecs) et doit être représentée par une propriété.

Sont MakeMoveet TakeBackMovedifférents pour les noirs et les blancs? Pas du tout: les règles sont les mêmes pour les deux joueurs, ce qui signifie que ces deux méthodes seront exactement les mêmes pour les noirs et les blancs , ce qui signifie que vous ajoutez une condition où vous n'en avez pas besoin.

D'un autre côté, si vous en avez WhitePawnet BlackPawnje ne serai pas surpris si tôt ou tard vous écrirez:

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

ou même quelque chose comme:

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
la source
4
@Freshblood Je pense qu'il vaudrait mieux avoir une directionpropriété et l'utiliser pour se déplacer que d'utiliser la propriété color. La couleur ne devrait idéalement être utilisée que pour le rendu, pas pour la logique.
Gyan aka Gary Buyn
7
En outre, les pièces se déplacent dans la même direction, en avant, en arrière, à gauche, à droite. La différence est de quel côté du plateau ils commencent. Sur une route, les voitures des voies opposées avancent toutes les deux.
puces
1
@Blrfl Vous avez peut-être raison de dire que la directionpropriété est un booléen. Je ne vois pas ce qui ne va pas avec ça. Ce qui m'a troublé à propos de la question jusqu'au commentaire de Freshblood ci-dessus, c'est pourquoi il voudrait changer la logique du mouvement en fonction de la couleur. Je dirais que c'est plus difficile à comprendre car cela n'a aucun sens que la couleur affecte le comportement. Si tout ce que vous voulez faire est de distinguer quel joueur possède quelle pièce, vous pouvez les mettre dans deux collections distinctes et éviter d'avoir besoin d'une propriété ou d'un héritage. Les pièces elles-mêmes pourraient parfaitement ignorer à quel joueur elles appartiennent.
Gyan aka Gary Buyn
1
Je pense que nous examinons la même chose sous deux angles différents. Si les deux côtés étaient rouges et bleus , leur comportement serait-il différent de ceux en noir et blanc? Je dirais non, c'est la raison pour laquelle je dis que la couleur n'est pas la cause de différences de comportement. Ce n'est qu'une distinction subtile, mais c'est pourquoi j'utiliserais une directionou peut-être une playerpropriété au lieu d'une colordans la logique du jeu.
Gyan aka Gary Buyn
1
@Freshblood white castle's moves differ from black's- comment? Voulez-vous dire roque? Le roque côté roi et le roque côté reine sont 100% symétriques pour le noir et le blanc.
Konrad Morawski
4

Ce que vous devez vous demander, c'est pourquoi vous avez vraiment besoin de ces déclarations dans votre classe Pawn:

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

    }
    else
    {
    }

Je suis presque sûr qu'il suffira d'avoir un petit ensemble de fonctions comme

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

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

dans votre classe Piece et implémentez toutes les fonctions dans Pawn(et les autres dérivations de Piece) en utilisant ces fonctions, sans jamais avoir l'une de ces ifinstructions ci-dessus. La seule exception peut être la fonction pour déterminer la direction de déplacement d'un pion par sa couleur, car cela est spécifique aux pions, mais dans presque tous les autres cas, vous n'aurez pas besoin de code spécifique à la couleur.

L'autre question intéressante est de savoir si vous devriez vraiment avoir différentes sous-classes pour différents types de pièces. Cela me semble raisonnable et naturel, car les règles de déplacement pour chaque pièce sont différentes et vous pouvez sûrement l'implémenter en utilisant différentes fonctions surchargées pour chaque type de pièce.

Bien sûr, on pourrait essayer de modéliser cela d'une manière plus générale, en séparant les propriétés des pièces de leurs règles en mouvement, mais cela peut se terminer par une classe abstraite MovingRuleet une hiérarchie de sous - classe MovingRulePawn, MovingRuleQueen... je ne commencerais pas ce que car cela pourrait facilement conduire à une autre forme de suringénierie.

Doc Brown
la source
+1 pour avoir souligné que le code spécifique à la couleur est probablement un non-non. Les pions blancs et noirs se comportent essentiellement de la même manière, comme toutes les autres pièces.
Konrad Morawski
2

L'héritage n'est pas aussi utile lorsque l'extension n'affecte pas les capacités externes de l'objet .

La propriété Color n'affecte pas la façon dont un objet Piece est utilisé . Par exemple, toutes les pièces pourraient invoquer une méthode moveForward ou moveBackwards (même si le mouvement n'est pas légal), mais la logique encapsulée de la pièce utilise la propriété Color pour déterminer la valeur absolue qui représente un mouvement vers l'avant ou vers l'arrière (-1 ou + 1 sur l '"axe y"), en ce qui concerne votre API, votre superclasse et vos sous-classes sont des objets identiques (car ils exposent tous les mêmes méthodes).

Personnellement, je définirais des constantes qui représentent chaque type de pièce, puis j'utiliserais une instruction switch pour créer des méthodes privées renvoyant les mouvements possibles pour "cette" instance.

Leo
la source
Le mouvement en arrière n'est illégal qu'en cas de pions. Et ils n'ont pas vraiment besoin de savoir s'ils sont noirs ou blancs - c'est suffisant (et logique) pour les distinguer avant / arrière. La Boardclasse (par exemple) pourrait être en charge de convertir ces concepts en -1 ou +1 selon la couleur de la pièce.
Konrad Morawski
0

Personnellement, je n'hériterais de rien du Piecetout, car je ne pense pas que vous gagniez quoi que ce soit à cela. Vraisemblablement, une pièce ne se soucie pas vraiment de la façon dont elle se déplace, mais uniquement des cases qui sont une destination valide pour son prochain mouvement.

Vous pourriez peut-être créer une calculatrice de déplacement valide qui connaît les modèles de mouvement disponibles pour un type de pièce et est capable de récupérer une liste de carrés de destination valides, par exemple

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
la source
Mais qui détermine quelle pièce obtient quelle calculatrice de mouvement? Si vous aviez une classe de base de pièces et que vous aviez PawnPiece, vous pourriez faire cette supposition. Mais à ce rythme, la pièce elle-même pourrait simplement implémenter la façon dont elle se déplace, comme conçu dans la question d'origine
Steven Striga
@WeekendWarrior - "Personnellement, je n'hériterais de rien du Piecetout". Il semble que Piece soit responsable de plus que de simples calculs de mouvements, ce qui est la raison pour laquelle le mouvement est divisé en une préoccupation distincte. Déterminer quelle pièce obtient quelle calculatrice serait une question d'injection de dépendance, par exemplevar my_knight = new Piece(new KnightMoveCalculator())
Ben Cottrell
Ce serait un bon candidat pour le modèle de poids mouche. Il y a 16 pions sur un échiquier et ils partagent tous le même comportement . Ce comportement pourrait facilement être extrait en une seule masselotte. Il en va de même pour toutes les autres pièces.
MattDavey
-2

Dans cette réponse, je suppose que par "moteur d'échecs", l'auteur de la question signifie "AI d'échecs", pas simplement un moteur de validation de mouvement.

Je pense que l'utilisation de la POO pour les pièces et leurs mouvements valides est une mauvaise idée pour deux raisons:

  1. La force d'un moteur d'échecs dépend fortement de la rapidité avec laquelle il peut manipuler les tableaux, voir par exemple les représentations des tableaux des programmes d'échecs . Compte tenu du niveau d'optimisation décrit dans cet article, je ne pense pas qu'un appel de fonction régulier soit abordable. Un appel à une méthode virtuelle ajouterait un niveau d'indirection et entraînerait une pénalité de performance encore plus élevée. Le coût de la POO n'est pas abordable là-bas.
  2. Les avantages de la POO tels que l'extensibilité ne sont d'aucune utilité pour les pièces, car les échecs ne sont pas susceptibles d'obtenir de nouvelles pièces de sitôt. Une alternative viable à la hiérarchie de types basée sur l'héritage comprend l'utilisation d'énumérations et d'un commutateur. Très low-tech et C-like, mais difficile à battre en termes de performances.

S'éloigner des considérations de performance, y compris la couleur de la pièce dans la hiérarchie des types, est à mon avis une mauvaise idée. Vous vous retrouverez dans des situations où vous devrez vérifier si deux pièces ont des couleurs différentes, par exemple pour la capture. La vérification de cette condition se fait facilement à l'aide d'une propriété. Le faire en utilisant is WhitePiece-tests serait plus laid en comparaison.

Il y a aussi une autre raison pratique pour éviter de déplacer la génération de mouvements vers des méthodes spécifiques aux pièces, à savoir que différentes pièces partagent des mouvements communs, par exemple des tours et des reines. Afin d'éviter le code en double, vous devez factoriser le code commun dans une méthode de base et avoir un autre appel coûteux.

Cela étant dit, si c'est le premier moteur d'échecs que vous écrivez, je vous conseillerais certainement d'aller avec des principes de programmation propres et d'éviter les astuces d'optimisation décrites par l'auteur de Crafty. Gérer les spécificités des règles d'échecs (roque, promotion, en passant) et des techniques d'IA complexes (planches de hachage, coupe alpha-bêta) est déjà assez difficile.

Joh
la source
1
Il n'est peut-être pas utile d'écrire le moteur d'échecs trop rapidement.
Freshblood
5
-1. Des jugements comme "hypothétiquement, cela pourrait devenir un problème de performance, donc c'est mauvais", sont à mon humble avis presque toujours une pure superstition. Et l'héritage ne concerne pas seulement «l'extensibilité» dans le sens où vous le mentionnez. Différentes règles d'échecs s'appliquent à différentes pièces, et chaque type de pièce ayant une implémentation différente d'une méthode comme WhichMovesArePossibleest une approche raisonnable.
Doc Brown du
2
Si vous n'avez pas besoin d'extensibilité, une approche basée sur switch / enum est préférable car elle est plus rapide qu'une distribution de méthode virtuelle. Un moteur d'échecs est lourd en CPU, et les opérations qui sont effectuées le plus souvent (et donc critiques en termes d'efficacité) effectuent des mouvements et les annulent. Juste un niveau au-dessus qui répertorie tous les mouvements possibles. Je m'attends à ce que ce soit également un facteur critique. Le fait est que j'ai programmé des moteurs d'échecs en C. Même sans POO, les optimisations de bas niveau sur la représentation du tableau étaient importantes. Quel genre d'expérience avez-vous pour renvoyer le mien?
Joh
2
@Joh: Je ne manquerai pas de respect à votre expérience, mais je suis à peu près sûr que ce n'est pas ce que le PO recherchait. Et bien que les optimisations de bas niveau puissent être importantes pour plusieurs problèmes, je considère que c'est une erreur de les fonder pour des décisions de conception de haut niveau - en particulier lorsque l'on ne rencontre pas de problèmes de performances jusqu'à présent. D'après mon expérience, Knuth avait tout à fait raison de dire que «l'optimisation prématurée est la racine de tout mal».
Doc Brown
1
-1 Je dois être d'accord avec le Doc. Le fait est que ce "moteur" dont parle l'OP pourrait simplement être le moteur de validation d'une partie d'échecs à 2 joueurs. Dans ce cas, penser aux performances de la POO par rapport à tout autre style est complètement ridicule, de simples validations de moments prendraient des millisecondes même sur un appareil ARM bas de gamme. Ne lisez pas plus les exigences que ce qui est réellement indiqué.
Timothy Baldridge