Come evitare il downcasting?


12

La mia domanda riguarda un caso speciale dell'Animale super class.

  1. La mia Animallattina moveForward()e eat().
  2. Sealsi estende Animal.
  3. Dogsi estende Animal.
  4. E c'è una creatura speciale che si estende anche Animalchiamata Human.
  5. Humanimplementa anche un metodo speak()(non implementato da Animal).

In un'implementazione di un metodo astratto che accetta Animalvorrei usare il speak()metodo. Ciò non sembra possibile senza fare un downcast. Jeremy Miller ha scritto nel suo articolo che un downcast puzza.

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


6
Correggi il tuo modello. L'uso della gerarchia tassonomica esistente per modellare una gerarchia di classi è generalmente un'idea sbagliata. Dovresti estrarre le tue astrazioni dal codice esistente, non creare astrazioni e quindi provare ad adattare il codice ad esso.
Euforico,

2
Se vuoi che Animali parli, allora la capacità di parlare fa parte di ciò che rende qualcosa un animale: artima.com/interfacedesign/PreferPoly.html
JeffO

8
moveForward? che dire di granchi?
Fabio Marcolini,

Quale articolo # è quello in Effective Java, BTW?
Riccioli d'oro,

1
Alcune persone non possono parlare, quindi viene generata un'eccezione se ci provi.
Tulains Córdova,

Risposte:


12

Se hai un metodo che deve sapere se la classe specifica è di tipo Humanper fare qualcosa, allora stai infrangendo alcuni principi SOLID , in particolare:

  • Principio aperto / chiuso: se, in futuro, è necessario aggiungere un nuovo tipo di animale in grado di 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 prevede un particolare tipo di classe, chiamarlo è un metodo particolare, quindi cambia quel metodo per accettare solo quella classe e non la sua interfaccia.

Qualcosa come questo :

public void MakeItSpeak( Human obj );

e non così:

public void SpeakIfHuman( Animal obj );

Si potrebbe anche creare un metodo astratto su Animalchiamato canSpeake ogni implementazione concreta deve definire se può "parlare" o meno.
Brandon,

Ma mi piace di più la tua risposta. Molta complessità deriva dal cercare di trattare qualcosa come qualcosa che non lo è.
Brandon,

@Brandon Immagino, in tal caso, se non riesce a "parlare", allora lancia un'eccezione. O semplicemente non fare niente.
BЈовић,

Quindi ti sovraccarichi in questo modo: public void makeAnimalDoDailyThing(Animal animal) {animal.moveForward(); animal.eat()}epublic void makeAnimalDoDailyThing(Human human) {human.moveForward(); human.eat(); human.speak();}
Bart Weber il

1
Vai fino in fondo - makeItSpeak (ISpeakingAnimal animal) - puoi quindi avere ISpeakingAnimal implementa Animal. Potresti anche avere makeItSpeak (Animal animal) {parlare se istanza di ISpeakingAnimal}, ma ha un leggero odore.
ptyx,

6

Il problema non è che stai effettuando il downcasting, ma che stai eseguendo il downcasting 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. Questo significa che mysteriousMethodpuò funzionare con altre sottoclassi non umane Animalpurché implementate CanSpeak.


nel qual caso dovrebbe prendere come argomento un'istanza di CanSpeak anziché Animal
Newtopian,

1
@Newtopian Supponendo che quel metodo non abbia bisogno di nulla Animale che tutti gli utenti di quel metodo manterranno l'oggetto che vogliono inviargli tramite un CanSpeakriferimento di tipo (o anche un Humanriferimento di tipo). In tal caso, quel metodo avrebbe potuto essere utilizzato Humanin primo luogo e non avremmo bisogno di introdurre CanSpeak.
Idan Arye,

Sbarazzarsi dell'interfaccia non è legato all'essere specifico nella firma del metodo, è possibile sbarazzarsi dell'interfaccia se e solo se ci sono solo Umani e saranno mai solo umani che "CanSpeak".
Newtopian,

1
@Newtopian Il motivo che abbiamo introdotto CanSpeakin primo luogo non è che abbiamo qualcosa che lo implementa ( Human) ma che abbiamo qualcosa che lo utilizza (il metodo). Il punto CanSpeakè di separare quel metodo dalla classe concreta Human. Se non avessimo metodi per trattare le cose in modo CanSpeakdiverso, non avrebbe senso distinguere le cose CanSpeak. Non creiamo un'interfaccia solo perché abbiamo un metodo ...
Idan Arye,

2

È possibile aggiungere Communicate to Animal. Il cane abbaia, l'umano parla, il sigillo ... uhh ... non so cosa faccia il sigillo.

Ma sembra che il tuo metodo sia progettato per if (Animal is Human) Speak ();

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


15
Il sigillo deve ow ow , ma la volpe non è definita.

2

In questo caso, l'implementazione predefinita di speak()inAbstractAnimal classe 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 i tentativi catturati attraverso il codice per gestire tutti speak , ma l'alternativa a questo è invece if(thingy is Human)racchiudere tutti i discorsi.

Il vantaggio dell'eccezione è che se ad un certo punto hai un altro tipo di cose che parlano (un pappagallo) non dovrai reimplementare tutti i tuoi test.


1
Non penso che questo sia un buon motivo per usare l'eccezione (a meno che non si tratti di codice Python).
Bryan Chen,

1
Non voterò perché questo funzionerebbe tecnicamente, ma non mi piace davvero questo tipo di design. Perché definire un metodo a livello padre che il padre non può implementare? A meno che tu non sappia di che tipo è l'oggetto (e se lo facessi - non avresti questo problema), devi sempre usare un tentativo / cattura per verificare un'eccezione completamente evitabile. Puoi persino usare un canSpeak()metodo per gestirlo meglio.
Brandon,

2
Ho lavorato su un progetto che aveva molti difetti derivanti dalla decisione di qualcuno di riscrivere un mucchio di metodi di lavoro per lanciare un'eccezione perché stanno usando un'implementazione obsoleta. Avrebbe potuto sistemare l'implementazione, ma preferì invece lanciare eccezioni ovunque. Naturalmente, abbiamo inserito il QA con centinaia di bug. Quindi sono di parte contro il lancio di eccezioni in metodi che non dovrebbero essere chiamati. Se non devono essere chiamati, eliminali .
Brandon,

2
Alternativa al lancio di un'eccezione in questo caso potrebbe non fare nulla. Dopo tutto, non possono parlare :)
BЈовић,

@Brandon c'è una differenza tra la progettazione dall'inizio con l'eccezione e la modifica successiva. Si potrebbe anche guardare un'interfaccia con un metodo predefinito in Java8, o non lanciare un'eccezione ed essere semplicemente in silenzio. Il punto chiave è che per evitare di dover eseguire il downcast, la funzione deve essere definita nel tipo che viene passato.

1

Il downcasting è talvolta 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 qualche modo predefinito. Come semplice esempio, supponiamo che a Stringsia chiesto se è uguale a qualche altro oggetto arbitrario. Affinché uno Stringsia uguale all'altro String, deve esaminare la lunghezza e l'array di caratteri di supporto dell'altra stringa. Se a Stringviene chiesto se è uguale a a , il confronto dovrebbe usare un comportamento predefinito (riportando che l'altro oggetto non è uguale).Dog , tuttavia, non può accedere alla lunghezza di Dog, ma non dovrebbe; invece, se l'oggetto con cui Stringsi suppone che a si paragoni non è aString

Il momento in cui il downcasting dovrebbe essere considerato il più dubbio è quando l'oggetto che viene lanciato è "conosciuto" per essere del tipo giusto. 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 fare riferimento ad esso. Ci sono momenti in cui questo non sempre funziona, tuttavia. Ad esempio, una Zooraccolta potrebbe contenere coppie di oggetti in slot di matrice pari / dispari, con l'aspettativa che gli oggetti in ciascuna coppia possano agire l'uno sull'altro, anche se non possono agire sugli oggetti in altre coppie. In tal caso, gli oggetti in ciascuna coppia dovrebbero comunque accettare un tipo di parametro non specifico in modo tale che possano, sintatticamente , passare gli oggetti da qualsiasi altra coppia. Quindi, anche se Cat'splayWith(Animal other)metodo avrebbe funzionato solo quando otherera a Cat, Zooavrebbe dovuto essere in grado di passargli un elemento di un Animal[], quindi il suo tipo di parametro avrebbe dovuto essere Animalpiuttosto che Cat.

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


Nel caso di stringhe, dovresti piuttosto avere un metodo Object.equalToString(String string). Quindi hai boolean String.equal(Object object) { return object.equalStoString(this); }So, non è necessario alcun downcast: puoi utilizzare il dispacciamento dinamico.
Giorgio,

@Giorgio: il dispacciamento dinamico ha i suoi usi, ma è generalmente anche peggio del downcasting.
supercat,

il modo in cui il dispacciamento dinamico è generalmente peggiore del downcasting? pensavo che fosse il contrario
Bryan Chen,

@BryanChen: dipende dalla tua terminologia. Non credo Objectabbia alcun equalStoStringmetodo virtuale, e ammetto di non sapere come funzionerebbe l'esempio citato in Java, ma in C #, l'invio dinamico (come distinto dall'invio virtuale) significherebbe che il compilatore ha essenzialmente per eseguire la ricerca dei nomi basata su Reflection la prima volta che viene utilizzato un metodo su una classe, che è distinto dal dispacciamento virtuale (che effettua semplicemente una chiamata tramite uno slot nella tabella dei metodi virtuali che deve contenere un indirizzo di metodo valido).
supercat

Da un punto di vista della modellazione, preferirei il dispacciamento dinamico in generale. È anche il modo orientato agli oggetti di selezionare una procedura in base al tipo dei suoi parametri di input.
Giorgio,

1

In un'implementazione di un metodo astratto che accetta Animali, vorrei usare il metodo speak ().

Hai alcune scelte:

  • Usa la riflessione per chiamare speakse esiste. Vantaggio: nessuna dipendenza da Human. Svantaggio: ora esiste una dipendenza nascosta dal nome "speak".

  • Introdurre una nuova interfaccia Speakere downcast per l'interfaccia. Questo è più flessibile che a seconda di un tipo specifico di calcestruzzo. Ha lo svantaggio che devi modificare Humanper implementare Speaker. Questo non funzionerà se non puoi modificarloHuman

  • Abbattuto a Human. Questo ha lo svantaggio che dovrai modificare il codice ogni volta che vuoi parlare un'altra sottoclasse. Idealmente, si desidera estendere le applicazioni aggiungendo codice senza tornare ripetutamente e modificare il vecchio codice.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.