2010-10-14 4 views
16

AnimalCome evitare grandi se-dichiarazioni e instanceof

public abstract class Animal { 
String name; 

public Animal(String name) { 
    this.name = name; 
} 

} 

Leone

public class Lion extends Animal { 

public Lion(String name) { 
    super(name); 
    // TODO Auto-generated constructor stub 
} 

public void roar() { 
    System.out.println("Roar"); 
} 
} 

Deer

public class Deer extends Animal { 

public Deer(String name) { 
    super(name); 
} 

public void runAway() { 
    System.out.println("Running..."); 
} 

} 

TestAnimals

public class TestAnimals { 
public static void main(String[] args) { 
    Animal lion = new Lion("Geo"); 
    Animal deer1 = new Deer("D1"); 
    Animal deer2 = new Deer("D2"); 

    List<Animal> li = new ArrayList<Animal>(); 
    li.add(lion); 
    li.add(deer1); 
    li.add(deer2); 
    for (Animal a : li) { 
    if (a instanceof Lion) { 
    Lion l = (Lion) a; 
    l.roar(); 
    } 
    if (a instanceof Deer) { 
    Deer l = (Deer) a; 
    l.runAway(); 
    } 

    } 
} 
} 

C'è un modo migliore per scorrere la lista, senza dover lanciare? Nel caso di cui sopra sembra di male, ma se si hanno molte estensioni della classe base, allora avremo bisogno che molti, se blocco troppo. C'è un modello di progettazione o un principio per affrontare questo problema?

+0

I metodi devono avere nomi diversi (ruggire, runAway)? L'idea del polimorfismo sta avendo gli stessi metodi nelle sottoclassi, con la JVM che chiama quella appropriata. – dariopy

+0

Esiste un modo migliore per fare una cosa sbagliata? – InsertNickHere

+1

Qual è l'* intento * del codice in 'TestAnimals'? È "fare la cosa specifica per ogni animale"? O ''ruggisci' tutto' Lion's,' runAway' all 'Deer'' e niente di più? – AakashM

risposta

29

Un modo elegante di evitare instanceof senza inventare qualche nuovo metodo artificiale nella classe base (con un nome non descrittivo come performAction o doWhatYouAreSupposedToDo) è quello di utilizzare il visitor pattern.Ecco un esempio:

Animal

import java.util.*; 

abstract class Animal { 
    String name; 

    public Animal(String name) { 
     this.name = name; 
    } 

    public abstract void accept(AnimalVisitor av); // <-- Open up for visitors. 

} 

Leone e Deer

class Lion extends Animal { 
    public Lion(String name) { 
     super(name); 
    } 
    public void roar() { 
     System.out.println("Roar"); 
    } 

    public void accept(AnimalVisitor av) { 
     av.visit(this);       // <-- Accept and call visit. 
    } 
} 


class Deer extends Animal { 

    public Deer(String name) { 
     super(name); 
    } 

    public void runAway() { 
     System.out.println("Running..."); 
    } 

    public void accept(AnimalVisitor av) { 
     av.visit(this);       // <-- Accept and call visit. 
    } 

} 

Visitor

interface AnimalVisitor { 
    void visit(Lion l); 
    void visit(Deer d); 
} 

class ActionVisitor implements AnimalVisitor { 

    public void visit(Deer d) { 
     d.runAway(); 
    } 

    public void visit(Lion l) { 
     l.roar(); 
    } 
} 

TestAnimals

public class TestAnimals { 
    public static void main(String[] args) { 
     Animal lion = new Lion("Geo"); 
     Animal deer1 = new Deer("D1"); 
     Animal deer2 = new Deer("D2"); 

     List<Animal> li = new ArrayList<Animal>(); 
     li.add(lion); 
     li.add(deer1); 
     li.add(deer2); 
     for (Animal a : li) 
      a.accept(new ActionVisitor());   // <-- Accept/visit. 
    } 
} 
+0

Sì, tendono ad essere d'accordo. Non posso resistere alla votazione del visitatore. Lo adoro, ma penso che tu faccia più di me. +1 –

+0

Riguarda la descrizione che hai aggiunto in seguito, con il visitatore che aggiunge qualsiasi nuovo 'Animale' richiederà la modifica di 'AnimalVisitor'. Considerando che, l'altra opzione è buona per andare senza questo tipo di modifica. Che cosa dici? –

+0

@Adeel Ansari, un buon punto. Dipende dalla situazione che direi. Dipende se vuoi che le specifiche implementazioni di azioni siano raccolte in una classe, o se vuoi che siano sparse nelle diverse classi. Entrambi sono preferibili in diverse situazioni. In questo scenario, tuttavia, 'ruggente' e' runAway' sono così diversi che non vorrei inventare un nome comune per i due. – aioobe

5

Si forniscono un metodo chiamato action() in classe astratta, implementare in entrambi classe figlio, uno ruggirà altro sarà runaway

2

Se il metodo non è polimorfico non si può fare a meno del cast. Per renderlo polimorfico, dichiarare un metodo nella classe base e sovrascriverlo nelle classi discendenti.

12

Animal

public abstract class Animal { 
String name; 

public Animal(String name) { 
    this.name = name; 
} 

public abstract void exhibitNaturalBehaviour(); 

} 

Leone

public class Lion extends Animal { 

public Lion(String name) { 
    super(name); 
} 

public void exhibitNaturalBehaviour() { 
    System.out.println("Roar"); 
} 
} 

Deer

public class Deer extends Animal { 

public Deer(String name) { 
    super(name); 
} 

public void exhibitNaturalBehaviour() { 
    System.out.println("Running..."); 
} 

} 

TestAn imals

public class TestAnimals { 
public static void main(String[] args) { 

    Animal[] animalArr = {new Lion("Geo"), new Deer("D1"), new Deer("D2")}; 
    for (Animal a : animalArr) { 
    a.exhibitNaturalBehaviour();  
    } 

} 
} 
+1

Inoltre, questo potrebbe essere facilmente esteso per utilizzare un modello di strategia. Fornitura dell'azione da eseguire (con un setter separato o in un costruttore) su doWhatYouSupposeToDo(). Anche se potrebbe essere meglio chiamarlo act() o perform() – Steven

+0

@Steven: Tendo ad essere d'accordo, con entrambi i tuoi suggerimenti. Ho appena trovato questo nome arbitrariamente. Sì, sembra stupido e carino; D. –

+3

Nome metodo alternativo proposto: exhibitNaturalBehaviour() – mikera

2

qui si ha una List degli animali. Di solito quando si ha una lista di oggetti, tutti questi oggetti devono essere in grado di fare la stessa cosa senza essere castati.

Così i migliori due soluzioni sono:

  • Avere un metodo comune per le due classi concrete (così definiti come abstract in Animal)
  • separata Lion dal Deer fin dall'inizio, e hanno due liste differenti .
1

Considera l'aggiunta di un'interfaccia per l'azione (Ruggito, Scappa, ecc.) Impostata sull'animale nel costruttore. Quindi hai un metodo astratto come act() nella classe Animal che viene chiamato simile a ciò che ha Adeel.

In questo modo è possibile scambiare azioni per agire attraverso un campo in qualsiasi momento.

2

Il supporto di corrispondenza del modello nella lingua elimina la necessità del brutto modello di visitatore.

Vedi questo codice Scala, ad esempio:

abstract class Animal(name: String) 

class Lion(name: String) extends Animal(name) { 
    def roar() { 
    println("Roar!") 
    } 
} 

class Deer(name: String) extends Animal(name) { 
    def runAway() { 
    println("Running!") 
    } 
} 

object TestAnimals { 
    def main(args: Array[String]) { 
    val animals = List(new Lion("Geo"), new Deer("D1"), new Deer("D2")) 
    for(animal <- animals) animal match { 
     case l: Lion => l.roar() 
     case d: Deer => d.runAway() 
     case _  =>() 
    } 
    } 
} 
+0

Non lo so se funziona ma sembra bello. – Emil

+0

perché entrambi i casi sono "a"? – Emil

+2

@ Emil: Funziona. È testato :) –

1

L'approccio più semplice è quello di avere la classe di super-implementare un comportamento di default.

public enum AnimalBehaviour { 
    Deer { public void runAway() { System.out.println("Running..."); } }, 
    Lion { public void roar() { System.out.println("Roar"); } } 
    public void runAway() { } 
    public void roar() { } 
} 

public class Animal { 
    private final String name; 
    private final AnimalBehaviour behaviour; 
    public Animal(String name, AnimalBehaviour behaviour) { 
     this.name = name; 
     this.behaviour = behaviour; 
    } 
    public void runAway() { behaviour.runAway(); } 
    public void roar() { behaviour.roar(); } 
    } 

public class TestAnimals { 
    public static void main(String... args) { 
    Animal[] animals = { 
     new Animal("Geo", AnimalBehaviour.Lion), 
     new Animal("Bambi", AnimalBehaviour.Deer), 
     new Animal("D2", AnimalBehaviour.Deer) 
    }; 

    for (Animal a : animals) { 
     a.roar(); 
     a.runAway(); 
    } 
    } 
} 
+0

buon approccio. Mi piace. – Emil

+0

L'uso di Enum in questo caso non è molto flessibile, in quanto esisterà solo una singola istanza di comportamento dei cervi. (Il comportamento dei cervi non può quindi, ad esempio, avere un riferimento al relativo oggetto-cervo, poiché il comportamento è condiviso con altre istanze di Cervo.) Inoltre, non è molto elegante poter scrivere myDeer.roar(). – aioobe

+0

@aioobe: si il tuo diritto. Non ci ho pensato molto. – Emil

3

Si scopre che instanceof è più veloce del modello di visitatore presentato sopra; Penso che questo dovrebbe farci dubitare, il modello di visitatore è davvero più elegante di instanceof quando sta facendo la stessa cosa più lentamente con più linee di codice?

Ecco il mio test. Ho confrontato 3 metodi: il modello di visitatore sopra, instanceof e un campo di tipo esplicito in Animal.

OS: Windows 7 Enterprise SP1 a 64 bit
Processore: Intel (R) core (TM) i7 CPU 860 @ 2.80 GHz 2.93 GHz
RAM: 8.00 GB
JRE: 1.7.0_21-B11, a 32 bit

import java.util.ArrayList; 
import java.util.List; 

public class AnimalTest1 { 
    public static void main(String[] args) { 
     Animal lion = new Lion("Geo"); 
     Animal deer1 = new Deer("D1"); 
     Animal deer2 = new Deer("D2"); 

     List<Animal> li = new ArrayList<Animal>(); 
     li.add(lion); 
     li.add(deer1); 
     li.add(deer2); 

     int reps = 10000000; 

     long start, elapsed; 

     start = System.nanoTime(); 
     for (int i = 0; i < reps; i++) { 
      for (Animal a : li) 
       a.accept(new ActionVisitor()); // <-- Accept/visit. 
     } 
     elapsed = System.nanoTime() - start; 

     System.out.println("Visitor took " + elapsed + " ns"); 

     start = System.nanoTime(); 
     for (int i = 0; i < reps; i++) { 
      for (Animal a : li) { 
       if (a instanceof Lion) { 
        ((Lion) a).roar(); 
       } else if (a instanceof Deer) { 
        ((Deer) a).runAway(); 
       } 
      } 
     } 
     elapsed = System.nanoTime() - start; 

     System.out.println("instanceof took " + elapsed + " ns"); 

     start = System.nanoTime(); 
     for (int i = 0; i < reps; i++) { 
      for (Animal a : li) { 
       switch (a.type) { 
       case Animal.LION_TYPE: 
        ((Lion) a).roar(); 
        break; 
       case Animal.DEER_TYPE: 
        ((Deer) a).runAway(); 
        break; 
       } 
      } 
     } 
     elapsed = System.nanoTime() - start; 

     System.out.println("type constant took " + elapsed + " ns"); 
    } 
} 

abstract class Animal { 
    public static final int LION_TYPE = 0; 
    public static final int DEER_TYPE = 1; 

    String name; 
    public final int type; 

    public Animal(String name, int type) { 
     this.name = name; 
     this.type = type; 
    } 

    public abstract void accept(AnimalVisitor av); // <-- Open up for visitors. 
} 

class Lion extends Animal { 
    public Lion(String name) { 
     super(name, LION_TYPE); 
    } 

    public void roar() { 
     // System.out.println("Roar"); 
    } 

    public void accept(AnimalVisitor av) { 
     av.visit(this); // <-- Accept and call visit. 
    } 
} 

class Deer extends Animal { 

    public Deer(String name) { 
     super(name, DEER_TYPE); 
    } 

    public void runAway() { 
     // System.out.println("Running..."); 
    } 

    public void accept(AnimalVisitor av) { 
     av.visit(this); // <-- Accept and call visit. 
    } 

} 

interface AnimalVisitor { 
    void visit(Lion l); 

    void visit(Deer d); 
} 

class ActionVisitor implements AnimalVisitor { 

    public void visit(Deer d) { 
     d.runAway(); 
    } 

    public void visit(Lion l) { 
     l.roar(); 
    } 
} 

I risultati dei test:

Visitor preso 920842192 ns
instanceof preso 511.837.398 ns
tipo costante ha preso 535296640 ns

Questo modello visitatore introduce 2 chiamate di metodo in più che non sono necessari con instanceof. Questo è probabilmente il motivo per cui è più lento.

Non che le prestazioni siano l'unica considerazione, ma si noti come 2 instanceofs sono più veloci persino di un'istruzione switch di 2 casi. Un sacco di persone si sono preoccupate delle prestazioni di instanceof, ma questo dovrebbe far riposare la preoccupazione.

Come sviluppatore Java, mi sento frustrato quando le persone hanno un atteggiamento dogmatico sull'evitare l'uso di instanceof, perché ci sono state diverse volte nel mio lavoro che volevo pulire o scrivere un nuovo codice pulito usando instanceof, ma colleghi/i superiori non approvano questo approccio, perché hanno accettato più o meno ciecamente l'idea che l'instanceof non dovrebbe mai essere usata. Mi sento frustrato perché questo punto è spesso portato a casa con esempi di giocattoli che non riflettono le reali preoccupazioni di business.

Ogni volta che si persegue la progettazione di software modulare, ci saranno sempre momenti in cui le decisioni dipendenti dal tipo devono essere isolate dai tipi in questione, in modo che i tipi abbiano meno dipendenze possibili.

Questo modello di visitatore non interrompe la modularità, ma non è un'alternativa superiore a instanceof.

+0

Penso che sia giusto portarlo in ballo. Tuttavia, nella grande maggioranza dei casi, la facilità di manutenzione supererà di gran lunga il modesto guadagno in termini di prestazioni. Con il modello del visitatore, l'aggiunta di un nuovo tipo di animale esporrà immediatamente tutti i luoghi che necessitano di essere aggiornati, in quanto ora non sarà possibile compilare alcun visitatore esistente in precedenza. Se le altre catene che usano instanceof si compileranno felicemente e c'è una buona possibilità che non scoprirete di aver effettuato il modulo straniero fino a quando non si noterà il comportamento scorretto. – derekv

+0

@derekv Dove pensi che instanceof sia più appropriato? Dì che java.lang.Number era stato scritto con un pattern di visitatore. Vale la pena scrivere un visitatore ogni volta che si desidera verificare il tipo di un'istanza di Numero? Soprattutto se volessi fare qualcosa solo se fosse un doppio, ad esempio? Inoltre, se stai creando una libreria e intendi che gli utenti siano in grado di estendere un tipo nella tua libreria per la quale hai creato un modello di visitatore, gli utenti della tua libreria non sarebbero in grado di aggiungere un metodo per la loro nuova digita nell'interfaccia del visitatore nella tua libreria, a meno che non vogliano biforcare. – Andy

+0

Non penso che esista una soluzione che funzionerà al meglio per ogni situazione, quello che sostengo è il miglior sforzo per sapere quali strumenti avete e prendere una decisione che soddisfi al meglio le esigenze del problema da risolvere, e il rispetto per quelli chi manterrà il codice più tardi. Quasi ovunque ho visto 'instanceof' nel codice dell'applicazione è un odore e non un ciclo critico delle prestazioni o altro. Sono a conoscenza di diversi problemi con il modello di visitatore. Per quanto riguarda la tua domanda di biblioteca, avrei bisogno di vedere alcune specifiche, ma è un punto interessante, potrebbe essere una buona domanda a parte. – derekv