¿Debo evitar usar la herencia de objetos como sea posible para desarrollar un juego?

36

Prefiero las características de OOP cuando desarrollo juegos con Unity. Por lo general, creo una clase base (principalmente abstraída) y uso la herencia de objetos para compartir la misma funcionalidad con los otros objetos.

Sin embargo, recientemente escuché de alguien que se debe evitar el uso de la herencia, y deberíamos usar interfaces en su lugar. Entonces le pregunté por qué, y dijo que "la herencia de objetos es importante, por supuesto, pero si hay muchos objetos extendidos, la relación entre las clases está profundamente acoplada.

Estoy usando una clase base abstracta, algo así como WeaponBasey la creación de clases de armas específicas, como Shotgun, AssaultRifle, Machinegunalgo por el estilo. Hay muchas ventajas, y un patrón que realmente disfruto es el polimorfismo. Puedo tratar el objeto creado por las subclases como si fueran la clase base, de modo que la lógica pueda reducirse drásticamente y hacerse más reutilizable. Si necesito acceder a los campos o métodos de la subclase, vuelvo a la subclase.

No quiero definir mismos campos, que se utiliza comúnmente en diferentes clases, como AudioSource/ Rigidbody/ Animatory un montón de campos miembros I definidos como fireRate, damage, range, spready así sucesivamente. También en algunos casos, algunos de los métodos podrían sobrescribirse. Entonces, estoy usando métodos virtuales en ese caso, de modo que básicamente puedo invocar ese método usando el método de la clase padre, pero si la lógica debería ser diferente en el niño, los he anulado manualmente.

Entonces, ¿deberían abandonarse todas estas cosas como malas prácticas? ¿Debo usar interfaces en su lugar?

modernizador
fuente
30
"Hay muchas ventajas, y un patrón que realmente disfruto es el polimorfismo". El polimorfismo no es un patrón. Es, literalmente, todo el punto de la POO. Otras cosas como los campos comunes, etc. se pueden lograr fácilmente sin duplicación de código o herencia.
Cubic
3
¿La persona que sugirió que sus interfaces eran mejores que la herencia le dio algún argumento? Soy curioso.
Hawker65
1
@Cubic Nunca dije que el polimorfismo es un patrón. Dije "... un patrón que realmente disfruto es el polimorfismo". Significa que el patrón que realmente disfruto es usar "polimorfismo", no el polimorfismo es un patrón.
modernizador
44
Las personas promocionan el uso de interfaces sobre la herencia en cualquier forma de desarrollo. Sin embargo, tiende a agregar mucha sobrecarga, disonancia cognitiva, conduce a una explosión de clase, con frecuencia agrega un valor cuestionable y generalmente no se sinergia bien en un sistema a menos que TODOS en el proyecto sigan ese enfoque en las interfaces. Así que no abandones la herencia todavía. Sin embargo, los juegos tienen algunas características únicas y la arquitectura de Unity espera que uses un paradigma de diseño CES, por lo que debes modificar tu enfoque. Lea la serie de Wizards and Warriors de Eric Lippert que describe problemas de tipo de juego.
Dunk
2
Yo diría que una interfaz es un caso particular de herencia donde la clase base no contiene campos de datos. Creo que la sugerencia es tratar de no construir un árbol enorme donde cada objeto deba clasificarse, sino muchos árboles pequeños basados ​​en interfaces donde pueda aprovechar la herencia múltiple. El polimorfismo no se pierde con las interfaces.
Emanuele Paolini

Respuestas:

65

Favorezca la composición sobre la herencia en su entidad y en los sistemas de inventario / artículos. Este consejo tiende a aplicarse a las estructuras lógicas del juego, cuando la forma en que puedes ensamblar cosas complejas (en tiempo de ejecución) puede conducir a muchas combinaciones diferentes; ahí es cuando preferimos la composición.

Favorezca la herencia sobre la composición en su lógica de nivel de aplicación, todo, desde construcciones de UI hasta servicios. Por ejemplo,

Widget->Button->SpinnerButton o

ConnectionManager->TCPConnectionManagervs ->UDPConnectionManager.

... aquí hay una jerarquía de derivación claramente definida, en lugar de una multitud de derivaciones potenciales, por lo que es más fácil usar la herencia.

En pocas palabras : use la herencia donde pueda, pero use la composición donde deba. PD La otra razón por la que podemos favorecer la composición en los sistemas de entidades es que generalmente hay muchas entidades, y la herencia puede incurrir en un costo de rendimiento para acceder a los miembros en cada objeto; ver vtables .

Ingeniero
fuente
10
La herencia no implica que todos los métodos sean virtuales. Por lo tanto, no genera automáticamente un costo de rendimiento. VTable solo juega un papel en el despacho de las llamadas virtuales , no en el acceso a los miembros de datos.
Ruslan
1
@Ruslan He agregado las palabras 'may' y 'can' para acomodar su comentario. De lo contrario, en aras de mantener la respuesta concisa, no entré en demasiados detalles. Gracias.
Ingeniero
77
P.S. The other reason we may favour composition in entity systems is ... performance: ¿Es esto realmente cierto? Mirando la página de WIkipedia vinculada por @Linaith se muestra que necesitarías componer tu objeto de interfaces. Por lo tanto, tiene (todavía o incluso más) llamadas a funciones virtuales y más errores de caché, ya que introdujo otro nivel de indirección.
Flamefire
1
@Flamefire Sí, es cierto; hay formas de organizar sus datos de manera que la eficiencia de su caché sea óptima, independientemente, y ciertamente mucho más óptima que esas indirecciones adicionales. Estos no son herencia y son composición, aunque más en el sentido de matriz de estructura / estructura de matrices (datos intercalados / no intercalados en disposición lineal de caché), dividiéndose en matrices separadas y a veces duplicando datos para que coincidan con los conjuntos de operaciones en diferentes partes de su tubería. Pero una vez más, entrar en eso aquí sería una distracción muy importante que es mejor evitar por el bien de la concisión.
Ingeniero
2
Recuerde mantener comentarios a las solicitudes civiles de aclaración o crítica y debatir el contenido de las ideas y no el carácter o la experiencia de la persona que las expresa.
Josh
18

Ya tienes algunas buenas respuestas, pero el gran elefante en la habitación en tu pregunta es esta:

alguien ha escuchado que se debe evitar el uso de la herencia, y deberíamos usar interfaces en su lugar

Como regla general, cuando alguien te da una regla general, ignórala. Esto no solo va para "alguien que te dice algo", sino también para leer cosas en Internet. A menos que sepa por qué (y realmente puede respaldarlo), dicho consejo no tiene valor y, a menudo, es muy dañino.

En mi experiencia, los conceptos más importantes y útiles en OOP son "bajo acoplamiento" y "alta cohesión" (las clases / objetos saben lo menos posible el uno del otro, y cada unidad es responsable de la menor cantidad de cosas posible).

Acoplamiento bajo

Esto significa que cualquier "paquete de cosas" en su código debe depender de su entorno lo menos posible. Esto se aplica a las clases (diseño de clase) pero también a los objetos (implementación real), "archivos" en general (es decir, número de #includes por .cpparchivo único , número de importpor .javaarchivo individual , etc.).

Una señal de que dos entidades están acopladas es que una de ellas se romperá (o necesita ser cambiada) cuando la otra se cambie de alguna manera.

La herencia aumenta el acoplamiento, obviamente; El cambio de la clase base cambia todas las subclases.

Las interfaces reducen el acoplamiento: al definir un contrato claro basado en métodos, puede cambiar cualquier cosa sobre ambos lados de la interfaz libremente, siempre que no cambie el contrato. (Tenga en cuenta que "interfaz" es un concepto general, las interfaceclases abstractas Java o C ++ son solo detalles de implementación).

Alta cohesión

Esto significa que cada clase, objeto, archivo, etc. se preocupe o sea responsable de la menor cantidad posible. Es decir, evite las clases grandes que hacen muchas cosas. En su ejemplo, si sus armas tienen aspectos completamente separados (munición, comportamiento de disparo, representación gráfica, representación de inventario, etc.), entonces puede tener diferentes clases que representan exactamente una de esas cosas. La clase de arma principal se transforma en un "titular" de esos detalles; un objeto de arma es poco más que unos pocos indicadores de esos detalles.

En este ejemplo, se aseguraría de que su clase que representa el "Comportamiento de disparo" sepa lo menos humanamente posible sobre la clase de arma principal. Óptimamente, nada en absoluto. Esto significaría, por ejemplo, que podría dar "Comportamiento de disparo" a cualquier objeto en su mundo (torretas, volcanes, PNJ ...) con solo un chasquido de un dedo. Si en algún momento desea cambiar la forma en que se representan las armas en el inventario, simplemente puede hacerlo, solo su clase de inventario lo sabe.

Una señal de que una entidad no es coherente es si crece más y más, ramificándose en varias direcciones al mismo tiempo.

La herencia tal como la describe disminuye la cohesión: sus clases de armas son, al final del día, grandes pedazos que manejan todo tipo de aspectos diferentes y no relacionados de sus armas.

Las interfaces aumentan indirectamente la cohesión al dividir claramente las responsabilidades entre los dos lados de la interfaz.

Qué hacer ahora

Todavía no hay reglas duras y rápidas, todo esto son solo pautas. En general, como el usuario TKK mencionó en su respuesta, la herencia se enseña mucho en la escuela y los libros; son las cosas elegantes sobre OOP. Las interfaces son probablemente más aburridas de enseñar, y también (si pasa ejemplos triviales) un poco más difícil, abriendo el campo de la inyección de dependencia, que no es tan claro como la herencia.

Al final del día, su esquema basado en la herencia es aún mejor que no tener un diseño claro de OOP. Así que siéntase libre de seguir con eso. Si lo desea, puede reflexionar / googlear un poco sobre Acoplamiento bajo, Cohesión alta y ver si desea agregar ese tipo de pensamiento a su arsenal. Siempre puede refactorizar para probar eso si lo desea, más tarde; o pruebe los enfoques basados ​​en la interfaz en su próximo nuevo módulo de código más grande.

AnoE
fuente
Gracias por una explicación detallada. Es realmente útil entender lo que me estoy perdiendo.
modernizador
13

La idea de que se debe evitar la herencia es simplemente errónea.

Existe un principio de codificación llamado Composición sobre Herencia . Dice que puede lograr lo mismo con la composición, y es preferible, porque puede reutilizar parte del código. Ver ¿Por qué debería preferir la composición sobre la herencia?

Tengo que decir que me gustan tus clases de armas y que harían lo mismo. Pero no he hecho un juego por ahora ...

Como señaló James Trotter, la composición podría tener algunas ventajas, especialmente en la flexibilidad en tiempo de ejecución para cambiar la forma en que funciona el arma. Esto sería posible con la herencia, pero es más difícil.

Linaith
fuente
14
Sin embargo, en lugar de tener clases para las armas, podrías tener una sola clase de "arma" y componentes para manejar lo que hace el disparo, qué accesorios tiene y qué hacen, qué "almacenamiento de munición" tiene, puedes construir muchas configuraciones , algunos de los cuales pueden ser una "escopeta" o un "rifle de asalto", pero eso también se puede cambiar en tiempo de ejecución, por ejemplo, para cambiar los accesorios y cambiar las capacidades del cargador, etc. O se pueden crear configuraciones completamente nuevas de armas que no Ni siquiera pensé en originalmente. Sí, puede lograr algo similar con la herencia, pero no tan fácil.
Trotski94
3
@JamesTrotter En este punto, pienso en Borderlands y sus armas, y me pregunto qué monstruosidad sería con solo herencia ...
Baldrickk
1
@JamesTrotter Ojalá fuera una respuesta y no un comentario.
Pharap
6

El problema es que la herencia conduce al acoplamiento: sus objetos necesitan saber más unos de otros. Es por eso que la regla es "Siempre favorecer la composición sobre la herencia". Esto no significa que NUNCA use la herencia, significa usarla donde sea completamente apropiado, pero si alguna vez está sentado allí pensando "podría hacer esto de ambas maneras y ambas tienen sentido", simplemente vaya directamente a la composición.

La herencia también puede ser un poco limitante. Tienes un "WeaponBase" que puede ser un AssultRifle: increíble. ¿Qué sucede cuando tienes una escopeta de doble cañón y quieres permitir que los cañones disparen de forma independiente? Un poco más difícil pero factible, solo tienes una clase de cañón simple y doble cañón, pero no puedes montar 2 cañones individuales con la misma arma, ¿puedes? ¿Podría modificar uno para tener 3 barriles o necesita una nueva clase para eso?

¿Qué tal un rifle de asalto con un lanzagranadas debajo? Hmm, un poco más duro. ¿Se puede reemplazar el Lanzagranadas con una linterna para la caza nocturna?

Finalmente, ¿cómo le permite a su usuario hacer las armas anteriores editando un archivo de configuración? Esto es difícil porque tiene relaciones codificadas que podrían estar mejor compuestas y configuradas con datos.

La herencia múltiple puede solucionar algunos de estos ejemplos triviales, pero agrega su propio conjunto de problemas.

Antes de que hubiera dichos comunes como "Prefiera la composición sobre la herencia", descubrí esto escribiendo un árbol de herencia demasiado complejo (que fue increíblemente divertido y funcionó perfectamente) y luego descubrí que era realmente difícil de modificar y mantener más tarde. El dicho es solo para que tenga una forma alternativa y compacta de aprender y recordar tal concepto sin tener que pasar por toda la experiencia, pero si desea utilizar la herencia en gran medida, le recomiendo mantener una mente abierta y evaluar qué tan bien funciona para usted: nada terrible sucederá si usa mucho la herencia y, en el peor de los casos, podría ser una gran experiencia de aprendizaje (en el mejor de los casos, podría funcionar bien para usted)

Bill K
fuente
2
"¿Cómo permite que su usuario haga las armas anteriores editando un archivo de configuración?" -> El patrón que generalmente hago es crear una clase final para cada arma. Por ejemplo, como Glock17. Primero crea la clase WeaponBase. Esta clase se resume, define valores comunes como modo de disparo, cadencia de disparo, tamaño de cargador, munición máxima, perdigones. También maneja la entrada del usuario, como mouse1 / mouse2 / reload e invoca el método correspondiente. Son casi virtuales, o se dividen en más funciones para que el desarrollador pueda anular la funcionalidad básica si es necesario. Y luego cree otra clase que amplíe WeaponBase,
modernizador
2
algo como SlideStoppableWeapon. La mayoría de las pistolas tienen esto, y esto maneja comportamientos adicionales como el deslizamiento detenido. Finalmente, cree la clase Glock17 que se extiende desde SlideStoppableWeapon. Glock17 es la clase final, si el arma tiene una habilidad única que solo tiene, finalmente escrita aquí. Usualmente uso este patrón cuando la pistola tiene un mecanismo especial de disparo o un mecanismo de recarga. Implemente ese comportamiento único para la jerarquía de fin de clase, combine las lógicas comunes tanto como sea posible de los padres.
modernizador
2
@modernator Como dije, es solo una guía: si no confías en él, siéntete libre de ignorarlo, solo mantén los ojos abiertos en cuanto a los resultados a lo largo del tiempo y evalúa los beneficios frente a los dolores de vez en cuando. Trato de recordar lugares donde me golpeé los dedos de los pies y ayudo a otros a evitar lo mismo, pero lo que funciona para mí puede no funcionar para ti.
Bill K
77
@modernator En Minecraft hicieron Monsteruna subclase de WalkingEntity. Luego agregaron limos. ¿Qué tan bien crees que funcionó?
user253751
1
@immibis ¿Tiene una referencia o un enlace anecdótico a ese problema? Buscar en Google palabras clave de Minecraft me ahoga en enlaces de video ;-)
Peter - Restablecer Mónica
3

Al contrario de las otras respuestas, esto no tiene nada que ver con la herencia versus la composición. La herencia versus la composición es una decisión que usted toma con respecto a cómo se implementará una clase. Interfaces vs. clases es una decisión que viene antes de eso.

Las interfaces son los ciudadanos de primera clase de cualquier lenguaje OOP. Las clases son detalles de implementación secundaria. Las mentes de los nuevos programadores están severamente deformadas por maestros y libros que introducen clases y herencia antes que las interfaces.

El principio clave es que, siempre que sea posible, los tipos de todos los parámetros del método y los valores de retorno deben ser interfaces, no clases. Esto hace que sus API sean mucho más flexibles y potentes. La gran mayoría de las veces, sus métodos y las personas que llaman no deberían tener ninguna razón para conocer o preocuparse por las clases reales de los objetos con los que están tratando. Si su API es independiente de los detalles de implementación, puede cambiar libremente entre composición y herencia sin romper nada.

No U
fuente
Si las interfaces son los ciudadanos de primera clase de cualquier lenguaje OOP, me pregunto si Python, Ruby ... no son idiomas OOP.
BlackJack
@BlackJack: En Ruby, la interfaz es (duck typing si se quiere) implícita, y se expresa mediante el uso de la composición en lugar de la herencia (también, tiene Mixins que son en algún punto intermedio, en lo que a mí respecta) ...
AnoE
@BlackJack "Interfaz" (el concepto) no requiere interface(la palabra clave del idioma).
Caleth
2
@Caleth Pero llamarlos ciudadanos de primera clase lo hace en mi humilde opinión. Cuando una descripción de idioma afirma que algo es ciudadano de primera clase , generalmente significa que es algo real en ese idioma que tiene un tipo y un valor y se puede transmitir. Las clases iguales son ciudadanos de primera clase en Python, al igual que las funciones, los métodos y los módulos. Si estamos hablando del concepto, entonces las interfaces también son parte de todos los lenguajes que no son OOP.
BlackJack
2

Cada vez que alguien le dice que un enfoque específico es el mejor para todos los casos, es lo mismo que decirle que un mismo medicamento cura todas las enfermedades.

La herencia frente a la composición es una pregunta is-a vs has-a. Las interfaces son otra forma (tercera), apropiada para algunas situaciones.

Herencia, o la lógica is-a: se usa cuando la nueva clase se va a comportar y se usa completamente (externamente) como la clase anterior de la que se deriva, si la nueva clase se va a exponer al público toda la funcionalidad que tenía la clase anterior ... entonces la heredas.

Composición, o la lógica has-a: si la nueva clase solo necesita usar internamente la clase antigua, sin exponer las características de la clase anterior al público, entonces usa composición, es decir, tiene una instancia de la clase antigua como propiedad miembro o una variable de la nueva. (Esto puede ser una propiedad privada, o protegida, o lo que sea, dependiendo del caso de uso). El punto clave aquí es que este es el caso de uso en el que no desea exponer las características de clase originales y usarlas al público, solo úselas internamente, mientras que en el caso de herencia las está exponiendo al público, a través del Nueva clase. A veces necesitas un enfoque, y a veces el otro.

Interfaces: Las interfaces son para otro caso de uso - cuando se desea que la nueva clase a parte y no completamente aplicar y exponer al público la funcionalidad de la antigua. Esto le permite tener una nueva clase, una clase de una jerarquía totalmente diferente de la anterior, comportarse como la anterior solo en algunos aspectos.

Digamos que tienes una variedad de criaturas representadas por clases y que tienen funcionalidades representadas por funciones. Por ejemplo, un pájaro tendría Talk (), Fly () y Poop (). Class Duck heredaría de la clase Bird, implementando todas las características.

La clase Fox, obviamente, no puede volar. Entonces, si define al antepasado para que tenga todas las características, entonces no podría derivar el descendiente correctamente.

Sin embargo, si divide las características en grupos, que representan cada grupo de llamadas con una interfaz, por ejemplo, IFly, que contiene Takeoff (), FlapWings () y Land (), entonces podría para la clase Fox implementar funciones de ITalk e IPoop pero no IFly. Luego definiría variables y parámetros para aceptar objetos que implementan una interfaz específica, y luego el código que trabaja con ellos sabría lo que puede llamar ... y siempre puede consultar otras interfaces, si necesita ver si otras funcionalidades también son implementado para el objeto actual.

Cada uno de estos enfoques tiene casos de uso cuando es el mejor, ningún enfoque es la mejor solución absoluta para todos los casos.

Dragan Juric
fuente
1

Para un juego, especialmente con Unity, que funciona con una arquitectura de componente de entidad, debe favorecer la composición sobre la herencia para los componentes de su juego. Es mucho más flexible y evita entrar en una situación en la que desea que una entidad del juego "sea" dos cosas que están en diferentes hojas de un árbol de herencia.

Entonces, por ejemplo, digamos que tiene un TalkingAI en una rama de un árbol de herencia, y VolumetricCloud en otra rama separada, y desea una nube parlante. Esto es difícil con un árbol de herencia profunda. Con el componente de entidad, solo crea una entidad que tiene componentes TalkingAI y Cloud, y está listo para comenzar.

Pero eso no significa que, por ejemplo, en su implementación de nube volumétrica, no deba usar la herencia. El código para eso podría ser sustancial y consistir en varias clases, y puede usar OOP según sea necesario para ello. Pero todo equivaldrá a un solo componente del juego.

Como nota al margen, tengo algún problema con esto:

Por lo general, creo una clase base (principalmente abstraída) y uso la herencia de objetos para compartir la misma funcionalidad con los otros objetos.

La herencia no es para reutilizar el código. Es para establecer una relación es-una . Muchas veces van de la mano, pero hay que tener cuidado. El hecho de que dos módulos necesiten usar el mismo código, no significa que uno sea del mismo tipo que el otro.

Puede usar interfaces si quiere, por ejemplo, tener una List<IWeapon>con diferentes tipos de armas. De esta manera, puede heredar de la interfaz IWeapon y de la subclase MonoBehaviour para todas las armas, y evitar cualquier problema con la ausencia de herencia múltiple.

Sava B.
fuente
-3

Parece que no comprende lo que es o hace una interfaz. Me tomó más de 10 años pensar en OO y hacer algunas preguntas a personas realmente inteligentes fue que pude tener un momento ah-ha con las interfaces. Espero que esto ayude.

Lo mejor es usar una combinación de ellos. En mi mente OO modelando el mundo y la gente, digamos, un ejemplo profundo. El alma está interconectada con el cuerpo a través de la mente. La mente ES la interfaz entre el alma (algunos consideran el intelecto) y los comandos que emite al cuerpo. La interfaz de las mentes con el cuerpo es la misma para todas las personas, funcionalmente hablando.

Se puede usar la misma analogía entre la persona (cuerpo y alma) que interactúa con el mundo. El cuerpo es la interfaz entre la mente y el mundo. Todos interactúan con el mundo de la misma manera, la única singularidad es CÓMO interactuamos con el mundo, así es como usamos nuestra MENTE para DECIDIR CÓMO interactuamos / interactuamos en el mundo.

Una interfaz es simplemente un mecanismo para relacionar su estructura OO con otra estructura OO diferente con cada lado que tiene diferentes atributos que deben negociarse / correlacionarse entre sí para producir una salida funcional en un medio / entorno diferente.

Entonces, para que la mente llame a la función de mano abierta, debe implementar la interfaz sistema nervioso_comando con ESO que tiene su propia API con instrucciones sobre cómo implementar todos los requisitos necesarios antes de llamar a mano abierta.

Christopher Hoffman
fuente