¿Cuándo detener la herencia?

9

Hace una vez hice una pregunta en Stack Overflow sobre la herencia.

He dicho que diseño el motor de ajedrez en la moda OOP. Así que heredo todas mis piezas de la clase abstracta Pieza, pero la herencia aún continúa. Déjame mostrarte por código

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

public abstract class Pawn: Piece {}

public class WhitePawn :Pawn {}

public class BlackPawn:Pawn {}

A los programadores se les ha encontrado un poco mi diseño sobre ingeniería y me sugirieron eliminar las clases de piezas de color y mantener el color de la pieza como miembro de la propiedad como a continuación.

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

De esta manera, Piece puede conocer su color. Después de la implementación, he visto que mi implementación es la siguiente.

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
        {

        }
    }
}

Ahora veo que mantienen la propiedad de color causando sentencias if en implementaciones. Me hace sentir que necesitamos piezas de color específicas en la jerarquía de herencia.

En tal caso, ¿tendría clases como WhitePawn, BlackPawn o iría a diseñar manteniendo la propiedad Color?

Sin ver tal problema, ¿cómo le gustaría comenzar a diseñar? ¿Mantener la propiedad Color o tener una solución de herencia?

Editar: Quiero especificar que mi ejemplo puede no coincidir completamente con el ejemplo de la vida real. Entonces, antes de tratar de adivinar los detalles de implementación, solo concentre la pregunta.

¿Realmente solo pregunto si usar la propiedad Color causará si las declaraciones son mejores para usar la herencia?

Sangre fresca
fuente
3
Realmente no entiendo por qué modelarías las piezas de esta manera. Cuando llame a MakeMove (), ¿qué movimiento hará? Diría que lo que realmente necesita para comenzar es modelar el estado del tablero y ver qué clases necesita para eso
jk.
11
Creo que su idea errónea básica es no darse cuenta de que en el ajedrez el "color" es un atributo real del jugador en lugar de la pieza. Es por eso que decimos "movimiento de los negros", etc., el color está ahí para identificar qué jugador posee la pieza. Un peón sigue las mismas reglas y restricciones de movimiento, no importa de qué color sea, la única variación es en qué dirección está "hacia adelante".
James Anderson el
55
cuando no puedas calcular la herencia de "cuándo parar", mejor déjalo por completo. Puede reintroducir la herencia en una etapa posterior de diseño / implementación / mantenimiento, cuando la jerarquía se volverá obvia para usted. Utilizo este enfoque, funciona como un encanto
mosquito
1
El tema más desafiante es si crear una subclase para cada tipo de pieza o no. El tipo de pieza determina más aspectos de comportamiento que el color de la pieza.
NoChance
3
Si tiene la tentación de comenzar un diseño con ABC (clases base abstractas), háganos un favor a todos y no ... Los casos en que los ABC son realmente útiles son muy raros e incluso entonces, generalmente es más útil usar una interfaz en lugar.
Evan Plaice

Respuestas:

12

Imagine que diseña una aplicación que contiene vehículos. ¿Creas algo como esto?

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

En la vida real, el color es una propiedad de un objeto (ya sea un automóvil o una pieza de ajedrez), y debe estar representado por una propiedad.

¿Son MakeMovey TakeBackMovediferentes para negros y blancos? Para nada: las reglas son las mismas para ambos jugadores, lo que significa que esos dos métodos serán exactamente los mismos para negros y blancos , lo que significa que está agregando una condición donde no la necesita.

Por otro lado, si usted tiene WhitePawny BlackPawn, yo no se sorprenda si pronto o más tarde que voy a escribir:

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

o incluso algo como:

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
fuente
44
@Freshblood Creo que sería mejor tener una directionpropiedad y usarla para mover que usar la propiedad de color. Idealmente, el color solo debe usarse para renderizar, no para lógica.
Gyan alias Gary Buyn
77
Además, las piezas se mueven en la misma dirección, adelante, atrás, izquierda, derecha. La diferencia es desde qué lado del tablero comienzan. En una carretera, los automóviles en carriles opuestos avanzan.
slipset
1
@Blrfl Quizás tengas razón en que la directionpropiedad es booleana. No veo qué hay de malo en eso. Lo que me confundió sobre la pregunta hasta el comentario anterior de Freshblood es por qué querría cambiar la lógica de movimiento en función del color. Diría que es más difícil de entender porque no tiene sentido que el color afecte el comportamiento. Si todo lo que quieres hacer es distinguir qué jugador posee qué pieza puedes poner en dos colecciones separadas y evitar la necesidad de una propiedad o herencia. Las piezas en sí mismas podrían ser felizmente inconscientes de a qué jugador pertenecen.
Gyan alias Gary Buyn
1
Creo que estamos viendo lo mismo desde dos perspectivas diferentes. Si los dos lados fueran rojos y azules, ¿sería diferente su comportamiento cuando son blancos y negros? Yo diría que no, por eso digo que el color no es la causa de ninguna diferencia de comportamiento. Es solo una distinción sutil, pero es por eso que usaría una directiono tal vez una playerpropiedad en lugar de una coloren la lógica del juego.
Gyan alias Gary Buyn
1
@Freshblood white castle's moves differ from black's: ¿cómo? ¿Te refieres al enroque? Tanto el enroque del lado del rey como el del lado de la reina son 100% simétricos para blanco y negro.
Konrad Morawski
4

Lo que debe preguntarse es por qué realmente necesita esas declaraciones en su clase de Peón:

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

    }
    else
    {
    }

Estoy bastante seguro de que será suficiente tener un pequeño conjunto de funciones como

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

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

en su clase Piece e implemente todas las funciones en Pawn(y las otras derivaciones de Piece) el uso de esas funciones, sin tener alguna de las ifdeclaraciones anteriores. La única excepción puede ser la función para determinar la dirección de movimiento de un Peón por su color, ya que esto es específico para los peones, pero en casi todos los demás casos no necesitará ningún código de color específico.

La otra pregunta interesante es si realmente debería tener diferentes subclases para diferentes tipos de piezas. Eso me parece razonable y natural, ya que las reglas para los movimientos de cada pieza son diferentes y seguramente puedes implementar esto usando diferentes funciones sobrecargadas para cada tipo de pieza.

Por supuesto, se podría tratar de modelar esto de una manera más genérica, mediante la separación de las propiedades de las piezas de sus reglas en movimiento, pero esto puede terminar en una clase abstracta MovingRuley una jerarquía subclase MovingRulePawn, MovingRuleQueen, ... yo no iniciarlo que manera, ya que podría conducir fácilmente a otra forma de ingeniería excesiva.

Doc Brown
fuente
+1 para señalar que el código de color específico es probablemente un no-no. Los peones blancos y negros se comportan esencialmente de la misma manera, al igual que todas las otras piezas.
Konrad Morawski
2

La herencia no es tan útil cuando la extensión no afecta las capacidades externas del objeto .

La propiedad Color no afecta cómo se usa un objeto Piece . Por ejemplo, todas las piezas podrían invocar un método moveForward o moveBackwards (incluso si el movimiento no es legal), pero la lógica encapsulada de la pieza utiliza la propiedad Color para determinar el valor absoluto que representa un movimiento hacia adelante o hacia atrás (-1 o + 1 en el "eje y"), en lo que respecta a su API, su superclase y subclases son objetos idénticos (ya que todos exponen los mismos métodos).

Personalmente, definiría algunas constantes que representan cada tipo de pieza, luego usaría una instrucción switch para ramificar en métodos privados que devuelvan posibles movimientos para "esta" instancia.

León
fuente
El movimiento hacia atrás solo es ilegal en caso de peones. Y realmente no tienen que saber si son negros o blancos: es suficiente (y lógico) hacer que distingan hacia adelante y hacia atrás. La Boardclase (por ejemplo) podría encargarse de convertir estos conceptos en -1 o +1 dependiendo del color de la pieza.
Konrad Morawski
0

Personalmente no heredaría nada de nada Piece, porque no creo que ganes nada haciendo eso. Presumiblemente, a una pieza no le importa cómo se mueve, solo qué casillas son un destino válido para su próximo movimiento.

Quizás podría crear una Calculadora de movimiento válida que conozca los patrones de movimiento disponibles para un tipo de pieza y pueda recuperar una lista de cuadrados de destino válidos, por ejemplo

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
fuente
¿Pero quién determina qué pieza obtiene qué calculadora de movimiento? Si tuviera una clase base de pieza y tuviera PawnPiece, podría hacer esa suposición. Pero luego, a ese ritmo, la pieza en sí podría implementar cómo se mueve, como se diseñó en la pregunta original
Steven Striga,
@WeekendWarrior - "Personalmente no heredaría nada de nada Piece". Parece que Piece es responsable de algo más que simplemente calcular movimientos, que es la razón para dividir el movimiento como una preocupación separada. Determinar qué pieza obtiene qué calculadora sería una cuestión de inyección de dependencia, por ejemplovar my_knight = new Piece(new KnightMoveCalculator())
Ben Cottrell
Este sería un buen candidato para el patrón de peso mosca. Hay 16 peones en un tablero de ajedrez y todos comparten el mismo comportamiento . Este comportamiento podría extraerse fácilmente en un solo peso mosca. Lo mismo ocurre con todas las otras piezas.
MattDavey
-2

En esta respuesta, supongo que por "motor de ajedrez", el autor de la pregunta significa "IA de ajedrez", no simplemente un motor de validación de movimiento.

Creo que usar OOP para piezas y sus movimientos válidos es una mala idea por dos razones:

  1. La fuerza de un motor de ajedrez depende en gran medida de lo rápido que pueda manipular tableros, ver, por ejemplo , representaciones de tableros del programa de ajedrez . Teniendo en cuenta el nivel de optimización que describe este artículo, no creo que una llamada de función regular sea asequible. Una llamada a un método virtual agregaría un nivel de indirección e incurriría en una penalización de rendimiento aún mayor. El costo de OOP no es asequible allí.
  2. Los beneficios de la POO, como la extensibilidad, no sirven para las piezas, ya que no es probable que el ajedrez obtenga nuevas piezas en el corto plazo. Una alternativa viable a la jerarquía de tipos basada en herencia incluye el uso de enumeraciones y conmutadores. Muy baja tecnología y tipo C, pero difícil de superar en cuanto a rendimiento.

Alejarme de las consideraciones de rendimiento, incluido el color de la pieza en la jerarquía de tipos, en mi opinión es una mala idea. Se encontrará en situaciones en las que necesita verificar si dos piezas tienen colores diferentes, por ejemplo, para capturar. La comprobación de esta condición se realiza fácilmente mediante una propiedad. Hacerlo usando is WhitePiece-tests sería más feo en comparación.

También hay otra razón práctica para evitar mover la generación de movimientos a métodos específicos de pieza, a saber, que las diferentes piezas comparten movimientos comunes, por ejemplo, torres y reinas. Para evitar el código duplicado, tendría que factorizar el código común en un método base y tener otra llamada costosa.

Dicho esto, si es el primer motor de ajedrez que estás escribiendo, definitivamente recomendaría seguir principios de programación limpios y evitar los trucos de optimización descritos por el autor de Crafty. Tratar con los detalles de las reglas del ajedrez (enroque, promoción, pasatiempo) y las complejas técnicas de IA (tablas de hash, corte alfa-beta) ya es bastante desafiante.

Joh
fuente
1
Puede que no tenga la intención de escribir el motor de ajedrez demasiado rápido.
Freshblood
55
-1. Juicios como "hipotéticamente podría convertirse en un problema de rendimiento, por lo que es malo", son en mi humilde opinión casi siempre pura superstición. Y la herencia no se trata solo de "extensibilidad" en el sentido en que lo mencionas. Se aplican diferentes reglas de ajedrez a diferentes piezas, y cada tipo de pieza que tiene una implementación diferente de un método WhichMovesArePossiblees un enfoque razonable.
Doc Brown
2
Si no necesita extensibilidad, es preferible un enfoque basado en switch / enum porque es más rápido que un envío de método virtual. Un motor de ajedrez es pesado en la CPU, y las operaciones que se realizan con mayor frecuencia (y, por lo tanto, la eficiencia crítica) son realizar movimientos y deshacerlos. Solo un nivel por encima de eso viene listando todos los movimientos posibles Espero que esto también sea crítico en el tiempo. El hecho es que he programado motores de ajedrez en C. Incluso sin OOP, las optimizaciones de bajo nivel en la representación del tablero fueron significativas. ¿Qué tipo de experiencia tienes para descartar la mía?
Joh
2
@Joh: no faltaré al respeto a su experiencia, pero estoy bastante seguro de que eso no fue lo que buscó el OP. Y aunque las optimizaciones de bajo nivel pueden ser importantes para varios problemas, considero que es un error hacerlas una base para decisiones de diseño de alto nivel, especialmente cuando uno no enfrenta ningún problema de rendimiento hasta ahora. Mi experiencia es que Knuth tenía mucha razón al decir "La optimización prematura es la raíz de todo mal".
Doc Brown
1
-1 Tengo que estar de acuerdo con el Doc. El hecho es que este "motor" del que habla el OP podría ser simplemente el motor de validación para un juego de ajedrez de 2 jugadores. En ese caso, pensar en el rendimiento de OOP frente a cualquier otro estilo es completamente absurdo, las validaciones simples de momentos tomarían milisegundos incluso en un dispositivo ARM de gama baja. No lea más sobre los requisitos de lo que realmente se dice.
Timothy Baldridge