La pregunta anterior es un ejemplo abstracto de un problema común que encuentro en el código heredado, o más exactamente, problemas resultantes de intentos anteriores para resolver este problema.
Puedo pensar en al menos un método de .NET Framework destinado a abordar este problema, como el Enumerable.OfType<T>
método. Pero el hecho de que finalmente termines interrogando el tipo de un objeto en tiempo de ejecución no me satisface.
Más allá de preguntarle a cada caballo "¿Eres un unicornio?" También se me ocurren los siguientes enfoques:
- Lanza una excepción cuando se intenta obtener la longitud del cuerno de un no-unicornio (expone la funcionalidad no apropiada para cada caballo)
- Devuelve un valor predeterminado o mágico para la longitud del cuerno de un no-unicornio (requiere controles predeterminados salpicados a lo largo de cualquier código que quiera romper las estadísticas del cuerno en un grupo de caballos que podrían no ser unicornios)
- Elimine la herencia y cree un objeto separado en un caballo que le diga si el caballo es un unicornio o no (lo que potencialmente está empujando el mismo problema por una capa)
Tengo la sensación de que esto se responderá mejor con una "no respuesta". Pero, ¿cómo aborda este problema y, si depende, cuál es el contexto en torno a su decisión?
También me interesaría cualquier idea sobre si este problema todavía existe en el código funcional (¿o tal vez solo existe en lenguajes funcionales que admiten la mutabilidad?)
Esto se marcó como un posible duplicado de la siguiente pregunta: ¿Cómo evitar el downcasting?
La respuesta a esa pregunta supone que uno está en posesión de una HornMeasurer
por la cual todas las mediciones de la bocina deben hacerse. Pero eso es una imposición en una base de código que se formó bajo el principio igualitario de que todos deberían ser libres de medir el cuerno de un caballo.
En ausencia de a HornMeasurer
, el enfoque de la respuesta aceptada refleja el enfoque basado en excepciones mencionado anteriormente.
También ha habido cierta confusión en los comentarios sobre si los caballos y los unicornios son equinos, o si un unicornio es una subespecie mágica de caballo. Deben considerarse ambas posibilidades: ¿quizás una sea preferible a la otra?
fuente
Respuestas:
Suponiendo que desee tratar a
Unicorn
como un tipo especialHorse
, existen básicamente dos formas de modelarlo. La forma más tradicional es la relación de subclase. Puede evitar verificar el tipo y el downcasting simplemente refactorizando su código para mantener siempre las listas separadas en los contextos donde importa, y solo combinarlas en los contextos donde nunca le interesan losUnicorn
rasgos. En otras palabras, lo arreglas para que nunca te metas en la situación en la que necesitas extraer unicornios de una manada de caballos en primer lugar. Esto parece difícil al principio, pero es posible en el 99,99% de los casos, y generalmente hace que su código sea mucho más limpio.La otra forma de modelar un unicornio es simplemente dando a todos los caballos una longitud de bocina opcional. Luego, puede probar si es un unicornio comprobando si tiene una longitud de bocina, y encontrar la longitud promedio de la bocina de todos los unicornios por (en Scala):
Este método tiene la ventaja de ser más directo, con una sola clase, pero la desventaja de ser mucho menos extensible y tener una forma indirecta de verificar la "unicorniosidad". El truco si opta por esta solución es reconocer, cuando comienza a extenderla con frecuencia, que necesita pasar a una arquitectura más flexible. Este tipo de solución es mucho más popular en lenguajes funcionales donde tiene funciones simples y potentes como
flatMap
filtrar fácilmente losNone
elementos.fuente
Horse
tenía unaIsUnicorn
propiedad y algún tipo deUnicornStuff
propiedad con la longitud de la bocina (cuando se escala para el piloto / brillo mencionado en su pregunta).Has cubierto prácticamente todas las opciones. Si tiene un comportamiento que depende de un subtipo específico y está mezclado con otros tipos, su código debe tener en cuenta ese subtipo; Es un razonamiento lógico simple.
Personalmente, simplemente iría con
horses.OfType<Unicorn>().Average(u => u.HornLength)
. Expresa la intención del código muy claramente, que a menudo es lo más importante ya que alguien tendrá que mantenerlo más adelante.fuente
Unicorn
s de todos modos (para el registro que podría omitirreturn
).No hay nada malo en .NET con:
Usar el equivalente de Linq también está bien:
Según la pregunta que hizo en el título, este es el código que esperaría encontrar. Si la pregunta hiciera algo como "¿qué es el promedio de animales con cuernos", sería diferente?
Tenga en cuenta que cuando use Linq,
Average
(yMin
yMax
) arrojarán una excepción si el enumerable está vacío y el tipo T no es anulable. Eso es porque el promedio realmente no está definido (0/0). Entonces realmente necesitas algo como esto:Editar
Simplemente creo que esto necesita agregarse ... una de las razones por las que una pregunta como esta no se adapta bien a los programadores orientados a objetos es que supone que estamos usando clases y objetos para modelar una estructura de datos. La idea original de Smalltalk-esque orientada a objetos era estructurar su programa a partir de módulos que fueron instanciados como objetos y le prestaron servicios cuando les envió un mensaje. El hecho de que también podamos usar clases y objetos para modelar una estructura de datos es un efecto secundario (útil), pero son dos cosas diferentes. Ni siquiera creo que este último deba considerarse una programación orientada a objetos, ya que podría hacer lo mismo con un
struct
, pero simplemente no sería tan bonito.Si está utilizando la programación orientada a objetos para crear servicios que hacen cosas por usted, entonces, por buenas razones, no está bien consultar si ese servicio es en realidad otro servicio o implementación concreta. Se le proporcionó una interfaz (generalmente mediante inyección de dependencia) y debe codificar esa interfaz / contrato.
Por otro lado, si está (mal) usando las ideas de clase / objeto / interfaz para crear una estructura de datos o modelo de datos, entonces personalmente no veo ningún problema con el uso de la idea is-a en toda su extensión. Si ha definido que los unicornios son un subtipo de caballos y tiene mucho sentido dentro de su dominio, entonces continúe y consulte a los caballos de su rebaño para encontrar los unicornios. Después de todo, en un caso como este, generalmente estamos tratando de crear un lenguaje específico de dominio para expresar mejor las soluciones de los problemas que tenemos que resolver. En ese sentido, no hay nada malo con
.OfType<Unicorn>()
etc.En última instancia, tomar una colección de elementos y filtrarlos por tipo es realmente solo programación funcional, no programación orientada a objetos. Afortunadamente, lenguajes como C # se sienten cómodos manejando ambos paradigmas ahora.
fuente
animal
es unUnicorn
; simplemente eche en lugar de usaras
, o potencialmente incluso mejor usoas
y luego verifique si es nulo.El problema con esta afirmación es que, no importa qué mecanismo uses, siempre estarás interrogando al objeto para saber de qué tipo es. Eso puede ser RTTI o puede ser una unión o una estructura de datos simple donde usted pregunta
if horn > 0
. Los detalles exactos cambian ligeramente, pero la intención es la misma: le preguntas al objeto sobre sí mismo de alguna manera para ver si debes interrogarlo más.Dado eso, tiene sentido usar el soporte de su idioma para hacer esto. En .NET usarías,
typeof
por ejemplo.La razón para hacerlo va más allá de usar bien su idioma. Si tiene un objeto que se parece a otro pero por algún pequeño cambio, es probable que encuentre más diferencias con el tiempo. En su ejemplo de unicornios / caballos, puede decir que solo hay una longitud de bocina ... pero mañana verificará si un jinete potencial es virgen o si la caca es brillante. (un ejemplo clásico del mundo real sería los widgets GUI que se derivan de una base común y hay que buscar las casillas de verificación y los cuadros de lista de manera diferente. El número de diferencias sería demasiado grande para crear simplemente un súper objeto que contenga todas las permutaciones de datos posibles )
Si verificar el tipo de un objeto en tiempo de ejecución no funciona bien, entonces su alternativa es dividir los diferentes objetos desde el principio; en lugar de almacenar una sola manada de unicornios / caballos, tiene 2 colecciones, una para caballos, otra para unicornios . Esto puede funcionar muy bien, incluso si los almacena en un contenedor especializado (por ejemplo, un mapa múltiple donde la clave es el tipo de objeto ... pero luego, aunque los almacenamos en 2 grupos, volvemos a interrogar el tipo de objeto !)
Ciertamente, un enfoque basado en excepciones está mal. Usar excepciones como el flujo normal del programa es un olor a código (si tuvieras una manada de unicornios y un burro con una concha marina pegada a la cabeza, entonces diría que el enfoque basado en excepciones está bien, pero si tienes una manada de unicornios y los caballos luego verifican que cada unicornio no sea inesperado. Las excepciones son para circunstancias excepcionales, no una
if
declaración complicada ). En cualquier caso, el uso de excepciones para este problema es simplemente interrogar el tipo de objeto en tiempo de ejecución, solo que aquí está haciendo mal uso de la función de lenguaje para verificar si no hay objetos que no sean unicornios. También podría codificar en unif horn > 0
y al menos procese su colección de forma rápida y clara, utilizando menos líneas de código y evitando cualquier problema que surja cuando se lanzan otras excepciones (por ejemplo, una colección vacía o tratando de medir la concha de ese burro)fuente
if horn > 0
es más o menos la forma en que se resuelve este problema al principio. Entonces, los problemas que generalmente surgen son cuando quieres verificar a los jinetes y el brillo, yhorn > 0
está enterrado por todos lados en un código no relacionado (también el código sufre errores misteriosos debido a la falta de controles para cuando la bocina es 0). Además, la subclasificación de caballos después del hecho suele ser la propuesta más costosa, por lo que generalmente no estoy dispuesto a hacerlo si todavía están encerrados juntos al final del refactorizador. Así que ciertamente se convierte en "cuán feas son las alternativas"Dado que la pregunta tiene una
functional-programming
etiqueta, podríamos usar un tipo de suma para reflejar los dos sabores de los caballos y la coincidencia de patrones para desambiguar entre ellos. Por ejemplo, en F #:Sobre OOP, FP tiene la ventaja de la separación de datos / funciones, lo que quizás lo salve de la (¿injustificada?) "Conciencia culpable" de violar el nivel de abstracción al rechazar subtipos específicos de una lista de objetos de un supertipo.
En contraste con las soluciones OO propuestas en otras respuestas, la coincidencia de patrones también proporciona un punto de extensión más fácil en caso de que otra especie de cuernos
Equine
aparezca algún día.fuente
La forma breve de la misma respuesta al final requiere leer un libro o un artículo web.
Patrón de visitante
El problema tiene una mezcla de caballos y unicornios. (Violar el principio de sustitución de Liskov es un problema común en las bases de código heredadas).
Agregue un método a horse y todas las subclases
La interfaz de visitante equina se parece a esto en java / c #
Para medir cuernos ahora escribimos ...
El patrón de visitante es criticado por dificultar la refactorización y el crecimiento.
Respuesta corta: Use el patrón de diseño Visitante para obtener el envío doble.
ver también https://en.wikipedia.org/wiki/Visitor_pattern
vea también http://c2.com/cgi/wiki?VisitorPattern para la discusión de los visitantes.
ver también Patrones de diseño de Gamma et al.
fuente
Suponiendo que en su arquitectura los unicornios son una subespecie de caballo y encuentra lugares donde obtiene una colección de
Horse
donde algunos de ellos pueden estarUnicorn
, personalmente elegiría el primer método (.OfType<Unicorn>()...
) porque es la forma más directa de expresar su intención. . Para cualquiera que venga más tarde (incluido usted mismo en 3 meses), es inmediatamente obvio lo que está tratando de lograr con ese código: elija los unicornios de entre los caballos.Los otros métodos que enumeró se sienten como otra forma de hacer la pregunta "¿Eres un unicornio?". Por ejemplo, si usa algún tipo de método basado en excepciones para medir cuernos, es posible que tenga un código similar a este:
Entonces, la excepción se convierte en el indicador de que algo no es un unicornio. Y ahora esto ya no es realmente una situación excepcional , sino que es parte del flujo normal del programa. Y usar una excepción en lugar de una
if
parece aún más sucio que simplemente hacer la verificación de tipo.Digamos que sigues la ruta del valor mágico para controlar los cuernos de los caballos. Así que ahora tus clases se ven así:
Ahora su
Horse
clase tiene que saber sobre laUnicorn
clase y tener métodos adicionales para lidiar con cosas que no le importan. Ahora imagine que también tienePegasus
syZebra
s que heredan deHorse
. AhoraHorse
necesita unFly
método así comoMeasureWings
,CountStripes
etc. Y luego laUnicorn
clase también obtiene estos métodos. Ahora todas sus clases tienen que conocerse unas a otras y las ha contaminado con un montón de métodos que no deberían estar allí solo para evitar preguntar al sistema de tipos "¿Es esto un unicornio?"Entonces, ¿qué hay de agregar algo a
Horse
s para decir si algo esUnicorn
ay manejar todas las mediciones de bocina? Bueno, ahora tienes que verificar la existencia de este objeto para saber si algo es un unicornio (que solo sustituye un cheque por otro). También enturbia un poco las aguas porque ahora es posible que tengas unList<Horse> unicorns
eso realmente contiene todos los unicornios, pero el sistema de tipos y el depurador no pueden decirlo fácilmente. "Pero sé que todo es unicornios", dices, "incluso el nombre lo dice". Bueno, ¿y si algo se llama mal? O digamos, usted escribió algo con la suposición de que realmente serían todos unicornios, pero luego los requisitos cambiaron y ahora también podría tener pegasi mezclado. (Porque nunca sucede nada como esto, especialmente en software / sarcasmo heredado). Ahora el sistema de tipos felizmente pondrá tu pegasi con tus unicornios. Si su variable hubiera sido declarada comoList<Unicorn>
el compilador (o el entorno de ejecución) encajaría si intentara mezclar pegasi o caballos.Finalmente, todos estos métodos son solo un reemplazo para la verificación del sistema de tipos. Personalmente, preferiría no reinventar la rueda aquí y espero que mi código funcione tan bien como algo que está integrado y que miles de otros programadores han probado miles de veces.
Finalmente, el código debe ser entendible para usted . La computadora lo resolverá independientemente de cómo lo escriba. Usted es quien tiene que depurarlo y poder razonar al respecto. Haga la elección que haga su trabajo más fácil. Si por alguna razón, uno de estos otros métodos le ofrece una ventaja que supera el código más claro en los dos puntos que aparecería, hágalo. Pero eso depende de su base de código.
fuente
if(horse.IsUnicorn) horse.MeasureHorn();
y las excepciones no se detectarían , ya sea que se activen cuando!horse.IsUnicorn
y esté en un contexto de medición de unicornio, o dentroMeasureHorn
de un no-unicornio. De esta forma, cuando se lanza la excepción, no oculta los errores, explota por completo y es una señal de que algo debe corregirse. Obviamente, solo es apropiado para ciertos escenarios, pero es una implementación que no utiliza excepciones para determinar una ruta de ejecución.Bueno, parece que su dominio semántico tiene una relación IS-A, pero desconfía de usar subtipos / herencia para modelar esto, particularmente debido a la reflexión del tipo de tiempo de ejecución. Sin embargo, creo que tienes miedo de lo incorrecto: el subtipo sí conlleva peligros, pero el hecho de que estés consultando un objeto en tiempo de ejecución no es el problema. Verás a qué me refiero.
La programación orientada a objetos se ha apoyado bastante en la noción de relaciones IS-A, posiblemente se ha apoyado demasiado en ella, lo que lleva a dos conceptos críticos famosos:
Pero creo que hay otra forma más basada en la programación funcional de ver las relaciones IS-A que quizás no tenga estas dificultades. Primero, queremos modelar caballos y unicornios en nuestro programa, por lo que vamos a tener un
Horse
y unUnicorn
tipo. ¿Cuáles son los valores de estos tipos? Bueno, yo diría esto:Eso puede sonar obvio, pero creo que una de las formas en que las personas se involucran en problemas como el problema de círculo-elipse es no prestar atención a esos puntos con suficiente cuidado. Cada círculo es una elipse, pero eso no significa que cada descripción esquematizada de un círculo sea automáticamente una descripción esquematizada de una elipse de acuerdo con un esquema diferente. En otras palabras, solo porque un círculo es una elipse no significa que a
Circle
sea unEllipse
, por así decirlo. Pero sí significa que:Circle
(descripción de círculo esquematizada) en unEllipse
(tipo diferente de descripción) que describe los mismos círculos;Ellipse
y, si describe un círculo, devuelve el correspondienteCircle
.Entonces, en términos de programación funcional, su
Unicorn
tipo no necesita ser un subtipoHorse
, solo necesita operaciones como estas:Y
toUnicorn
debe ser un inverso correcto detoHorse
:El
Maybe
tipo de Haskell es lo que otros idiomas llaman un tipo de "opción". Por ejemplo, elOptional<Unicorn>
tipo Java 8 es unUnicorn
o nada. Tenga en cuenta que dos de sus alternativas, lanzar una excepción o devolver un "valor predeterminado o mágico", son muy similares a los tipos de opciones.Entonces, básicamente, lo que he hecho aquí es reconstruir el concepto de relación IS-A en términos de tipos y funciones, sin usar subtipos ni herencia. Lo que sacaría de esto es:
Horse
tipo;Horse
tipo necesita codificar suficiente información para determinar inequívocamente si algún valor describe un unicornio;Horse
tipo necesitan exponer esa información para que los clientes del tipo puedan observar si un dadoHorse
es un unicornio;Horse
tipo tendrán que usar estas últimas operaciones en tiempo de ejecución para discriminar entre unicornios y caballos.Así que esto es fundamentalmente un
Horse
modelo de "preguntar a todos si es un unicornio". Usted desconfía de ese modelo, pero creo que sí. Si le doy una lista deHorse
s, todo lo que el tipo garantiza es que las cosas que describen los elementos en la lista son caballos, por lo que inevitablemente tendrá que hacer algo en el tiempo de ejecución para saber cuáles de ellos son unicornios. Así que creo que no hay forma de evitarlo; debe implementar operaciones que lo hagan por usted.En la programación orientada a objetos, la forma familiar de hacerlo es la siguiente:
Horse
tipo;Unicorn
como subtipo deHorse
;Horse
es unUnicorn
.Esto tiene una gran debilidad, cuando lo miras desde el ángulo "cosa versus descripción" que presenté arriba:
Horse
instancia que describe un unicornio pero no es unaUnicorn
instancia?Volviendo al principio, esto es lo que creo que es la parte realmente aterradora del uso de subtipos y downcasts para modelar esta relación IS-A, no el hecho de que tenga que hacer una verificación de tiempo de ejecución. Abusar un poco de la tipografía, preguntar
Horse
si es unaUnicorn
instancia no es sinónimo de preguntarHorse
si es un unicornio (si es unaHorse
descripción de un caballo que también es un unicornio). No, a menos que su programa haya hecho todo lo posible para encapsular el código que construye deHorses
manera que cada vez que un cliente intente construir unHorse
que describa un unicornio,Unicorn
se crea una instancia de la clase. En mi experiencia, rara vez los programadores hacen las cosas con cuidado.Así que iría con el enfoque donde hay una operación explícita, no abatida, que convierte
Horse
s enUnicorn
s. Esto podría ser un método delHorse
tipo:... o podría ser un objeto externo (su "objeto separado en un caballo que le dice si el caballo es un unicornio o no"):
La elección entre estos es una cuestión de cómo está organizado su programa: en ambos casos, tiene el equivalente de mi
Horse -> Maybe Unicorn
operación desde arriba, solo lo empaqueta de diferentes maneras (lo que ciertamente tendrá efectos secundarios sobre qué operacionesHorse
necesita el tipo para exponer a sus clientes).fuente
El comentario de OP en otra respuesta aclaró la pregunta, pensé
Dicho de esa manera, creo que necesitamos más información. La respuesta probablemente depende de varias cosas:
herd.averageHornLength()
parece coincidir con nuestro modelo conceptual.Sin embargo, en general, ni siquiera pensaría en herencia y subtipos aquí. Tienes una lista de objetos. Algunos de esos objetos pueden identificarse como unicornios, tal vez porque tienen un
hornLength()
método. Filtra la lista en función de esta propiedad única de unicornio. Ahora el problema se ha reducido a promediar la longitud de la bocina de una lista de unicornios.OP, avísame si sigo malinterpretando ...
fuente
HerdMember
) que inicializamos con un caballo o un unicornio (liberando al caballo y al unicornio de la necesidad de una relación de subtipo )HerdMember
es libre de implementarisUnicorn()
como le parezca, y la solución de filtrado que sugiero sigue.Un método GetUnicorns () que devuelve un IEnumerable me parece la solución más elegante, flexible y universal. De esta forma, podría lidiar con cualquier (combinación de) rasgos que determinen si un caballo pasará como un unicornio, no solo el tipo de clase o el valor de una propiedad en particular.
fuente
horses.ofType<Unicorn>...
construcciones. Tener unaGetUnicorns
función sería de una sola línea, pero sería más resistente a los cambios en la relación caballo / unicornio desde la perspectiva de la persona que llama.IEnumerable<Horse>
, aunque su criterio de unicornio está en un solo lugar, está encapsulado, por lo que las personas que llaman deben hacer suposiciones sobre por qué necesitan unicornios (puedo obtener sopa de almejas al ordenar la sopa del día hoy, pero eso no lo hace) t significa que lo conseguiré mañana haciendo lo mismo). Además, debe exponer un valor predeterminado para una bocina en elHorse
. SiUnicorn
es su propio tipo, debe crear un nuevo tipo y mantener asignaciones de tipos, lo que puede introducir una sobrecarga.