Come evitare il downcasting?

10

La mia domanda riguarda un caso speciale della super classe Animal.

  1. Il mio Animal può moveForward() e eat() .
  2. Seal estende Animal .
  3. Dog estende Animal .
  4. E c'è una creatura speciale che estende anche Animal chiamata Human .
  5. Human implementa anche un metodo speak() (non implementato da Animal ).

In un'implementazione di un metodo astratto che accetta Animal vorrei utilizzare il metodo speak() . Ciò non sembra possibile senza un downcast. Jeremy Miller ha scritto in suo articolo che ha un odore di abbattimento.

Quale sarebbe una soluzione per evitare il downcasting in questa situazione?

    
posta Bart Weber 17.06.2014 - 17:34
fonte

6 risposte

11

Se hai un metodo che ha bisogno di sapere se la classe specifica è di tipo Human per fare qualcosa, allora stai rompendo alcuni Principi SOLID , in particolare:

  • Principio aperto / chiuso - se, in futuro, devi aggiungere un nuovo tipo di animale che può parlare (ad esempio un pappagallo) o fare qualcosa di specifico per quel tipo, il codice esistente dovrà cambiare
  • Principio di segregazione dell'interfaccia: sembra che tu stia generalizzando troppo. Un animale può coprire un ampio spettro di specie.

Secondo me, se il tuo metodo si aspetta un particolare tipo di classe, chiamare il suo metodo particolare, quindi cambiare quel metodo per accettare solo quella classe, e non la sua interfaccia.

Qualcosa del genere:

public void MakeItSpeak( Human obj );

e non in questo modo:

public void SpeakIfHuman( Animal obj );
    
risposta data 17.06.2014 - 17:58
fonte
5

Il problema non è il downcasting: è il downcasting a Human . Invece, crea un'interfaccia:

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

In questo modo, la condizione non è che l'animale sia Human - la condizione è che possa parlare. Ciò significa che mysteriousMethod può funzionare con altre sottoclassi non umane di Animal se implementano CanSpeak .

    
risposta data 18.06.2014 - 13:06
fonte
2

In questo caso, l'implementazione predefinita di speak() nella classe AbstractAnimal sarebbe:

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

A quel punto, hai un'implementazione predefinita nella classe Abstract e si comporta correttamente.

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

Sì, questo significa che hai ottenuto dei try-catches sparsi nel codice per gestire ogni speak , ma l'alternativa a questo è if(thingy is Human) che include invece tutti i talk.

Il vantaggio dell'eccezione è che se hai un altro tipo di cosa a un certo punto che parla (un pappagallo) non avrai bisogno di reimplementare tutti i tuoi test.

    
risposta data 17.06.2014 - 18:33
fonte
1

Potresti aggiungere Communicate to Animal. Cane abbaia, Umano parla, Sigilla .. uhh .. Non so che sigillo faccia.

Ma sembra che il tuo metodo sia progettato per     se (l'animale è umano)       Parlare ();

La domanda che potresti volere è, qual è l'alternativa? È difficile dare un suggerimento poiché non so esattamente cosa vuoi ottenere. Ci sono situazioni teoriche in cui downcasting / upcasting è l'approccio migliore.

    
risposta data 17.06.2014 - 18:04
fonte
1

Il downcasting a volte è necessario e appropriato. In particolare, è spesso appropriato nei casi in cui si hanno oggetti che possono o meno avere qualche abilità, e si desidera usare quell'abilità quando esiste mentre si maneggiano oggetti senza quell'abilità in un modo predefinito. Come semplice esempio, supponiamo che venga chiesto a String se è uguale ad altri oggetti arbitrari. Per un String per uguagliare un altro String , deve esaminare la lunghezza e il backing array dei caratteri dell'altra stringa. Se viene chiesto a String se è uguale a Dog , tuttavia, non può accedere alla lunghezza di Dog , ma non dovrebbe farlo; invece, se l'oggetto a cui un String deve confrontare se stesso non è un String , il confronto dovrebbe utilizzare un comportamento predefinito (segnalando che l'altro oggetto non è uguale).

Il momento in cui il downcasting dovrebbe essere considerato il più discutibile è quando l'oggetto che viene lanciato è "noto" per essere del tipo corretto. In generale, se un oggetto è noto per essere un Cat , si dovrebbe usare una variabile di tipo Cat , piuttosto che una variabile di tipo Animal , per riferirsi ad esso. Ci sono volte in cui questo non funziona sempre, comunque. Ad esempio, una collezione Zoo può contenere coppie di oggetti in slot di array pari / dispari, con l'aspettativa che gli oggetti di ciascuna coppia siano in grado di agire l'uno sull'altro, anche se non possono agire sugli oggetti in altre coppie. In tal caso, gli oggetti di ciascuna coppia dovrebbero ancora accettare un tipo di parametro non specifico in modo che possano, sintatticamente , essere passati gli oggetti da qualsiasi altra coppia. Pertanto, anche se il metodo Cat playWith(Animal other) funzionasse solo quando other era un Cat , il Zoo avrebbe dovuto essere in grado di passarlo un elemento di un Animal[] , quindi il suo tipo di parametro dovrebbe essere Animal anziché Cat .

Nei casi in cui il downcasting è legittimamente inevitabile, si dovrebbe usarlo senza scrupoli. La domanda chiave è determinare quando è possibile evitare sensibilmente il downcasting ed evitarlo quando è sensibilmente possibile.

    
risposta data 17.06.2014 - 18:10
fonte
1

In an implementation of an abstract method which accepts Animals I would like to use the speak() method.

Hai alcune scelte:

  • Utilizza reflection per chiamare speak se esiste. Vantaggio: nessuna dipendenza su Human . Svantaggio: ora c'è una dipendenza nascosta dal nome "parla".

  • Introduci una nuova interfaccia Speaker e downcast all'interfaccia. Questo è più flessibile di quello che dipende da un tipo concreto specifico. Ha lo svantaggio che devi modificare Human per implementare Speaker . Questo non funzionerà se non puoi modificare Human

  • Downcast a Human . Questo ha lo svantaggio che dovrai modificare il codice ogni volta che vuoi far parlare un'altra sottoclasse. Idealmente, vuoi estendere le applicazioni aggiungendo il codice senza dover tornare indietro e cambiare il vecchio codice.

risposta data 18.06.2014 - 22:22
fonte

Leggi altre domande sui tag