Dada una manada de caballos, ¿cómo encuentro la longitud promedio del cuerno de todos los unicornios?

30

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 HornMeasurerpor 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?

placa de moarboiler
fuente
22
Los caballos no tienen cuernos, por lo que el promedio no está definido (0/0).
Scott Whitlock
3
@moarboilerplate En cualquier lugar del 10 al infinito.
niñera
44
@StephenP: Eso no funcionaría matemáticamente para este caso; todos esos ceros sesgarían el promedio.
Mason Wheeler
3
Si su pregunta se responde mejor con una no respuesta, entonces no pertenece a un sitio de preguntas y respuestas; reddit, quora u otros sitios basados ​​en la discusión están diseñados para material de tipo sin respuesta ... dicho eso, creo que puede ser claramente responsable si está buscando el código que @MasonWheeler le dio, si no creo que no tengo idea lo que estás tratando de preguntar ..
Jimmy Hoffa
3
@JimmyHoffa "lo estás haciendo mal" resulta ser una "no respuesta" aceptable y muchas veces es mejor que "bueno, aquí hay una forma de hacerlo": no se requiere una discusión prolongada.
moarboilerplate

Respuestas:

11

Suponiendo que desee tratar a Unicorncomo un tipo especial Horse, 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 los Unicornrasgos. 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):

case class Horse(val hornLength: Option[Double])

val horse = Horse(None)
val unicorn = Horse(Some(12.0))
val anotherUnicorn = Horse(Some(6.0))

val herd = List(horse, unicorn, anotherUnicorn)
val hornLengths = herd flatMap {_.hornLength}
val averageLength = hornLengths.sum / hornLengths.size

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 flatMapfiltrar fácilmente los Noneelementos.

Karl Bielefeldt
fuente
77
Por supuesto, esto supone que la única diferencia entre un caballo ordinario y un unicornio es el cuerno. Si este no es el caso, entonces las cosas se complican mucho más rápidamente.
Mason Wheeler
@MasonWheeler solo en el segundo método presentado.
moarboilerplate
1
Observe los comentarios sobre cómo los no unicornios y los unicornios nunca deben estar juntos en un escenario de herencia hasta que se encuentre en un contexto en el que no le importen los unicornios. Claro, .OfType () puede resolver el problema y hacer que las cosas funcionen, pero está resolviendo un problema que ni siquiera debería existir en primer lugar. En cuanto al segundo enfoque, funciona porque las opciones son muy superiores a confiar en nulo para implicar algo. Creo que el segundo enfoque se puede lograr en OO con un compromiso si encapsulas los rasgos de unicornio en una propiedad independiente y estás extremadamente atento.
moarboilerplate
1
Comprométete si encapsulas los rasgos de unicornio en una propiedad independiente y eres extremadamente vigilante : ¿por qué hacerte la vida difícil? Use typeof directamente y ahorre una tonelada de problemas futuros.
gbjbaanb
@gbjbaanb Consideraría que este enfoque solo es realmente apropiado para escenarios en los que una anémica Horsetenía una IsUnicornpropiedad y algún tipo de UnicornStuffpropiedad con la longitud de la bocina (cuando se escala para el piloto / brillo mencionado en su pregunta).
moarboilerplate
38

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.

Mason Wheeler
fuente
Perdóname si mi sintaxis lambda no es correcta; No soy muy codificador de C # y nunca puedo mantener en orden detalles arcanos como este. Sin embargo, debería quedar claro a qué me refiero.
Mason Wheeler
1
No se preocupe, el problema se resuelve una vez que la lista solo contiene Unicorns de todos modos (para el registro que podría omitir return).
moarboilerplate
44
Esta es la respuesta que elegiría si quisiera resolver el problema rápidamente. Pero no es la respuesta si quisiera refactorizar el código para que sea más plausible.
Andy
66
Esta es definitivamente la respuesta a menos que necesite un nivel absurdo de optimización. La claridad y la facilidad de lectura hacen que casi todo lo demás sea discutible.
David dice que reinstale a Mónica el
1
@DavidGrinberg, ¿qué pasaría si escribir este método limpio y legible significara que primero tendría que implementar una estructura de herencia que anteriormente no existía?
moarboilerplate
9

No hay nada malo en .NET con:

var unicorn = animal as Unicorn;
if(unicorn != null)
{
    sum += unicorn.HornLength;
    count++;
}

Usar el equivalente de Linq también está bien:

var averageUnicornHornLength = animals
    .OfType<Unicorn>()
    .Select(x => x.HornLength)
    .Average();

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?

var averageHornedAnimalHornLength = animals
    .OfType<IHornedAnimal>()
    .Select(x => x.HornLength)
    .Average();

Tenga en cuenta que cuando use Linq, Average(y Miny Max) 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:

var hornedAnimals = animals
    .OfType<IHornedAnimal>()
    .ToList();
if(hornedAnimals.Count > 0)
{
    var averageHornLengthOfHornedAnimals = hornedAnimals
        .Average(x => x.HornLength);
}
else
{
    // deal with it in your own way...
}

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.

Scott Whitlock
fuente
77
Ya sabes que animal es un Unicorn ; simplemente eche en lugar de usar as, o potencialmente incluso mejor uso as y luego verifique si es nulo.
Philip Kendall
3

Pero el hecho de que finalmente termines interrogando el tipo de un objeto en tiempo de ejecución no me sienta bien.

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, typeofpor 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 ifdeclaració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)

gbjbaanb
fuente
En un contexto heredado, if horn > 0es 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, y horn > 0está 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"
moarboilerplate
@moarboilerplate lo dices tú mismo, ve con la solución barata y fácil y se convertirá en un desastre. Es por eso que se inventaron los lenguajes OO, como una solución a este tipo de problema. La subclasificación de caballos puede parecer cara al principio, pero pronto se amortiza. Continuar con la solución simple, pero turbia, cuesta más y más con el tiempo.
gbjbaanb
3

Dado que la pregunta tiene una functional-programmingetiqueta, 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 #:

type Equine =
| Horse
| Unicorn of hornLength: float

module equines =

  let averageHornLength (equines : Equine list) =
    equines 
    |> List.choose (fun x -> 
      match x with
      | Unicorn u -> Some(u)
      | _ -> None)
    |> List.average

let herd = [ Horse ; Horse ; Unicorn(35.0) ; Horse ; Unicorn(50.0) ]

printfn "Average horn length in herd : %f" (equines.averageHornLength herd) // prints 42.5

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 Equineaparezca algún día.

guillaume31
fuente
2

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

Horse.visit(EquineVisitor v)

La interfaz de visitante equina se parece a esto en java / c #

interface EquineVisitor {
  void visitHorse(Horse z);
  void visitUnicorn(Unicorn z);
}

Unicorn.visit(EquineVisitor v){
   v.visitUnicorn(this);
}

Horse.visit(EquineVisitor v){
   v.visitHorse(this);
}

Para medir cuernos ahora escribimos ...

class HornMeasurer implements EquineVistor {
    void visitHorse(Horse h){} // ignore horses
    void visitUnicorn(Unicorn u){
         double len = u.getHornLength();
         totalLength+=len;
         unicornCount++;
    }

    double getAverageLength(){
          return totalLength/unicornCount;
    }

    double totalLength=0;
    int unicornCount=0;
}

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.

Tim Williscroft
fuente
Estaba a punto de responder yo mismo con el patrón de visitante. ¡Tuve que desplazarme hacia abajo de una manera sorprendente para encontrar si alguien ya lo había mencionado!
Ben Thurley
0

Suponiendo que en su arquitectura los unicornios son una subespecie de caballo y encuentra lugares donde obtiene una colección de Horsedonde 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:

foreach (var horse in horses)
{
    try
    {
        var length = horse.MeasureHorn();
        //...
    }
    catch (NoHornException e)
    {
        continue;
    }
}

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 ifparece 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í:

class Horse
{
    public double MeasureHorn() { return -1; }
    //...
}

class Unicorn : Horse
{
    public override double MeasureHorn { return _hornLength; }
    //...
}

Ahora su Horseclase tiene que saber sobre la Unicornclase y tener métodos adicionales para lidiar con cosas que no le importan. Ahora imagine que también tiene Pegasussy Zebras que heredan de Horse. Ahora Horsenecesita un Flymétodo así como MeasureWings,CountStripes etc. Y luego la Unicornclase 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 Horses para decir si algo es Unicornay 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> unicornseso 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 como List<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.

Becuzz
fuente
La excepción silenciosa es definitivamente mala: mi propuesta era un control que sería if(horse.IsUnicorn) horse.MeasureHorn();y las excepciones no se detectarían , ya sea que se activen cuando !horse.IsUnicorny esté en un contexto de medición de unicornio, o dentro MeasureHornde 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.
moarboilerplate
0

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 Horsey un Unicorntipo. ¿Cuáles son los valores de estos tipos? Bueno, yo diría esto:

  1. Los valores de estos tipos son representaciones o descripciones de caballos y unicornios (respectivamente);
  2. Son representaciones o descripciones esquematizadas : no son de forma libre, están construidas de acuerdo con reglas muy estrictas.

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 Circlesea ​​un Ellipse, por así decirlo. Pero sí significa que:

  1. Hay una función total que convierte cualquier Circle(descripción de círculo esquematizada) en un Ellipse(tipo diferente de descripción) que describe los mismos círculos;
  2. Hay una función parcial que toma un Ellipsey, si describe un círculo, devuelve el correspondiente Circle.

Entonces, en términos de programación funcional, su Unicorntipo no necesita ser un subtipo Horse, solo necesita operaciones como estas:

-- Convert any unicorn-description of into a horse-description that
-- describes the same unicorns.
toHorse :: Unicorn -> Horse

-- If the horse described by the given horse-description is a unicorn,
-- then return a unicorn-description of that unicorn, otherwise return
-- nothing.
toUnicorn :: Horse -> Maybe Unicorn

Y toUnicorndebe ser un inverso correcto de toHorse:

toUnicorn (toHorse x) = Just x

El Maybetipo de Haskell es lo que otros idiomas llaman un tipo de "opción". Por ejemplo, el Optional<Unicorn>tipo Java 8 es un Unicorno 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:

  1. Su modelo necesita tener un Horsetipo;
  2. El Horsetipo necesita codificar suficiente información para determinar inequívocamente si algún valor describe un unicornio;
  3. Algunas operaciones del Horsetipo necesitan exponer esa información para que los clientes del tipo puedan observar si un dado Horsees un unicornio;
  4. Los clientes del Horsetipo tendrán que usar estas últimas operaciones en tiempo de ejecución para discriminar entre unicornios y caballos.

Así que esto es fundamentalmente un Horsemodelo de "preguntar a todos si es un unicornio". Usted desconfía de ese modelo, pero creo que sí. Si le doy una lista de Horses, 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:

  • Tener un Horsetipo;
  • Tener Unicorncomo subtipo de Horse;
  • Utilice la reflexión de tipo de tiempo de ejecución como la operación accesible para el cliente que discierne si un dado Horsees un Unicorn.

Esto tiene una gran debilidad, cuando lo miras desde el ángulo "cosa versus descripción" que presenté arriba:

  • ¿Qué pasa si tienes una Horseinstancia que describe un unicornio pero no es una Unicorninstancia?

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 Horsesi es una Unicorninstancia no es sinónimo de preguntar Horsesi es un unicornio (si es una Horsedescripció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 de Horsesmanera que cada vez que un cliente intente construir un Horseque describa un unicornio, Unicornse 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 Horses en Unicorns. Esto podría ser un método del Horsetipo:

interface Horse {
    // ...
    Optional<Unicorn> toUnicorn();
}

... o podría ser un objeto externo (su "objeto separado en un caballo que le dice si el caballo es un unicornio o no"):

class HorseToUnicornCoercion {
    Optional<Unicorn> convert(Horse horse) {
       // ...
    }
}

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 Unicornoperación desde arriba, solo lo empaqueta de diferentes maneras (lo que ciertamente tendrá efectos secundarios sobre qué operaciones Horsenecesita el tipo para exponer a sus clientes).

sacundim
fuente
-1

El comentario de OP en otra respuesta aclaró la pregunta, pensé

eso es parte de lo que la pregunta también hace. Si tengo una manada de caballos, y algunos de ellos son conceptualmente unicornios, ¿cómo deberían existir para que el problema pueda resolverse limpiamente sin demasiados impactos negativos?

Dicho de esa manera, creo que necesitamos más información. La respuesta probablemente depende de varias cosas:

  • Nuestras instalaciones lingüísticas. Por ejemplo, probablemente abordaría esto de manera diferente en ruby, javascript y Java.
  • Los conceptos mismos: ¿qué es un caballo y qué es un unicornio? ¿Qué datos están asociados con cada uno? ¿Son exactamente iguales excepto por la bocina, o tienen otras diferencias?
  • ¿De qué otra forma los estamos usando, además de tomar promedios de longitud de bocina? ¿Y qué hay de los rebaños? ¿Quizás deberíamos modelarlos también? ¿Los usamos en otro lugar? herd.averageHornLength()parece coincidir con nuestro modelo conceptual.
  • ¿Cómo se crean los objetos caballo y unicornio? ¿Cambiar ese código dentro de los límites de nuestra refactorización?

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 ...

Jonás
fuente
1
Puntos justos. Para evitar que el problema se vuelva aún más abstracto, tenemos que hacer algunas suposiciones razonables: 1) un lenguaje fuertemente tipado 2) el rebaño limita a los caballos a un tipo, probablemente debido a una colección 3) probablemente deberían evitarse las técnicas como la tipificación de patos . En cuanto a lo que se puede cambiar, no hay necesariamente ninguna limitación, pero cada tipo de cambio tiene sus propias consecuencias ...
moarboilerplate
Si la manada restringe a los caballos a un tipo, no son nuestras únicas opciones de herencia (no me gusta esa opción) o un objeto contenedor (digamos HerdMember) que inicializamos con un caballo o un unicornio (liberando al caballo y al unicornio de la necesidad de una relación de subtipo ) HerdMemberes libre de implementar isUnicorn()como le parezca, y la solución de filtrado que sugiero sigue.
Jonás
En algunos idiomas, hornLength () se puede mezclar, y si ese es el caso, puede ser una solución válida. Sin embargo, en los idiomas en los que la escritura es menos flexible, debe recurrir a algunas técnicas de hackeo para hacer lo mismo, o debe hacer algo como poner la longitud de la horquilla en un caballo donde puede generar confusión en el código porque un caballo no t conceptualmente tiene cuernos. Además, si hacer cálculos matemáticos, incluir valores predeterminados puede sesgar los resultados (ver comentarios en la pregunta original)
moarboilerplate
Sin embargo, los mixins, a menos que se realicen en tiempo de ejecución, son simplemente herencia bajo otro nombre. Su comentario "un caballo no tiene conceptualmente cuernos" se relaciona con mi comentario sobre la necesidad de saber más acerca de lo que son, si nuestra respuesta debe incluir cómo modelamos caballos y unicornios y cuál es su relación entre ellos. Cualquier solución que incluya valores predeterminados está fuera de control incorrectamente.
Jonás
Tienes razón en que para obtener una solución precisa para una manifestación específica de este problema necesitas tener mucho contexto. Para responder a su pregunta sobre un caballo con cuerno y vincularlo a los mixins, estaba pensando en un escenario donde un hornLength mezclado con un caballo que no es un unicornio es un error. Considere un rasgo de Scala que tiene una implementación predeterminada para hornLength que arroja una excepción. Un tipo de unicornio puede anular esa implementación, y si un caballo lo convierte en un contexto donde se evalúa hornLength, es una excepción.
moarboilerplate
-2

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.

Martin Maat
fuente
Estoy de acuerdo con ésto. Mason Wheeler también tiene una buena solución en su respuesta, pero si necesita identificar unicornios por muchas razones diferentes en diferentes lugares, su código tendrá muchas horses.ofType<Unicorn>...construcciones. Tener una GetUnicornsfunció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.
Shaz
@Ryan Si devuelve un 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 el Horse. Si Unicornes su propio tipo, debe crear un nuevo tipo y mantener asignaciones de tipos, lo que puede introducir una sobrecarga.
moarboilerplate
1
@moarboilerplate: consideramos todo eso de apoyo a la solución. La parte de belleza es que es independiente de cualquier detalle de implementación de unicornio. Ya sea que discrimine en función de un miembro de datos, una clase o una hora del día (todos esos caballos pueden convertirse en unicornios a la medianoche si la luna es la correcta, por lo que sé), la solución se mantiene, la interfaz sigue siendo la misma.
Martin Maat