¿Se pueden tener clases / resúmenes "vacíos"?

10

Por supuesto que puedes, solo me pregunto si es racional diseñar de esa manera.

Estoy haciendo un clon y estaba haciendo un diseño de clase. Quería usar la herencia, aunque no es necesario, para aplicar lo que aprendí en C ++. Estaba pensando en el diseño de clase y se me ocurrió algo como esto:

GameObject -> clase base (consta de miembros de datos como desplazamientos xey), y un vector de SDL_Surface *

MovableObject: GameObject -> clase abstracta + clase derivada de GameObject (un método void move () = 0;)

NonMovableObject: GameObject -> clase vacía ... no hay métodos o miembros de datos que no sean constructor y destructor (¿al menos por ahora?).

Más tarde, planeaba derivar una clase de NonMovableObject, como Tileset: NonMovableObject. Me preguntaba si a menudo se usan clases abstractas "vacías" o simplemente clases vacías ... Me doy cuenta de que de la manera que estoy haciendo esto, solo estoy creando la clase NonMovableObject solo por el bien de la categorización.

Sé que estoy pensando demasiado para hacer un clon de ruptura, pero mi enfoque está menos en el juego y más en usar la herencia y diseñar algún tipo de marco de juego.

Langostinos
fuente

Respuestas:

2

En C ++ tiene herencia múltiple, por lo que literalmente no tiene ningún beneficio tener esas clases vacías. Si desea que Ball herede de GameObject y MovableObject más tarde (por ejemplo, desea mantener una matriz de GameObjects y llamar a un método Tick cada enésima de segundo para que se muevan), eso es bastante fácil de hacer.

Pero, en esa situación, yo personalmente sugeriría que recuerde el axioma " prefiera la composición (encapsulación) a la herencia " y mire el patrón de Estado . Si desea que cada objeto tenga su estado de movimiento más adelante, páselo al GameObject. Por ejemplo: DiagonalBouncingMovementState para la pelota, HoriazontalControlledMovementState para la pala, NoMovementState para un ladrillo.

1) Esto hará que le resulte más fácil escribir pruebas unitarias , si lo desea (lo cual, una vez más, lo recomendaría), porque puede probar GameObject y cada uno de sus estados de forma independiente.

2) Digamos que tiene fichas que caen de los ladrillos, pero luego desea cambiar una de ellas para que se muevan diagonalmente, en lugar de cambiarla por una jerarquía compleja de herencia de clases, simplemente puede cambiar el estado de movimiento codificado que se le pasa. .

3) Lo más importante: cuando quieres comenzar a cambiar en el juego cómo se mueve la pelota y / o la paleta en función de esas fichas, simplemente sigues cambiando su estado de movimiento (DiagonalMovementState se convierte en DiagonalWithGravityMovementState).

Ahora, nada de esto fue para sugerir que la herencia siempre es mala, solo para demostrar por qué preferimos la encapsulación sobre la herencia. Es posible que aún desee obtener cada tipo de objeto de GameObject y que esas clases comprendan su estado de movimiento inicial. Seguramente querrás derivar cada uno de tus estados de movimiento a partir de una clase abstracta de MotionState porque C ++ no tiene interfaces.

$ 0.02

pdr
fuente
8

En Java, las interfaces "vacías" se utilizan como marcadores (p. Ej., Serializable), porque en tiempo de ejecución, los objetos se pueden verificar si ellos "implementan" esa interfaz o no; pero en C ++, me parece bastante inútil.

En mi opinión, las características del lenguaje deben considerarse herramientas, algo que lo ayuda a alcanzar ciertos objetivos y no obligaciones. El hecho de que pueda usar una función no significa que tenga que hacerlo . La herencia sin sentido no hace que un programa sea mejor.

usuario281377
fuente
3

No has terminado de pensarlo; estás pensando y eso es bueno.

Como ya se dijo, no use una función de idioma solo porque está allí. Las compensaciones de las características del lenguaje de aprendizaje son clave para un buen diseño.

No se deben usar clases base para la categorización. Eso podría implicar verificar el tipo. Esa es una mala idea.

Considere escribir abstracciones puras que definan el contrato de sus objetos. El contrato de sus objetos es la interfaz externa. Su interfaz pública. Que hace.

Nota: Es importante comprender que no se trata de los datos que tiene x, ysino de lo que hace move().

En tu diseño, tienes una clase GameObject. Confía en él para sus datos y no para su funcionalidad. Se siente eficiente y correcto usar la herencia para compartir lo que parecen datos compartidos comunes, en su caso xy y.

Este no es el uso correcto de la herencia. Siempre recuerde, la herencia es la forma más estrecha de acoplamiento que posiblemente pueda tener y debe luchar por un acoplamiento suelto en todo momento.

Por su propia naturaleza NonMovableObjectno se mueve. Su xy ysin duda debe ser declarado const. Por su propia naturaleza MoveableObjectnecesita moverse. No puede declarar su xy ycomo const. Por lo tanto, estos no son los mismos datos y no se pueden compartir.

Considere la funcionalidad de sus objetos o contrato. ¿Qué deben hacer ellos ? Haz eso bien y los datos vendrán. Trabaja en un objeto a la vez. Preocúpate por cada uno a su vez. Encontrará que sus buenos diseños son reutilizables.

Quizás no haya ninguno NonMovableObjecten absoluto. ¿Qué pasa con una abstracción pura GameObjectBaseque define un move()método? Su sistema GameObjectejecuta instancias que implementan move()y el sistema solo mueve las que necesitan moverse.

Este es solo el comienzo; un sabor. La madriguera del conejo es mucho más profunda. No hay cuchara.

hiwaylon
fuente
Si bien es cierto que ni MovableObjecttampoco NonMovableObjectpueden heredar el uno del otro, los dos tienen una ubicación; como tal, ambos deben ser consumibles por código que, por ejemplo, quiere dirigir la puntería de un monstruo hacia un objeto sin tener en cuenta si ese objeto podría moverse o no.
supercat
2

Tiene aplicaciones en C # como se menciona en ammoQ, pero en C ++ la única aplicación sería si quisieras tener una lista de punteros de objetos no móviles.

Pero me imagino que estarías destruyendo los objetos a través del puntero NonMoveableObject, en cuyo caso necesitarías destructores virtuales, y ya no sería una clase vacía. :)

tenpn
fuente
También pensé en ese caso, pero ¿qué podrías hacer con una lista de objetos que no exponen ningún comportamiento? Lo más probable es que de alguna manera descubras el tipo real y uses un molde para acceder a los métodos y campos disponibles, definitivamente un olor a código.
user281377
Sí, ese es un buen punto.
Tenpn
1

Una situación en la que puede ver esto es donde desea que una colección fuertemente tipada contenga tipos heterogéneos. No digo que sea una gran idea, puede indicar una abstracción débil o un diseño deficiente, pero sucede. Como mencionas, es una forma de categorizar cosas y puede ser útil y perfectamente válido.

Por ejemplo, quieres una colección para guardar gatos y perros. En su diseño, usted decide que tienen mucho en común, así que tenga una clase base de Mamal y use este tipo en su colección (por ejemplo, Lista). Todo bien. Luego decides que quieres agregar algunas ranas a tu colección, pero no son mamals, por lo que una forma de hacerlo sería hacer que Frogs and Mamals sea una subclase de Animal, pero tal vez no puedas pensar mucho en las ranas y mamals que tienen en común para que el animal se quede vacío. Luego escribes tu colección con Animal en lugar de Mamal y todo está bien.

En la vida real, encuentro que incluso si creo una clase base vacía, tarde o temprano, alguna funcionalidad termina allí, pero ciertamente hay momentos en que existen.

Steve
fuente
Sin embargo, una pregunta que debe hacerse es: si los tipos no tienen nada en común, ¿por qué se mantienen en la misma colección? No hay ninguna operación que pueda realizar en todos los elementos de la colección, lo que frustra el propósito de tener dicha colección. Ahora puede haber algunas operaciones artificiales que desee agregar a ambas para simplificar la lógica, por ejemplo, implementar el patrón de diseño de Visitantes, en cuyo punto puede volver a ser útil, pero ahora tienen algo en común ...
Julio