¿Cómo evitar el downcasting?

12

Mi pregunta es sobre un caso especial de la súper clase Animal.

  1. Mi Animallata moveForward()y eat().
  2. Sealse extiende Animal.
  3. Dogse extiende Animal.
  4. Y hay una criatura especial que también se extiende Animalllamada Human.
  5. Humanimplementa también un método speak()(no implementado por Animal).

En una implementación de un método abstracto que acepta, Animalme gustaría usar el speak()método. Eso no parece posible sin hacer un downcast. Jeremy Miller escribió en su artículo que huele mal.

¿Cuál sería una solución para evitar abatir en esta situación?

Bart Weber
fuente
66
Arregla tu modelo. Usar una jerarquía taxonómica existente para modelar una jerarquía de clases suele ser una idea incorrecta. Debe extraer sus abstracciones del código existente, no crear abstracciones y luego tratar de ajustar el código a él.
Eufórico
2
Si desea que los animales hablen, entonces la capacidad de hablar es parte de lo que hace que un animal sea: artima.com/interfacedesign/PreferPoly.html
JeffO
8
moveForward? ¿Qué hay de los cangrejos?
Fabio Marcolini
¿Qué elemento # es ese en Java efectivo, por cierto?
Ricitos
1
Algunas personas no pueden hablar, por lo que se produce una excepción si lo intentas.
Tulains Córdova

Respuestas:

12

Si tiene un método que necesita saber si la clase específica es de tipo Humanpara hacer algo, entonces está rompiendo algunos principios SÓLIDOS , particularmente:

  • Principio abierto / cerrado: si, en el futuro, necesita agregar un nuevo tipo de animal que pueda hablar (por ejemplo, un loro) o hacer algo específico para ese tipo, su código existente tendrá que cambiar
  • Principio de segregación de interfaz: parece que está generalizando demasiado. Un animal puede cubrir un amplio espectro de especies.

En mi opinión, si su método espera un tipo de clase particular, para llamar a su método particular, entonces cambie ese método para aceptar solo esa clase, y no su interfaz.

Algo como esto :

public void MakeItSpeak( Human obj );

y no asi:

public void SpeakIfHuman( Animal obj );
BЈовић
fuente
También se podría crear un método abstracto en Animalllamado canSpeaky cada implementación concreta debe definir si puede o no "hablar".
Brandon
Pero me gusta tu respuesta mejor. Mucha complejidad proviene de tratar de tratar algo como algo que no es.
Brandon
@Brandon, supongo, en ese caso, si no puede "hablar", entonces lanza una excepción. O simplemente no hacer nada.
B 18овић
Entonces te sobrecargas así: public void makeAnimalDoDailyThing(Animal animal) {animal.moveForward(); animal.eat()}ypublic void makeAnimalDoDailyThing(Human human) {human.moveForward(); human.eat(); human.speak();}
Bart Weber
1
Ve todo el camino - makeItSpeak (animal ISpeakingAnimal) - entonces puedes hacer que ISpeakingAnimal implemente Animal. También podría tener makeItSpeak (Animal animal) {hablar si se trata de ISpeakingAnimal}, pero eso tiene un ligero olor.
ptyx
6

El problema no es que estés abatiendo, sino que estás abatiendo Human. En cambio, cree una interfaz:

public interface CanSpeak{
    void speak();
}

public abstract class Animal{
    //....
}

public class Human extends Animal implements CanSpeak{
    public void speak(){
        //....
    }
}

public void mysteriousMethod(Animal animal){
    //....
    if(animal instanceof CanSpeak){
        ((CanSpeak)animal).speak();
    }else{
        //Throw exception or something
    }
    //....
}

De esta manera, la condición no es que el animal lo sea Human, sino que puede hablar. Esto significa que mysteriousMethodpuede funcionar con otras subclases no humanas Animalmientras se implementen CanSpeak.

Idan Arye
fuente
en cuyo caso debería tomar como argumento una instancia de CanSpeak en lugar de Animal
Newtopian
1
@Newtopian Suponiendo que ese método no necesita nada Animal, y que todos los usuarios de ese método mantendrán el objeto que desean enviarle a través de una CanSpeakreferencia de tipo (o incluso Humanreferencia de tipo). Si ese fuera el caso, ese método podría haberse utilizado Humanen primer lugar y no necesitaríamos presentarlo CanSpeak.
Idan Arye
Deshacerse de la interfaz no está vinculado a ser específico en la firma del método, puede deshacerse de la interfaz si y solo si hay solo Humanos y alguna vez habrá solo humanos que "CanSpeak".
Newtopian
1
@Newtopian La razón que presentamos CanSpeaken primer lugar no es que tengamos algo que lo implemente ( Human) sino que tengamos algo que lo use (el método). El punto CanSpeakes desacoplar ese método de la clase concreta Human. Si no tuviéramos métodos para tratar las cosas de manera CanSpeakdiferente, no tendría sentido distinguir las cosas CanSpeak. No creamos una interfaz solo porque tenemos un método ...
Idan Arye
2

Puede agregar Comunicar a Animal. El perro ladra, el humano habla, Seal ... uhh ... No sé qué hace Seal.

Pero parece que su método está diseñado para si (Animal is Human) Speak ();

La pregunta que puede hacer es, ¿cuál es la alternativa? Es difícil dar una sugerencia ya que no sé exactamente lo que quieres lograr. Hay situaciones teóricas en las que el mejor enfoque es el downcasting / upcasting.

Andrew Hoffman
fuente
15
El sello se va muy rápido , pero el zorro no está definido.
2

En este caso, la implementación predeterminada de speak()en la AbstractAnimalclase sería:

void speak() throws CantSpeakException {
  throw new CantSpeakException();
}

En ese momento, tiene una implementación predeterminada en la clase Resumen, y se comporta correctamente.

try {
  thingy.speak();
} catch (CantSeakException e) {
  System.out.println("You can't talk to the " + thingy.name());
}

Sí, esto significa que tiene capturas de prueba dispersas a través del código para manejar cada una speak, pero la alternativa a esto es if(thingy is Human)envolver todos los discursos en su lugar.

La ventaja de la excepción es que si tiene otro tipo de cosas en algún momento que habla (un loro), no necesitará volver a implementar todas sus pruebas.


fuente
1
No creo que esta sea una buena razón para usar una excepción (a menos que sea un código de Python).
Bryan Chen
1
No rechazaré el voto porque esto técnicamente funcionaría, pero realmente no me gusta este tipo de diseño. ¿Por qué definir un método a nivel primario que el primario no puede implementar? A menos que sepa de qué tipo es el objeto (y si lo hiciera, no tendría este problema), siempre debe usar un try / catch para verificar si hay una excepción completamente evitable. Incluso podría usar un canSpeak()método para manejar esto mejor.
Brandon
2
He trabajado en un proyecto que tenía un montón de defectos originados por la decisión de alguien de reescribir un montón de métodos de trabajo para lanzar una excepción porque están usando una implementación obsoleta. Pudo haber arreglado la implementación, pero eligió lanzar excepciones en todas partes. Naturalmente, ingresamos al control de calidad con cientos de errores. Por lo tanto, estoy predispuesto a lanzar excepciones en métodos que no deben llamarse. Si se supone que no deben llamarse, elimínelos .
Brandon
2
La alternativa a lanzar una excepción en este caso puede ser no hacer nada. Después de todo, no pueden hablar :)
BЈовић
@Brandon hay una diferencia entre diseñarlo desde el principio con la excepción frente a adaptarlo más tarde. También se podría mirar una interfaz con un método predeterminado en Java8, o no lanzar una excepción y simplemente estar en silencio. Sin embargo, el punto clave es que para evitar la necesidad de downcast, la función debe definirse en el tipo que se pasa.
1

La abatimiento es a veces necesaria y apropiada. En particular, a menudo es apropiado en los casos en que uno tiene objetos que pueden o no tener alguna habilidad, y uno desea usar esa habilidad cuando existe mientras maneja objetos sin esa habilidad de alguna manera predeterminada. Como ejemplo simple, supongamos que Stringse le pregunta a a si es igual a algún otro objeto arbitrario. Para que uno sea Stringigual a otro String, debe examinar la longitud y la matriz de caracteres de respaldo de la otra cadena. Si una Stringse le pregunta si es igual a un Dogembargo, no se puede acceder a la longitud de la Dog, pero no tiene por qué; en cambio, si el objeto con el que Stringse supone que se compara no es unString, la comparación debe usar un comportamiento predeterminado (informar que el otro objeto no es igual).

El momento en que se debe considerar que la abatición es más dudosa es cuando se "sabe" que el objeto que se está lanzando es del tipo adecuado. En general, si se sabe que un objeto es a Cat, se debe usar una variable de tipo Cat, en lugar de una variable de tipo Animal, para referirse a él. Sin embargo, hay momentos en que esto no siempre funciona. Por ejemplo, una Zoocolección podría contener pares de objetos en ranuras de matriz pares / impares, con la expectativa de que los objetos de cada par puedan actuar unos sobre otros, incluso si no pueden actuar sobre los objetos de otros pares. En tal caso, los objetos en cada par aún tendrían que aceptar un tipo de parámetro no específico de modo que, sintácticamente , pudieran pasar los objetos desde cualquier otro par. Por lo tanto, incluso si Cat'splayWith(Animal other)El método solo funcionaría cuando otherfuera un Cat, el Zootendría que poder pasarle un elemento de un Animal[], por lo que su tipo de parámetro tendría que ser en Animallugar de Cat.

En los casos en que la derrota sea legítimamente inevitable, uno debería usarla sin reparos. La pregunta clave es determinar cuándo se puede evitar sensacionalmente el rechazo y evitarlo cuando sea razonablemente posible.

Super gato
fuente
En el caso de la cadena, debería tener un método Object.equalToString(String string). Entonces tiene boolean String.equal(Object object) { return object.equalStoString(this); }So, no necesita downcast: puede usar el despacho dinámico.
Giorgio
@Giorgio: el despacho dinámico tiene sus usos, pero en general es incluso peor que el downcasting.
supercat
¿Por qué el despacho dinámico generalmente es peor que el downcasting? pensé que era al revés
Bryan Chen
@BryanChen: Eso depende de su terminología. No creo que Objecttenga ningún equalStoStringmétodo virtual, y admito que no sé cómo funcionaría el ejemplo citado en Java, pero en C #, el despacho dinámico (a diferencia del despacho virtual) significaría que el compilador esencialmente tiene hacer una búsqueda de nombres basada en Reflection la primera vez que se usa un método en una clase, que es diferente del despacho virtual (que simplemente realiza una llamada a través de un espacio en la tabla de métodos virtuales que se requiere para contener una dirección de método válida).
supercat
Desde el punto de vista del modelado, preferiría el despacho dinámico en general. También es la forma orientada a objetos de seleccionar un procedimiento basado en el tipo de sus parámetros de entrada.
Giorgio
1

En una implementación de un método abstracto que acepta animales, me gustaría usar el método speak ().

Tienes algunas opciones:

  • Use la reflexión para llamar speaksi existe. Ventaja: no depende de Human. Desventaja: ahora hay una dependencia oculta en el nombre "hablar".

  • Introducir una nueva interfaz Speakery bajar a la interfaz. Esto es más flexible que dependiendo de un tipo de concreto específico. Tiene la desventaja de que tiene que modificar Humanpara implementar Speaker. Esto no funcionará si no puedes modificarHuman

  • Abatido a Human. Esto tiene la desventaja de que tendrá que modificar el código cada vez que desee que hable otra subclase. Idealmente, desea extender las aplicaciones agregando código sin tener que retroceder repetidamente y cambiar el código antiguo.

Kevin Cline
fuente