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:
- Los valores de estos tipos son representaciones o descripciones de caballos y unicornios (respectivamente);
- 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:
- 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;
- 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:
- Su modelo necesita tener un
Horsetipo;
- El
Horsetipo necesita codificar suficiente información para determinar inequívocamente si algún valor describe un unicornio;
- Algunas operaciones del
Horsetipo necesitan exponer esa información para que los clientes del tipo puedan observar si un dado Horsees un unicornio;
- 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).