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

14

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

Usted tiene una colección de animales y llama a todas las funciones de animales eat y no le importa si es un perro que come o un gato. Pero en la misma jerarquía de clases tiene animales que tienen adicionales, además de los heredados e implementados de la clase Animal , por ejemplo. makeEggs , getBackFromTheFreezedState y así sucesivamente. Entonces, en algunos casos, en su función, es posible que desee saber el tipo específico para llamar comportamientos adicionales.

Por ejemplo, en caso de que sea tiempo de la mañana y si es solo un animal, entonces llamas a eat ; de lo contrario, si es un ser humano, entonces llama primero a washHands , getDressed y solo luego a 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?

    
pregunta Narek 19.02.2018 - 14:58

4 respuestas

18

Depende. Desafortunadamente no hay una solución genérica. Piense en sus requisitos y trate de averiguar qué deben hacer estas cosas.

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

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

Si nada más funciona, entonces puedes probar el Patrón de visitante , que es < a href="https://lostechies.com/derekgreer/2010/04/19/double-dispatch-is-a-code-smell/"> una especie de truco para permitir una especie de Despacho dinámico donde puede enviar un visitante que recibirá devoluciones de llamada de tipo exacto de animales. Sin embargo, quisiera enfatizar que este debería ser un último recurso si todo lo demás falla.

    
respondido por el Robert Bräutigam 19.02.2018 - 16:03
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. Desearía poder decir que la mayoría lo supera, pero no estoy seguro de que ese sea el caso. La mayoría de los desarrolladores, según mi experiencia, terminan utilizando bolsas de propiedades de pseudo-OO .

Primero, déjame ser claro. Esto no es tu culpa. La forma en que se enseña típicamente OO es altamente defectuosa. El ejemplo de Animal es el principal agresor, IMO. Básicamente, decimos, hablemos de objetos, ¿qué pueden hacer ellos? Un Animal puede 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 incorrecta. ¿Por qué hay animales en este programa y por qué necesitan hablar y comer?

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

Vehicle
Road
Signal

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

Consideremos Vehicle . ¿Qué capacidades necesita el vehículo? Necesita viajar por una carretera. Necesita poder detenerse en las señales. Necesita poder navegar por las intersecciones.

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

Esto es probablemente demasiado simple pero es un comienzo. Ahora. ¿Qué pasa con todas las otras cosas que un vehículo podría hacer? Pueden desviarse de una carretera y convertirse en zanja. ¿Eso es parte de la simulación? No. No lo necesito. Algunos autos y autobuses tienen sistemas hidráulicos que les permiten rebotar o arrodillarse respectivamente. ¿Eso es parte de la simulación? No. No lo necesito. La mayoría de los autos queman gasolina. Algunos no lo hacen ¿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 los comportamientos que va a utilizar. Para ello, creo que a menudo es mejor crear 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 sabes qué métodos necesitas en tu interfaz. Luego, una vez que haya hecho eso, vaya y comience a definir las clases que implementan estos comportamientos. Los comportamientos que no se utilizan son irrelevantes y no es necesario definirlos.

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

    
respondido por el JimmyJames 19.02.2018 - 17:59
9

TL; DR:

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

Quedémonos primero con el ejemplo de eat() .

Es una propiedad de ser un 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 dices que se laven las manos y se visten, lo hacen solos cuando los invitas o responden " No, no puedo venir, no me he lavado las manos y todavía no estoy vestida ".

Volver al software:

Como una instancia Human no se comerá sin las condiciones previas, tendría el método Human 'co_de% do eat() y washHands() si no se hubiera hecho. No debería ser su trabajo como llamador getDressed() saber acerca de esa peculiaridad. La alternativa del terco humano sería lanzar una excepción ("¡No estoy preparada para comer!") Si no se cumplen las condiciones previas, lo que lo deja frustrado, pero al menos informado de que comer no funcionó.

¿Qué pasa con eat() ?

Recomiendo cambiar tu forma de pensar. Probablemente quieras ejecutar los deberes matinales programados de todos los seres. Una vez más, como persona que llama, no debería ser su trabajo saber cuáles son sus funciones. Así que recomiendo un método makeEggs() que implementen todas las clases.

    
respondido por el Ralf Kleberhoff 19.02.2018 - 16:09
2

La respuesta es bastante simple.

  

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

No necesita manejarlo porque no serviría para nada. Una interfaz se diseña generalmente dependiendo de cómo se va a utilizar. Si su interfaz no define el lavado de manos, entonces no le importa como llamador de la interfaz; Si lo hicieras, lo habrías diseñado de manera diferente.

  

Por ejemplo, en caso de que sea tiempo de la mañana y si es solo un animal, entonces llamas a comer; de lo contrario, si es un ser humano, entonces llama primero a WashHands, getDressed y solo luego a Call. ¿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 implementas IMorningPerformer para Animal solo para comer, y para Human también lo implementas para lavarte las manos y vestirte. La persona que llama con su método MorningTime podría importarle menos si es humano o un animal. Todo lo que quiere es realizar la rutina de la mañana, que cada objeto hace admirablemente gracias a OO.

  

Muere el polimorfismo.

¿O no?

  

Debes 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 jerarquía de clase o 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, ya que hizo algunas suposiciones más que no escribió en la pregunta en el momento de escribir. , y estas suposiciones son probablemente violadas.

Es posible ir a un agujero de conejo si ajustas tus suposiciones y yo modificando la respuesta para satisfacerlas, pero no creo que eso sea útil.

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

Ahí es donde faltan ejemplos simplistas de animales. Queremos enseñar de forma sencilla, pero el problema que intentamos resolver no es obvio hasta que profundiza, es decir, tiene consideraciones y dominios más complejos.

    
respondido por el Andrew Savinykh 20.02.2018 - 04:35

Lea otras preguntas en las etiquetas