He leído que cuando su programa necesita saber de qué clase es un objeto, generalmente indica una falla de diseño, por lo que quiero saber cuál es una buena práctica para manejar esto. Estoy implementando una Forma de clase con diferentes subclases heredadas de ella como Círculo, Polígono o Rectángulo y tengo diferentes algoritmos para saber si un Círculo choca con un Polígono o un Rectángulo. Luego, supongamos que tenemos dos instancias de Shape y queremos saber si una choca con la otra, en ese método tengo que inferir qué tipo de subclase es el objeto que estoy colisionando para saber a qué algoritmo debo llamar, pero este es un mal diseño o práctica? Así lo resolví.
abstract class Shape {
ShapeType getType();
bool collide(Shape other);
}
class Circle : Shape {
getType() { return Type.Circle; }
bool collide(Shape other) {
if(other.getType() == Type.Rect) {
collideCircleRect(this, (Rect) other);
} else if(other.getType() == Type.Polygon) {
collideCirclePolygon(this, (Polygon) other);
}
}
}
Este es un mal patrón de diseño? ¿Cómo podría resolver esto sin tener que inferir los tipos de subclase?
fuente
Shape
objetos abstractos . Su lógica depende de las partes internas de otro objeto a menos que esté verificando la colisión para detectar puntos límitebool collide(x, y)
(un subconjunto de puntos de control podría ser una buena compensación). De lo contrario, debe verificar el tipo de alguna manera: si realmente hay necesidad de abstracciones, producirCollision
tipos (para objetos dentro del área del actor actual) debería ser el enfoque correcto.Respuestas:
Polimorfismo
Mientras lo uses
getType()
o algo así, no estás usando polimorfismo.Entiendo sentir que necesitas saber qué tipo tienes. Pero cualquier trabajo que desee hacer sabiendo que realmente debe ser empujado a la clase. Entonces solo le dices cuándo hacerlo.
Este principio se llama decir, no preguntar . Seguirlo le ayuda a no difundir detalles como escribir y crear una lógica que actúe sobre ellos. Hacer eso convierte una clase de adentro hacia afuera. Es mejor mantener ese comportamiento dentro de la clase para que pueda cambiar cuando la clase cambie.
Encapsulación
Puedes decirme que nunca se necesitarán otras formas, pero no te creo y tampoco deberías.
Un buen efecto de seguir la encapsulación es que es fácil agregar nuevos tipos porque sus detalles no se extienden en el código donde aparecen
if
y en laswitch
lógica. El código para un nuevo tipo debería estar en un solo lugar.Un sistema de detección de colisión tipo ignorante
Permítame mostrarle cómo diseñaría un sistema de detección de colisión que sea eficaz y que funcione con cualquier forma 2D al no importarme el tipo.
Digamos que se supone que debes dibujar eso. Parece simple Es todo círculos. Es tentador crear una clase circular que entienda las colisiones. El problema es que esto nos envía a una línea de pensamiento que se desmorona cuando necesitamos 1000 círculos.
No deberíamos estar pensando en círculos. Deberíamos estar pensando en píxeles.
¿Qué pasaría si te dijera que el mismo código que usas para dibujar a estos tipos es el que puedes usar para detectar cuándo se tocan o incluso en cuáles está haciendo clic el usuario?
Aquí he dibujado cada círculo con un color único (si tus ojos son lo suficientemente buenos como para ver el contorno negro, simplemente ignóralo). Esto significa que cada píxel en esta imagen oculta se asigna de nuevo a lo que lo dibujó. Un hashmap se encarga de eso muy bien. De hecho, puedes hacer polimorfismo de esta manera.
Esta imagen nunca tiene que mostrarla al usuario. Lo creas con el mismo código que dibujó el primero. Solo con diferentes colores.
Cuando el usuario hace clic en un círculo, sé exactamente qué círculo porque solo un círculo es de ese color.
Cuando dibujo un círculo encima de otro, puedo leer rápidamente cada píxel que estoy a punto de sobrescribir al volcarlos en un conjunto. Cuando termine los puntos de ajuste para cada círculo con el que colisionó y ahora solo tengo que llamar a cada uno una vez para notificarle la colisión.
Un nuevo tipo: rectángulos
Todo esto se hizo con círculos, pero le pregunto: ¿funcionaría de manera diferente con los rectángulos?
Ningún conocimiento circular se ha filtrado en el sistema de detección. No le importa el radio, la circunferencia o el punto central. Se preocupa por los píxeles y el color.
La única parte de este sistema de colisión que debe empujarse hacia las formas individuales es un color único. Aparte de eso, las formas solo pueden pensar en dibujar sus formas. Es lo que son buenos de todos modos.
Ahora, cuando escribes la lógica de colisión, no te importa qué subtipo tengas. Le dices que choque y te dice lo que encontró debajo de la forma que pretende dibujar. No es necesario saber tipo. Y eso significa que puede agregar tantos subtipos como desee sin tener que actualizar el código en otras clases.
Opciones de implementación
Realmente, no necesita ser un color único. Podrían ser referencias de objetos reales y guardar un nivel de indirección. Pero esos no se verían tan bien cuando se dibuja en esta respuesta.
Este es solo un ejemplo de implementación. Ciertamente hay otros. Lo que esto debía mostrar es que cuanto más dejes que estos subtipos de formas se adhieran a su única responsabilidad, mejor funcionará todo el sistema. Es probable que haya soluciones más rápidas y menos intensivas en memoria, pero si me obligan a difundir el conocimiento de los subtipos, no me gustaría usarlas incluso con las ganancias de rendimiento. No los usaría a menos que los necesitara claramente.
Despacho doble
Hasta ahora he ignorado por completo el doble envío . Lo hice porque pude. Mientras la lógica de colisión no le importe qué dos tipos colisionaron, no la necesita. Si no lo necesita, no lo use. Si cree que podría necesitarlo, posponga tratarlo todo el tiempo que pueda. Esta actitud se llama YAGNI .
Si decides que realmente necesitas diferentes tipos de colisiones, pregúntate si n subtipos de formas realmente necesitan n 2 tipos de colisiones. Hasta ahora he trabajado muy duro para que sea fácil agregar otro subtipo de forma. No quiero estropearlo con una implementación de doble despacho que obliga a los círculos a saber que existen cuadrados.
¿Cuántos tipos de colisiones hay de todos modos? Un poco de especulación (algo peligroso) inventa colisiones elásticas (hinchables), inelásticas (pegajosas), enérgicas (explosivas) y destructivas (dañinas). Podría haber más, pero si es menor que n 2, no sobrediseñemos nuestras colisiones.
Esto significa que cuando mi torpedo golpea algo que acepta daño, no tiene que SABER que golpeó una nave espacial. Solo tiene que decirlo: "¡Ja, ja! Recibiste 5 puntos de daño".
Las cosas que causan daño envían mensajes de daño a las cosas que aceptan mensajes de daño. Hecho de esa manera, puede agregar nuevas formas sin decirle a las otras formas sobre la nueva forma. Solo terminas extendiéndote alrededor de nuevos tipos de colisiones.
La nave espacial puede enviar de vuelta al torp "¡Ja, ja! Recibiste 100 puntos de daño". así como "Ahora estás atascado en mi casco". Y el torp puede devolver "Bueno, ya terminé, así que olvídate de mí".
En ningún momento tampoco sabe exactamente qué es cada uno. Simplemente saben cómo comunicarse entre sí a través de una interfaz de colisión.
Ahora claro, el despacho doble te permite controlar las cosas más íntimamente que esto, pero ¿realmente quieres eso ?
Si lo hace, al menos piense en hacer un despacho doble a través de abstracciones de qué tipo de colisiones acepta una forma y no en la implementación real de la forma. Además, el comportamiento de colisión es algo que puede inyectarse como una dependencia y delegar a esa dependencia.
Actuación
El rendimiento siempre es crítico. Pero eso no significa que siempre sea un problema. Prueba de rendimiento. No solo especules. Sacrificar todo lo demás en nombre del rendimiento generalmente no conduce a un código de rendimiento de todos modos.
fuente
La descripción del problema parece que debería usar métodos múltiples (también conocido como envío múltiple), en este caso particular: envío doble . La primera respuesta fue extensa sobre cómo lidiar genéricamente con formas colisionantes en el renderizado ráster, pero creo que OP quería una solución "vectorial" o tal vez todo el problema se ha reformulado en términos de Formas, que es un ejemplo clásico en las explicaciones de OOP.
Incluso el artículo de wikipedia citado utiliza la misma metáfora de colisión, permítanme citarlo (Python no tiene métodos múltiples integrados como algunos otros lenguajes):
Entonces, la siguiente pregunta es cómo obtener soporte para métodos múltiples en su lenguaje de programación.
fuente
Este problema requiere un rediseño en dos niveles.
Primero, necesita extraer la lógica para detectar la colisión entre las formas fuera de las formas. Esto es para no violar OCP cada vez que necesite agregar una nueva forma al modelo. Imagina que ya tienes un círculo, un cuadrado y un rectángulo definidos. Entonces podrías hacerlo así:
A continuación, debe hacer arreglos para que se llame al método apropiado según la forma que lo llame. Puede hacerlo utilizando el polimorfismo y el patrón de visitante . Para lograr esto, debemos tener el modelo de objeto apropiado en su lugar. Primero, todas las formas deben adherirse a la misma interfaz:
A continuación, debemos tener una clase de padres visitantes:
Estoy usando una clase aquí en lugar de la interfaz, porque necesito que cada objeto visitante tenga un atributo de
ShapeCollisionDetector
tipo.Cada implementación de
IShape
interfaz instanciaría al visitante apropiado y llamaría alAccept
método apropiado del objeto con el que interactúa el objeto que llama, de esta manera:Y los visitantes específicos se verían así:
De esta manera, no necesita cambiar las clases de forma cada vez que agrega una nueva forma, y no necesita verificar el tipo de forma para llamar al método de detección de colisión apropiado.
Un inconveniente de esta solución es que si agrega una nueva forma, debe extender la clase ShapeVisitor con el método para esa forma (por ejemplo
VisitTriangle(Triangle triangle)
) y, en consecuencia, deberá implementar ese método en todos los demás visitantes. Sin embargo, dado que esta es una extensión, en el sentido de que no se cambian los métodos existentes, sino que solo se agregan otros nuevos, esto no viola el OCP y la sobrecarga del código es mínima. Además, al usar la claseShapeCollisionDetector
, evita la violación de SRP y evita la redundancia de código.fuente
Su problema básico es que en la mayoría de los lenguajes de programación OO modernos, la sobrecarga de funciones no funciona con enlace dinámico (es decir, el tipo de argumentos de función se determina en tiempo de compilación). Lo que necesitaría es una llamada a método virtual que sea virtual en dos objetos en lugar de solo uno. Dichos métodos se llaman métodos múltiples . Sin embargo, hay formas de emular este comportamiento en lenguajes como Java, C ++, etc. Aquí es donde el despacho doble es muy útil.
La idea básica es utilizar el polimorfismo dos veces. Cuando dos formas chocan, puede llamar al método de colisión correcto de uno de los objetos a través del polimorfismo y pasar el otro objeto del tipo de forma genérica. En el método llamado, entonces sabe si este objeto es un círculo, un rectángulo o lo que sea. Luego se llama al método de colisión en el objeto de forma pasado y se le pasa este objeto. Esta segunda llamada vuelve a encontrar el tipo de objeto correcto a través del polimorfismo.
Sin embargo, un gran inconveniente de esta técnica es que cada clase en la jerarquía tiene que saber acerca de todos los hermanos. Esto supone una gran carga de mantenimiento si se agrega una nueva forma más adelante.
fuente
Quizás esta no sea la mejor manera de abordar este problema
La colisión matemática de la forma de behing es particular de las combinaciones de formas. Lo que significa que el número de subrutinas que necesitará es el cuadrado del número de formas que admite su sistema. Las colisiones de formas no son realmente operaciones en formas, sino operaciones que toman formas como parámetros.
Estrategia de sobrecarga del operador
Si no puede simplificar el problema matemático subyacente, recomendaría el enfoque de sobrecarga del operador. Algo como:
En el intializador estático, usaría la reflexión para hacer un mapa de los métodos para implementar un disparador dinámico en el método de colisión genérica (Forma s1, Forma s2). El intializador estático también puede tener una lógica para detectar funciones de colisión faltantes e informarlas, negándose a cargar la clase.
Esto es similar a la sobrecarga del operador C ++. En C ++, la sobrecarga del operador es muy confusa porque tiene un conjunto fijo de símbolos que puede sobrecargar. Sin embargo, el concepto es muy interesante y puede replicarse con funciones estáticas.
La razón por la que usaría este enfoque es que la colisión no es una operación sobre un objeto. Una colisión es una operación externa que dice alguna relación sobre dos objetos arbitrarios. Además, el inicializador estático podrá verificar si pierdo alguna función de colisión.
Simplifica tu problema matemático si es posible
Como mencioné, el número de funciones de colisión es el cuadrado del número de tipos de formas. Esto significa que en un sistema con solo 20 formas necesitará 400 rutinas, con 21 formas 441 y así sucesivamente. Esto no es fácilmente extensible.
Pero puedes simplificar tus matemáticas . En lugar de extender la función de colisión, puede rasterizar o triangular cada forma. De esa forma, el motor de colisión no necesita ser extensible. Colisión, Distancia, Intersección, Fusión y varias otras funciones serán universales.
Triangular
¿Notaste que la mayoría de los paquetes y juegos en 3D triangulan todo? Esa es una de las formas de simplificar las matemáticas. Esto también se aplica a las formas 2D. Polys se puede triangular. Los círculos y las estrías se pueden aproximar a los polígonos.
Nuevamente ... tendrá una sola función de colisión. Tu clase se convierte entonces en:
Y tus operaciones:
¿Más simple no es?
Rasterizar
Puede rasterizar su forma para tener una sola función de colisión.
La rasterización puede parecer una solución radical, pero puede ser asequible y rápida dependiendo de cuán precisas sean las colisiones de forma. Si no necesitan ser precisos (como en un juego), es posible que tenga mapas de bits de baja resolución. La mayoría de las aplicaciones no necesita precisión absoluta en las matemáticas.
Las aproximaciones pueden ser lo suficientemente buenas. La supercomputadora ANTON para simulación de biología es un ejemplo. Sus matemáticas descartan muchos efectos cuánticos que son difíciles de calcular y hasta ahora las simulaciones realizadas son consistentes con los experimentos realizados en el mundo real. Los modelos de gráficos por computadora PBR utilizados en motores de juegos y paquetes de renderizado hacen simplificaciones que reducen la potencia de la computadora necesaria para renderizar cada cuadro. En realidad no es físicamente exacto, pero está lo suficientemente cerca como para convencer a simple vista.
fuente