Herencia vs Composición para piezas de ajedrez

9

Una búsqueda rápida de este intercambio de pila muestra que, en general, la composición generalmente se considera más flexible que la herencia, pero como siempre depende del proyecto, etc., y hay ocasiones en que la herencia es la mejor opción. Quiero hacer un juego de ajedrez en 3D donde cada pieza tenga una malla, posiblemente animaciones diferentes, etc. En este ejemplo concreto, parece que podría argumentar un caso para ambos enfoques, ¿estoy equivocado?

La herencia se vería así (con el constructor adecuado, etc.)

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

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

que ciertamente obedece al principio "es-a" mientras que la composición se vería así

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
 }

¿Es más una cuestión de preferencia personal o es un enfoque claramente una opción más inteligente que el otro aquí?

EDITAR: Creo que aprendí algo de cada respuesta, así que me siento mal por elegir solo una. Mi solución final se inspirará en varias de las publicaciones aquí (aún trabajando en ello). Gracias a todos los que se tomaron el tiempo para responder.

CanISleepYet
fuente
Creo que ambos pueden existir uno al lado del otro, estos dos tienen un propósito diferente, pero estos no son exclusivos entre sí. El ajedrez está compuesto de diferentes piezas y tableros, y las piezas son de diferentes tipos. Estas piezas comparten algunas propiedades y comportamientos básicos, y también tienen propiedades y comportamientos específicos. Entonces, según yo, la composición debe aplicarse sobre tablero y piezas, mientras que la herencia debe seguirse sobre los tipos de piezas.
Hitesh Gaur
Si bien es posible que algunas personas afirmen que toda la herencia es mala, creo que la idea general es que la herencia de la interfaz es buena / buena y que la herencia de la implementación es útil pero puede ser problemática. Cualquier cosa más allá de un solo nivel de herencia es cuestionable, en mi experiencia. Puede estar bien más allá de eso, pero no es fácil de lograr sin hacer un desastre complicado.
JimmyJames
El patrón de estrategia es común en la programación de juegos por una razón. var Pawn = new Piece(howIMove, howITake, whatILookLike)me parece mucho más simple, más manejable y más fácil de mantener que una jerarquía de herencia.
Ant P
@AntP Ese es un buen punto, ¡gracias!
CanISleepYet
@AntP ¡Haz una respuesta! ... ¡Hay mucho mérito en eso!
svidgen

Respuestas:

4

A primera vista, su respuesta pretende que la solución de "composición" no usa la herencia. Pero supongo que simplemente olvidaste agregar esto:

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

(y más de estas derivaciones, una para cada uno de los tipos de seis piezas). Ahora esto se parece más al patrón clásico de "Estrategia", y también está utilizando la herencia.

Desafortunadamente, esta solución tiene una falla: Pieceahora cada uno contiene su información de tipo de forma redundante dos veces:

  • dentro de la variable miembro type

  • dentro movementComponent(representado por el subtipo)

Esta redundancia es lo que realmente podría traerle problemas: una clase simple como una Piecedebería proporcionar una "única fuente de verdad", no dos fuentes.

Por supuesto, podría intentar almacenar la información de tipo solo en type, y no crear clases secundarias MovementComponenttambién. Pero este diseño probablemente conduciría a una " switch(type)" declaración enorme en la implementación de GetValidMoveSquares. Y esa es definitivamente una fuerte indicación de que la herencia es la mejor opción aquí .

Nota en el diseño de la "herencia", es bastante fácil de proporcionar el campo "tipo" de una manera no redundante: añadir un método virtual GetType()para BasePiecee implementar en consecuencia en cada clase base.

En cuanto a "promoción": estoy aquí con @svidgen, encuentro discutibles los argumentos presentados en la respuesta de @ TheCatWhisperer .

Interpretar "promoción" como un intercambio físico de piezas en lugar de interpretarlo como un cambio del tipo de la misma pieza me parece más natural. Por lo tanto, implementar esto de manera similar, intercambiando una pieza por otra de un tipo diferente, probablemente no causará grandes problemas, al menos no para el caso específico del ajedrez.

Doc Brown
fuente
Gracias por decirme el nombre del patrón, no me di cuenta de que se llamaba Estrategia. También tiene razón. Eché de menos la herencia del componente de movimiento en mi pregunta. ¿Es el tipo realmente redundante? Si quiero iluminar a todas las reinas en el tablero, parecería extraño verificar qué componente de movimiento tenían, además de que tendría que lanzar el componente de movimiento para ver qué tipo es, ¿cuál sería súper feo?
CanISleepYet
@CanISleepYet: el problema con el campo "tipo" propuesto es que podría diferir del subtipo de movementComponent. Vea mi edición sobre cómo proporcionar un campo de tipo de forma segura en el diseño de "herencia".
Doc Brown
4

Probablemente prefiera la opción más simple a menos que tenga razones explícitas, bien articuladas o fuertes preocupaciones de extensibilidad y mantenimiento.

La opción de herencia es menos línea de código y menos complejidad. Cada tipo de pieza tiene una relación "inmutable" de 1 a 1 con sus características de movimiento. (A diferencia de su representación, que es 1 a 1, pero no necesariamente inmutable; es posible que desee ofrecer varias "máscaras").

Si rompe esta relación inmutable 1-a-1 entre piezas y su comportamiento / movimiento en múltiples clases, probablemente solo esté agregando complejidad , y no mucho más. Entonces, si estuviera revisando o heredando ese código, esperaría ver buenas razones documentadas para la complejidad.

Todo lo dicho, una opción aún más simple , IMO, es crear una Piece interfaz que implementen sus Piezas individuales . En la mayoría de los idiomas, esto es realmente muy diferente de la herencia , porque una interfaz no le impedirá implementar otra interfaz . ... Simplemente no obtienes ningún comportamiento de clase base de forma gratuita en ese caso: querrás poner el comportamiento compartido en otro lugar.

svidgen
fuente
No creo que la solución de herencia sea 'menos líneas de código'. Quizás en esta clase, pero no en general.
JimmyJames
@JimmyJames ¿ En serio? ... Solo cuente las líneas de código en el OP ... ciertamente no es la solución completa, pero no es un mal indicador de qué solución es más LOC y complejidad para mantener.
svidgen
No quiero poner un punto demasiado fino en esto porque todo es nocional en este punto, pero sí, realmente lo creo. Mi opinión es que este código es insignificante en comparación con toda la aplicación. Si esto simplifica la implementación de las reglas (discutible), entonces podría guardar docenas o incluso cientos de líneas de código.
JimmyJames
Gracias, no pensé en la interfaz, pero puede que tengas razón en que esta es la mejor manera de hacerlo. Más simple es casi siempre mejor.
CanISleepYet
2

Lo que pasa con el ajedrez es que el juego y las reglas de las piezas se prescriben y arreglan. Cualquier diseño que funcione está bien, ¡usa lo que quieras! Experimente y pruébelos todos.

Sin embargo, en el mundo de los negocios, nada se prescribe de manera tan estricta como esta: las reglas y requisitos comerciales cambian con el tiempo, y los programas tienen que cambiar con el tiempo para adaptarse. Aquí es donde los datos is-a vs. has-a vs. hacen la diferencia. Aquí, la simplicidad hace que las soluciones complejas sean más fáciles de mantener con el tiempo y los requisitos cambiantes. En general, en los negocios, también tenemos que lidiar con la persistencia, que también puede involucrar una base de datos. Entonces, tenemos reglas como no usar múltiples clases cuando una sola clase hará el trabajo, y no usar herencia cuando la composición es suficiente. Pero todas estas reglas están orientadas a hacer que el código sea mantenible a largo plazo ante el cambio de reglas y requisitos, lo cual no es el caso con el ajedrez.

Con el ajedrez, la ruta de mantenimiento más probable a largo plazo es que su programa necesita ser más y más inteligente, lo que eventualmente significa que dominarán las optimizaciones de velocidad y almacenamiento. Para eso, generalmente tendrá que hacer concesiones que sacrifiquen la legibilidad por el rendimiento, por lo que incluso el mejor diseño OO finalmente se irá por el camino.

Erik Eidt
fuente
Tenga en cuenta que ha habido muchos juegos exitosos que toman un concepto y luego arrojan algunas cosas nuevas a la mezcla. Si desea hacer esto en el futuro con su juego de ajedrez, debería tener en cuenta los posibles cambios futuros.
Flater
2

Comenzaría haciendo algunas observaciones sobre el dominio del problema (es decir, las reglas del ajedrez):

  • El conjunto de movimientos válidos para una pieza depende no solo del tipo de pieza, sino también del estado del tablero (el peón puede avanzar si la casilla está vacía / puede moverse en diagonal si captura una pieza).
  • Ciertas acciones implican eliminar una pieza existente del juego (promoción / capturar una pieza) o mover varias piezas (enroque).

Estos son difíciles de modelar como una responsabilidad de la pieza en sí, y ni la herencia ni la composición se sienten bien aquí. Sería más natural modelar estas reglas como ciudadanos de primera clase:

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

MovementRulees una interfaz simple pero flexible que permite que las implementaciones actualicen el estado del tablero de cualquier forma que sea necesaria para admitir movimientos complejos como el enroque y la promoción. Para el ajedrez estándar, tendría una implementación para cada tipo de pieza, pero también puede conectar diferentes reglas en una Gameinstancia para admitir diferentes variantes de ajedrez.

casablanca
fuente
Enfoque interesante - Buena comida para pensar
CanISleepYet
1

Creo que en este caso la herencia sería la opción más limpia. La composición puede ser un poco más elegante, pero me parece un poco más forzada.

Si planeaba desarrollar otros juegos que usan piezas móviles que se comportan de manera diferente, la composición podría ser una mejor opción, especialmente si empleó un patrón de fábrica para generar las piezas necesarias para cada juego.

Kurt Haldeman
fuente
2
¿Cómo tratarías la promoción usando la herencia?
TheCatWhisperer
44
@TheCatWhisperer ¿Cómo lo manejas cuando estás jugando físicamente? (Esperemos que reemplace la pieza , no volverá a tallar el peón, ¿verdad?)
svidgen
0

que ciertamente obedece el principio "es-a"

Bien, entonces usas la herencia. Todavía puede componer dentro de su clase Piece, pero al menos tendrá una columna vertebral. La composición es más flexible, pero la flexibilidad está sobrevalorada, en general, y ciertamente en este caso porque las posibilidades de que alguien presente un nuevo tipo de pieza que requiera que abandones tu modelo rígido son cero. El juego de ajedrez no va a cambiar pronto. La herencia le brinda un modelo de guía, algo para relacionarse también, código de enseñanza, dirección. Composición no tanto, las entidades de dominio más importantes no se destacan, no tiene espinas, tienes que entender el juego para entender el código.

Martin Maat
fuente
gracias por agregar el punto de que con la composición es más difícil razonar sobre el código
CanISleepYet
0

Por favor, no use la herencia aquí. Si bien las otras respuestas ciertamente tienen muchas gemas de sabiduría, usar la herencia aquí ciertamente sería un error. El uso de la composición no solo te ayuda a lidiar con problemas que no anticipas, sino que ya veo un problema aquí que la herencia no puede manejar con gracia.

La promoción, cuando un peón se convierte en una pieza más valiosa, podría ser un problema con la herencia. Técnicamente, podría lidiar con esto reemplazando una pieza por otra, pero este enfoque tiene limitaciones. Una de estas limitaciones sería el seguimiento de estadísticas por pieza en las que es posible que no desee restablecer la información de la pieza después de la promoción (o escribir el código adicional para copiarlas).

Ahora, en cuanto a su diseño de composición en su pregunta, creo que es innecesariamente complicado. No sospecho que necesites tener un componente separado para cada acción y puedas quedarte con una clase por tipo de pieza. El tipo enum también podría ser innecesario.

Definiría una Piececlase (como ya lo ha hecho), así como una PieceTypeclase. Los PieceTypedefine clase todos los métodos que un peón, reina, ect debe definir, tales como, CanMoveTo, GetPieceTypeName, ect. Entonces las clases Pawne Queen, ect prácticamente heredarían de la PieceTypeclase.

TheCatWhisperer
fuente
3
Los problemas que ve con la promoción y el reemplazo son triviales, en mi opinión. La separación de las preocupaciones prácticamente exige que tal "seguimiento por pieza" (o lo que sea) se maneje independientemente de todos modos . ... Cualquier ventaja potencial que ofrece su solución se ve eclipsada por las preocupaciones imaginarias que está tratando de abordar, IMO.
svidgen
55
Para aclarar: Sin duda, hay algunos méritos para su solución propuesta. Pero son difíciles de encontrar en su respuesta, que se centra principalmente en problemas que son realmente fáciles de resolver en la "solución de herencia". ... Ese tipo de detrae (para mí de todos modos) de los méritos que estás tratando de comunicar sobre tu opinión.
svidgen
1
Si Pieceno es virtual, estoy de acuerdo con esta solución. Si bien el diseño de la clase puede parecer un poco más complejo en la superficie, creo que la implementación será más simple en general. Lo principal que se asociaría con el Piecey no con el PieceTypees su identidad y potencialmente su historial de movimientos.
JimmyJames
1
@svidgen Una implementación que usa una Pieza compuesta y una implementación que usa una pieza basada en herencia se verá básicamente idéntica aparte de los detalles descritos en esta solución. Esta solución de composición agrega un solo nivel de indirección. No veo eso como algo que valga la pena evitar.
JimmyJames
2
@JimmyJames Derecha. Pero, el historial de movimientos de múltiples piezas debe ser rastreado y correlacionado, no es algo de lo que una sola pieza debería ser responsable, IMO. Es una "preocupación separada" si te gustan las cosas SÓLIDAS.
svidgen
0

Desde mi punto de vista, con la información proporcionada, no es prudente responder si la herencia o la composición deberían ser el camino a seguir.

  • La herencia y la composición son solo herramientas en el cinturón de herramientas del diseñador orientado a objetos.
  • Puede usar estas herramientas para diseñar muchos modelos diferentes para el dominio del problema.
  • Dado que hay muchos modelos de este tipo que pueden representar un dominio, según el punto de vista del diseñador sobre los requisitos, puede combinar la herencia y la composición de múltiples maneras para obtener modelos funcionalmente equivalentes.

Sus modelos son totalmente diferentes, en el primero modeló en torno al concepto de piezas de ajedrez, en el segundo en torno al concepto de movimiento de las piezas. Pueden ser funcionalmente equivalentes, y elegiría el que represente mejor el dominio y me ayude a razonar más fácilmente.

Además, en su segundo diseño, el hecho de que su Piececlase tenga un typecampo, revela claramente que hay un problema de diseño aquí, ya que la pieza en sí puede ser de varios tipos. ¿No parece que esto probablemente deba usar algún tipo de herencia, donde la pieza en sí es del tipo?

Entonces, ya ves, es muy difícil discutir sobre tu solución. Lo que importa no es si utilizó la herencia o la composición, sino si su modelo refleja con precisión el dominio del problema y si es útil razonar al respecto y proporcionar una implementación del mismo.

Se necesita algo de experiencia para usar la herencia y la composición correctamente, pero son herramientas completamente diferentes y no creo que una pueda "sustituir" una con la otra, aunque puedo estar de acuerdo en que un sistema podría diseñarse completamente usando solo una composición.

edalorzo
fuente
Usted hace un buen punto de que es el modelo lo que es importante y no la herramienta. Con respecto al tipo redundante, ¿la herencia realmente ayuda? Por ejemplo, si quiero resaltar todos los peones o verificar si una pieza era un rey, ¿no necesitaría un poco de lanzamiento?
CanISleepYet
-1

Bueno, yo tampoco sugiero.

Si bien la orientación a objetos es una buena herramienta y, a menudo, ayuda a simplificar las cosas, no es la única herramienta en la barra de herramientas.

Considere implementar una clase de tablero en su lugar, que contenga un simple bytearray.
La interpretación y el cálculo de las cosas se realizan en las funciones miembro utilizando enmascaramiento de bits y conmutación.

Use el MSB para el propietario, dejando 7 bits para la pieza, aunque reserve cero para el vacío.
Alternativamente, cero para vacío, signo para el propietario, valor absoluto para la pieza.

Si lo desea, puede usar campos de bits para evitar el enmascaramiento manual, aunque no son portátiles:

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); }
};
Deduplicador
fuente
Interesante ... y apto teniendo en cuenta que es una pregunta de C ++. Pero, al no ser un programador de C ++, ¡este no sería mi primer (o segundo o tercer ) instinto! ... ¡Esta quizás sea la respuesta más C ++ aquí!
svidgen
77
Esta es una microoptimización que ninguna aplicación real debería seguir.
Lie Ryan
@LieRyan Si todo lo que tienes es POO, todo huele a objeto. Y si es realmente ese "micro" también es una pregunta interesante. Dependiendo de lo que hagas con él, eso podría ser bastante significativo.
Deduplicador
Je ... dicho eso , C ++ está orientado a objetos. Asumo que hay una razón por la OP está usando C ++ en lugar de simplemente C .
svidgen
3
Aquí estoy menos preocupado por la falta de OOP, sino por ofuscar el código con un poco de giro. A menos que esté escribiendo código para Nintendo de 8 bits o esté escribiendo algoritmos que requieran lidiar con millones de copias del estado de la placa, esta es una microoptimización prematura y desaconsejable. En los escenarios normales, probablemente no será más rápido de todos modos porque el acceso de bits no alineados requiere operaciones de bits adicionales.
Lie Ryan