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.
fuente
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.Respuestas:
A primera vista, su respuesta pretende que la solución de "composición" no usa la herencia. Pero supongo que simplemente olvidaste agregar esto:
(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:
Piece
ahora 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
Piece
deberí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 secundariasMovementComponent
también. Pero este diseño probablemente conduciría a una "switch(type)
" declaración enorme en la implementación deGetValidMoveSquares
. 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()
paraBasePiece
e 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.
fuente
movementComponent
. Vea mi edición sobre cómo proporcionar un campo de tipo de forma segura en el diseño de "herencia".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.fuente
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.
fuente
Comenzaría haciendo algunas observaciones sobre el dominio del problema (es decir, las reglas del ajedrez):
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:
MovementRule
es 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 unaGame
instancia para admitir diferentes variantes de ajedrez.fuente
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.
fuente
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.
fuente
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
Piece
clase (como ya lo ha hecho), así como unaPieceType
clase. LosPieceType
define clase todos los métodos que un peón, reina, ect debe definir, tales como,CanMoveTo
,GetPieceTypeName
, ect. Entonces las clasesPawn
eQueen
, ect prácticamente heredarían de laPieceType
clase.fuente
Piece
no 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 elPiece
y no con elPieceType
es su identidad y potencialmente su historial de movimientos.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.
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
Piece
clase tenga untype
campo, 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.
fuente
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:
fuente