¿Cómo manejar los métodos que se han agregado para subtipos en el contexto del polimorfismo?

14

Cuando utiliza el concepto de polimorfismo, crea una jerarquía de clases y, utilizando la referencia de los padres, llama a las funciones de la interfaz sin saber qué tipo específico tiene el objeto. Esto es genial. Ejemplo:

Tienes una colección de animales y llamas a todas las funciones de los animales eaty no te importa si es un perro comiendo o un gato. Pero en la misma jerarquía de clases hay animales que tienen otros, además de heredados e implementados de la clase Animal, por ejemplo makeEggs, getBackFromTheFreezedStateetc. Entonces, en algunos casos en su función, es posible que desee conocer el tipo específico para llamar a comportamientos adicionales.

Por ejemplo, en caso de que sea la hora de la mañana y si es solo un animal, llame eat, de lo contrario, si es un humano, llame primero washHands, getDressedy solo luego llame eat. ¿Cómo manejar estos casos? El polimorfismo muere. Debe averiguar el tipo de objeto, que suena como un olor a código. ¿Existe un enfoque común para manejar estos casos?

Narek
fuente
77
El tipo de polimorfismo que describió se llama polimorfismo de subtipo , pero no es el único tipo (ver Polimorfismo ). No es necesario crear una jerarquía de clases para hacer el polimorfismo (y en realidad diría que la herencia no es el método más común para lograr el subtipo de polimorfismo, la implementación de una interfaz es mucho más frecuente).
Vincent Savard
24
Si define una Eaterinterfaz con el eat()método, entonces, como cliente, no le importa que una Humanimplementación tenga que llamar primero washHands()y getDressed()es una implementación de detalles de esta clase. Si, como cliente, le importa este hecho, lo más probable es que no esté utilizando la herramienta correcta para el trabajo.
Vincent Savard
3
También debe tener en cuenta que, si bien en la mañana, un humano podría necesitarlo getDressedantes eat, ese no es el caso para el almuerzo. Dependiendo de sus circunstancias, washHands();if !dressed then getDressed();[code to actually eat]podría ser la mejor manera de implementar esto para un ser humano. Otra posibilidad es ¿qué pasa si otras cosas requieren eso washHandsy / o getDressedse llaman? ¿Y si tienes leaveForWork? Es posible que necesite estructurar el flujo de su programa para tenerlo de modo que se llame mucho antes de eso de todos modos.
Duncan X Simpson
1
Tenga en cuenta que verificar el tipo exacto puede ser un olor a código en OOP, pero es una práctica muy común en FP (es decir, utilice la coincidencia de patrones para determinar el tipo de unión discriminada y luego actuar en consecuencia).
Theodoros Chatzigiannakis
3
Tenga cuidado con los ejemplos de las jerarquías de OO de la escuela como los animales. Los programas reales casi nunca tienen taxonomías tan limpias. Por ejemplo, ericlippert.com/2015/04/27/wizards-and-warriors-part-one . O si desea ir por completo y cuestionar todo el paradigma: la programación orientada a objetos es mala .
jpmc26

Respuestas:

18

Depende Lamentablemente no hay una solución genérica. Piense en sus requisitos y trate de descubrir qué deberían hacer estas cosas.

Por ejemplo, dijiste en la mañana que diferentes animales hacen cosas diferentes. ¿Qué tal se introduce un método getUp()o prepareForDay()o algo por el estilo. Luego puede continuar con el polimorfismo y dejar que cada animal ejecute su rutina matutina.

Si desea diferenciar entre animales, entonces no debe almacenarlos indiscriminadamente en una lista.

Si nada más funciona, entonces puede probar el Patrón de visitante , que es una especie de truco para permitir una especie de despacho dinámico en el que puede enviar un visitante que recibirá devoluciones de llamada exactas de los animales. Sin embargo, destacaría que este debería ser el último recurso si todo lo demás falla.

Robert Bräutigam
fuente
33

Esta es una buena pregunta y es el tipo de problema que mucha gente trata de entender cómo usar OO. Creo que la mayoría de los desarrolladores luchan con esto. Ojalá pudiera decir que la mayoría lo supera, pero no estoy seguro de que sea así. La mayoría de los desarrolladores, en mi experiencia, terminan usando bolsas de propiedades pseudo-OO .

Primero, déjame ser claro. Esto no es tu culpa. La forma en que normalmente se enseña OO es altamente defectuosa. El Animalejemplo es el principal delincuente, IMO. Básicamente, decimos, hablemos de los objetos, qué pueden hacer. Una Animallata eat()y puede speak(). Súper. Ahora crea algunos animales y codifica cómo comen y hablan. Ahora sabes OO, ¿verdad?

El problema es que esto viene en OO desde la dirección equivocada. ¿Por qué hay animales en este programa y por qué necesitan hablar y comer?

Me cuesta pensar en un uso real para un Animaltipo. Estoy seguro de que existe, pero analicemos algo sobre lo que creo que es más fácil razonar: una simulación de tráfico. Supongamos que queremos modelar el tráfico en varios escenarios. Aquí hay algunas cosas básicas que debemos tener para poder hacerlo.

Vehicle
Road
Signal

Podemos profundizar en todo tipo de cosas para peatones y trenes, pero lo mantendremos simple.

Vamos a considerar Vehicle. ¿Qué capacidades necesita el vehículo? Necesita viajar en un camino. Necesita poder detenerse en las señales. Necesita poder navegar en las intersecciones.

interface Vehicle {
  move(Road road);
  navigate(Road... intersection);
}

Probablemente esto sea demasiado simple, pero es un comienzo. Ahora. ¿Qué pasa con todas las otras cosas que podría hacer un vehículo? Pueden salir de una carretera y convertirse en una zanja. ¿Es eso parte de la simulación? No. No lo necesito. Algunos automóviles y autobuses tienen sistemas hidráulicos que les permiten rebotar o arrodillarse, respectivamente. ¿Es eso parte de la simulación? No. No lo necesito. La mayoría de los automóviles queman gasolina. Algunos no. ¿La planta de energía es parte de la simulación? No. No lo necesito. ¿Tamaño de la rueda? No lo necesito ¿Navegacion GPS? Sistema de infoentretenimiento? No los necesito.

Solo necesita definir comportamientos que va a utilizar. Con ese fin, creo que a menudo es mejor construir interfaces OO a partir del código que interactúa con ellas. Comienza con una interfaz vacía y luego comienza a escribir el código que llama a los métodos inexistentes. Así es como sabe qué métodos necesita en su interfaz. Luego, una vez que haya hecho eso, vaya y comience a definir clases que implementen estos comportamientos. Los comportamientos que no se usan son irrelevantes y no necesitan ser definidos.

El objetivo de OO es que puede agregar nuevas implementaciones de estas interfaces más adelante sin cambiar el código de llamada. La única forma en que funciona es si las necesidades del código de llamada determinan lo que pasa en la interfaz. No hay forma de definir todos los comportamientos de todas las cosas posibles que podrían pensarse más adelante.

JimmyJames
fuente
13
Esta es una buena respuesta. "Con ese fin, creo que a menudo es mejor construir interfaces OO a partir del código que interactúa con ellas". Absolutamente, y diría que es la única forma. No se puede conocer el contrato público de una interfaz solo a través de la implementación, siempre se define desde la perspectiva de sus clientes. (Y como nota al margen, de esto se trata TDD en realidad).
Vincent Savard,
@ VincentSavard "Yo diría que es la única manera". Tienes razón. Supongo que la razón por la que no lo hice tan absoluto es que una vez que tienes la idea, puedes desarrollar la interfaz y luego refinarla de esta manera. En última instancia, cuando llegas a las tachuelas de bronce, es lo único que importa.
JimmyJames
@ jpmc26 Tal vez un poco fuerte redactado. No estoy seguro de estar de acuerdo en que es raro implementar esto. No estoy seguro de cómo las interfaces pueden ser útiles si no las está utilizando de esta manera, aparte de las interfaces de marcador, lo que creo que es una idea terrible.
JimmyJames
9

TL; DR:

Piense en una abstracción y métodos que se apliquen a todas las subclases y cubran todo lo que necesita.

Primero quedémonos con su eat()ejemplo.

Es una propiedad de ser humano que, como condición previa para comer, los humanos quieren lavarse las manos y vestirse antes de comer. Si quieres que alguien venga a desayunar contigo, no les digas que se laven las manos y se vistan, lo hacen solos cuando los invitas o responden: "No, no puedo ir. Ya no me lavé las manos y todavía no estoy vestida ".

Volver al software:

Como Humanejemplo no comer sin las condiciones previas, tendría la Human's eat()método de hacer washHands()y getDressed()si eso no se ha hecho. No debería ser su trabajo como eat()interlocutor saber sobre esa peculiaridad. La alternativa del terco humano sería lanzar una excepción ("¡No estoy preparado para comer!") Si no se cumplen las condiciones previas, dejándolo frustrado, pero al menos informado de que comer no funcionó.

¿Qué hay de makeEggs()?

Recomiendo cambiar tu forma de pensar. Probablemente quieras ejecutar las tareas matutinas programadas de todos los seres. Nuevamente, como la persona que llama no debería ser su trabajo saber cuáles son sus deberes. Por lo tanto, recomendaría un doMorningDuties()método que implementen todas las clases.

Ralf Kleberhoff
fuente
Estoy de acuerdo con esta respuesta Narek tiene razón sobre el olor del código. Es el diseño de la interfaz lo que huele mal, así que arregle eso y está bien.
Jonathan van de Veen
Lo que esta respuesta describe generalmente se conoce como el Principio de sustitución de Liskov .
Philipp
2

La respuesta es bastante simple.

¿Cómo manejar objetos que pueden hacer más de lo que esperas?

No necesita manejarlo porque no serviría para nada. Una interfaz generalmente se diseña dependiendo de cómo se vaya a usar. Si su interfaz no define lavarse las manos, entonces no le importa como interlocutor; si lo hubiera hecho, lo habría diseñado de manera diferente.

Por ejemplo, en caso de que sea la hora de la mañana y si se trata solo de un animal, se llama comer, de lo contrario, si es un humano, llame primero a washHands, getDressed y solo luego llame a eat. ¿Cómo manejar estos casos?

Por ejemplo, en pseudocódigo:

interface IEater { void Eat(); }
interface IMorningRoutinePerformer { void DoMorningRoutine(); }
interface IAnimal : IEater, IMorningPerformer;
interface IHuman : IEater, IMorningPerformer; 
{
  void WashHands();
  void GetDressed();
}

void MorningTime()
{
   IList<IMorningRoutinePerformer> items = Service.GetMorningPerformers();
   foreach(item in items) { item.DoMorningRoutine(); }
}

Ahora lo implementa IMorningPerformerpara Animalsimplemente comer, y Humantambién lo implementa para lavarse las manos y vestirse. La persona que llama de su método MorningTime podría importarle menos si es humano o un animal. Todo lo que quiere es la rutina matutina realizada, que cada objeto hace admirablemente gracias a OO.

El polimorfismo muere.

O lo hace?

Necesitas averiguar el tipo de objeto

¿Por qué están asumiendo eso? Creo que esto podría ser una suposición errónea.

¿Existe un enfoque común para manejar estos casos?

Sí, generalmente se resuelve con una clase o jerarquía de interfaz cuidadosamente diseñada. Tenga en cuenta que en el ejemplo anterior no hay nada que contradiga su ejemplo tal como lo ha dado, sin embargo, probablemente se sentirá insatisfecho, porque ha hecho algunas suposiciones más que no escribió en la pregunta al momento de escribir. , y estos supuestos son probablemente violados.

Es posible ir a una madriguera de conejos ajustando sus suposiciones y modificando la respuesta para satisfacerlas, pero no creo que sea útil.

Diseñar buenas jerarquías de clases es difícil y requiere mucha información sobre su dominio comercial. Para dominios complejos, uno pasa por dos, tres o incluso más iteraciones, ya que refinan su comprensión de cómo interactúan las diferentes entidades en su dominio comercial, hasta llegar a un modelo adecuado.

Ahí es donde faltan ejemplos simplistas de animales. Queremos enseñar de manera simple, pero el problema que estamos tratando de resolver no es obvio hasta que profundice, es decir, tenga consideraciones y dominios más complejos.

Andrew Savinykh
fuente