2013-02-13 14 views
13

Si consideri il seguente enums, che è meglio? Entrambi possono essere utilizzati esattamente allo stesso modo, ma quali sono i vantaggi l'uno rispetto all'altro?Sovrascrivere il metodo astratto o utilizzare un singolo metodo in enumerazione?

1. Esclusione metodo astratto:

public enum Direction { 
    UP { 
     @Override 
     public Direction getOppposite() { 
      return DOWN; 
     } 
     @Override 
     public Direction getRotateClockwise() { 
      return RIGHT; 
     } 
     @Override 
     public Direction getRotateAnticlockwise() { 
      return LEFT; 
     } 
    }, 
    /* DOWN, LEFT and RIGHT skipped */ 
    ; 
    public abstract Direction getOppposite(); 
    public abstract Direction getRotateClockwise(); 
    public abstract Direction getRotateAnticlockwise(); 
} 

2. Utilizzando un unico metodo:

public enum Orientation { 
    UP, DOWN, LEFT, RIGHT; 
    public Orientation getOppposite() { 
     switch (this) { 
     case UP: 
      return DOWN; 
     case DOWN: 
      return UP; 
     case LEFT: 
      return RIGHT; 
     case RIGHT: 
      return LEFT; 
     default: 
      return null; 
     } 
    } 
    /* getRotateClockwise and getRotateAnticlockwise skipped */ 
} 

Edit: Spero davvero di vedere alcuni ben motivata/risposte elaborate, con evidenze/fonti a rivendicazioni particolari. La maggior parte delle risposte esistenti riguardo alle prestazioni non è davvero convincente a causa della mancanza di prove.

È possibile può suggerire alternative, ma deve essere chiaro come è meglio di quelli dichiarati e/o come quelli dichiarati è peggio e fornire le prove quando necessario.

+3

Una terza opzione potrebbe essere quella di includere argomenti come 'opposite',' 'cwise' e ​​ccwise' al costruttore di' Direction', assegnarli alle variabili di istanza finali e con quelli (sia attraverso diretta accesso o getter definiti sulla 'classe'). – akaIDIOT

+0

Non mi piace neanche, perché non mi piace cercare la logica all'interno delle enumerazioni. Tuttavia il secondo è più conciso e non implica l'annullamento di un metodo astratto da parte di un membro enum, che è un po 'doloroso per i miei occhi :) – gd1

+0

@ gd1 Se nessuna logica dentro enum, come implementereste qualcosa come il mio esempio? Se hai un'alternativa puoi suggerire una risposta. –

risposta

20

Dimentica le prestazioni in questo confronto; ci vorrebbe un enorme enum perché ci sia una significativa differenza di prestazioni tra le due metodologie.

Concentriamoci invece sulla manutenibilità. Supponiamo di aver terminato di codificare l'enumerazione Direction e di passare infine a un progetto più prestigioso. Nel frattempo, a un altro sviluppatore viene assegnata la proprietà del vecchio codice incluso Direction - chiamiamolo Jimmy.

A un certo punto, i requisiti impongono a Jimmy di aggiungere due nuove direzioni: FORWARD e BACKWARD. Jimmy è stanco e oberato di lavoro e non si preoccupa di indagare a fondo su come questo potrebbe influire sulla funzionalità esistente - lo fa semplicemente. Vediamo cosa succede ora:

1. Esclusione metodo astratto:

Jimmy ottiene immediatamente un errore di compilazione (in realtà probabilmente avrebbe mai individuato il metodo prevale proprio sotto le enum dichiarazioni di costanti). In ogni caso, il problema viene individuato e risolto al tempo di compilazione.

2. Utilizzando un unico metodo:

Jimmy non ottiene un errore di compilazione, o anche un interruttore incompleta avvertimento dal suo IDE, dal momento che il switch ha già un caso default. Successivamente, durante il runtime, un certo numero di codice chiama FORWARD.getOpposite(), che restituisce null.Ciò causa un comportamento imprevisto e nel migliore dei casi causa il lancio immediato di NullPointerException.

Torniamo indietro e far finta si è aggiunto un po 'a prova di futuro, invece:

default: 
    throw new UnsupportedOperationException("Unexpected Direction!"); 

Anche allora il problema non sarebbe scoperto fino runtime. Speriamo che il progetto sia stato testato correttamente!

Ora, l'esempio Direction è piuttosto semplice, quindi questo scenario potrebbe sembrare esagerato. In pratica, tuttavia, le enumerazioni possono diventare un problema di manutenzione tanto facilmente quanto altre classi. In una base di codice più grande e datata con più resilienza degli sviluppatori al refactoring è una preoccupazione legittima. Molte persone parlano dell'ottimizzazione del codice, ma possono dimenticare che anche il tempo di sviluppo deve essere ottimizzato e che include la codifica per evitare errori.

Edit: un biglietto sotto JLS Example §8.9.2-4 sembra essere d'accordo:

corpi classe

Costante-specifici attribuiscono comportamenti alle costanti. [Questo] schema è molto più sicuro dell'uso di un'istruzione switch nel tipo di base ... poiché il modello preclude la possibilità di dimenticare di aggiungere un comportamento per una nuova costante (poiché la dichiarazione enum causerebbe un errore in fase di compilazione).

+3

Non ho davvero pensato a questo fattore prima di menzionarlo! Hai ragione, perché il mio esempio è * veramente * pensato per essere un semplice esempio, ma in pratica questo è davvero un fattore abbastanza importante. (Lasciatemi aspettare ancora qualche giorno per possibilmente più risposte prima di accettare e assegnare taglie ...) –

+0

Contento di aver potuto aiutare e grazie per l'interessante post. Sentiti libero di lasciare la finitura di taglie nel caso in cui altri vogliano pesare. –

1

La prima variante è più veloce ed è probabilmente più manutenibile, poiché tutte le proprietà della direzione sono descritte dove è definita la direzione stessa. Tuttavia, mettere la logica non banale in enigmi mi sembra strano.

0

I valori di enumerazione possono essere considerati come classi indipendenti. Pertanto, considerando i concetti orientati agli oggetti, ogni enumerazione dovrebbe definire il proprio comportamento. Quindi consiglierei il primo approccio.

0

La prima versione è probabilmente molto più veloce. Il compilatore JIT Java può applicare ottimizzazioni aggressive perché enum s sono finali (quindi tutti i metodi in essi sono final). Il codice:

Orientation o = Orientation.UP.getOppposite(); 

dovrebbe effettivamente diventare (a runtime):

Orientation o = Orientation.DOWN; 

cioè il compilatore può rimuovere l'overhead per la chiamata di metodo.

Dal punto di vista del design, è il modo corretto di fare queste cose con OO: spostare la conoscenza vicino all'oggetto che ne ha bisogno. Quindi UP dovrebbe sapere che è opposto, non un codice altrove.

Il vantaggio del secondo metodo è che è più leggibile poiché tutte le cose correlate sono raggruppate meglio (cioè tutto il codice relativo a "opposto" è in un posto invece che un po qui e un po 'lì).

EDIT Il mio primo argomento dipende da quanto sia intelligente il compilatore JIT. La mia soluzione per il problema sarebbe simile a questa:

public enum Orientation { 
    UP, DOWN, LEFT, RIGHT; 

    private static Orientation[] opposites = { 
     DOWN, UP, RIGHT, LEFT 
    }; 

    public Orientation getOpposite() { 
     return opposites[ ordinal() ]; 
    } 
} 

Questo codice è compatto e veloce, non importa quale sia il JIT può o potrebbe fare. Comunica chiaramente l'intento e, date le regole degli ordinali, funzionerà sempre.

Vorrei anche suggerire di aggiungere un test che fa in modo che quando si chiama getOpposite() per ogni valore della enum, si ottiene sempre un risultato diverso e nessuno dei risultati è null.In questo modo, puoi essere sicuro di avere ogni caso.

L'unico problema rimasto è quando si modifica l'ordine dei valori. Per evitare problemi in questo caso, assegnare a ciascun valore un indice e utilizzarlo per cercare valori in un array o anche in Orientation.values().

qui è un altro modo per farlo:

public enum Orientation { 
    UP(1), DOWN(0), LEFT(3), RIGHT(2); 

    private int opposite; 

    private Orientation(int opposite) { 
     this.opposite = opposite; 
    } 

    public Orientation getOpposite() { 
     return values()[ opposite ]; 
    } 
} 

Non mi piace questo approccio, però. È troppo difficile da leggere (devi contare l'indice di ciascun valore nella tua testa) e troppo facile da sbagliare. Avrebbe bisogno di un test unitario per valore nell'enum e per metodo che è possibile chiamare (quindi 4 * 3 = 12 nel tuo caso).

+0

La prima versione non sarà più veloce, anzi se sarà più lenta, vedi la mia risposta per i dettagli. – Recurse

+1

@Recurse: questa ipotesi è errata, come ho spiegato nel mio commento alla tua risposta. –

+0

@AaronDigulla Che ne dici se 'o' non è noto al momento della compilazione? Ad esempio essere passati a una funzione? –

0

Si potrebbe anche simpy implementare una volta come questo (è necessario mantenere le costanti enum nell'ordine appropriato):

public enum Orientation { 

    UP, RIGHT, DOWN, LEFT; //Order is important: must be clock-wise 

    public Orientation getOppposite() { 
     int position = ordinal() + 2; 
     return values()[position % 4]; 
    } 
    public Orientation getRotateClockwise() { 
     int position = ordinal() + 1; 
     return values()[position % 4]; 
    } 
    public Orientation getRotateAnticlockwise() { 
     int position = ordinal() + 3; //Not -1 to avoid negative position 
     return values()[position % 4]; 
    } 
} 
+1

+1 per il bel trucco, -1 per la leggibilità :-) –

+0

@AaronDigulla Giusto abbastanza :-) – assylias

+0

@AaronDigulla In realtà lo trovo abbastanza leggibile. – assylias

1

La seconda variante sarà probabilmente un po 'più veloce come il> 2-ary il polimorfismo costringerà all'interfaccia una chiamata di funzione virtuale completa, una chiamata diretta e un indice per quest'ultimo.

Il primo modulo è l'approccio orientato agli oggetti.

Il secondo modulo è un approccio di abbinamento di modelli.

Come tale, la prima forma, essendo orientata agli oggetti, rende facile aggiungere nuove enumerazioni, ma è difficile aggiungere nuove operazioni. Il secondo modulo fa il contrario

I programmatori più esperti che conosco raccomanderebbero l'utilizzo della corrispondenza del modello rispetto all'orientamento dell'oggetto. Quando le enumerazioni sono chiuse, aggiungere nuove enumerazioni non è un'opzione; quindi, mi piacerebbe sicuramente andare con quest'ultimo approccio me stesso.

+1

I metodi in enumerazione sono, di progettazione, tutti finalizzati (anche se si omette la parola chiave), quindi il compilatore può ottimizzarli in modo aggressivo. Pertanto, la prima variante dovrebbe essere molto più veloce poiché verrà sostituita con un riferimento alla costante enum (nessuna chiamata al metodo). –

+1

Tuttavia il problema critico non è se il metodo è definitivo, ma quante classi implementano l'interfaccia chiamata. Il problema non è come la JVM ottimizza Direction.UP.getOpposite(), ma come ottimizza myfunc (Direction d) {return d.getOpposite(); }, in questo caso devi scegliere tra una chiamata full-virtual e una chiamata diretta. – Recurse

+0

Dipende interamente da quanto è intelligente il JIT. Potrebbe creare una tabella con riferimenti ai valori e girare 'd.getOpposite()' in 'Orientation.opposites [d.ordinal()]' –

2

Realmente faccio qualcosa di diverso. Le tue soluzioni hanno delle battute d'arresto: i metodi sovrascritti astratti introducono un bel po 'di spese generali, e le dichiarazioni di switch sono piuttosto difficili da mantenere.

suggerisco il seguente schema (applicato al vostro problema):

public enum Direction { 
    UP, RIGHT, DOWN, LEFT; 

    static { 
     Direction.UP.setValues(DOWN, RIGHT, LEFT); 
     Direction.RIGHT.setValues(LEFT, DOWN, UP); 
     Direction.DOWN.setValues(UP, LEFT, RIGHT); 
     Direction.LEFT.setValues(RIGHT, UP, DOWN); 
    } 

    private void setValues(Direction opposite, Direction clockwise, Direction anticlockwise){ 
     this.opposite = opposite; 
     this. clockwise= clockwise; 
     this. anticlockwise= anticlockwise; 
    } 

    Direction opposite; 
    Direction clockwise; 
    Direction anticlockwise; 

    public final Direction getOppposite() { return opposite; } 
    public final Direction getRotateClockwise() { return clockwise; } 
    public final Direction getRotateAnticlockwise() { return anticlockwise; } 
} 

Con tale progetto si:

  • mai dimenticare di impostare una direzione, perché è applicata dal costruttore (nel caso case possibile

  • avere un piccolo metodo di chiamata in testa, perché il metodo è definitivo, non virtuale

  • codice pulito e breve

  • tuttavia si può dimenticare di impostare i valori la propria direzione

+0

... questa soluzione è già stata espressa in un commento, scusa @akaIDIOT, l'ho letto proprio ora :( – Dariusz

+0

Purtroppo questo non viene compilato: "Illegal forward reference "(javac)," Impossibile fare riferimento a un campo prima che sia definito "(eclipse). È necessario un po 'di direzione errata, ad esempio nell'esempio [di Aaron in basso] (http://stackoverflow.com/a/14850978/697449). –

+0

@PaulBellora ha acconsentito, ha cambiato la soluzione, ora è peggio, ma IMO è ancora migliore di quello proposto OP – Dariusz

0

Risposta: Dipende

  1. se il vostro definizioni dei metodi sono semplici

    Questo è il caso con le semplici metodi di esempio, che solo duro-codice un'uscita enum per ogni ingresso enumerazione

    • implementare definizioni specifiche per un valore di enumerazione vicino a quel valore di enumerazione
    • implementare definizioni comuni a tutti i valori di enumerazione nella parte inferiore della classe nella "area comune"; se la stessa firma metodo è quello di essere disponibile per tutti i valori enum, ma nessuno/parte della logica è comune, utilizzare definizioni astratte metodo nella zona comune

    cioè Opzione 1

    Perché?

    • leggibilità, coerenza, manutenibilità: il codice direttamente connesse con una definizione si trova proprio accanto alla definizione
    • fase di compilazione controllando se i metodi astratti dichiarati nella zona comune, ma non nominati nella zona di valore enum

    Si noti che l'esempio Nord/Sud/Est/Ovest potrebbe essere considerato rappresentare uno stato molto semplice (della direzione corrente) e i metodi opposti/ruotare in senso antiorario/ruotare in senso antiorario potrebbero essere considerati per rappresentare i comandi dell'utente per cambiare stato. Il che solleva la domanda, che cosa fai per una macchina a stati reali, in genere complessa?

  2. se il vostro definizioni dei metodi sono complessi:

    Stato-macchine sono spesso complesse, basandosi sulle attuali (valore enumerato) dello stato, ingresso di comando, timer, e un gran regole numero e le eccezioni di business per determinare il nuovo stato (valore enumerato). Altre volte, i metodi possono persino determinare l'output di valore numerico tramite calcoli (ad es. Categorizzazione di rating scientifico/ingegneristico/assicurativo). Oppure potrebbe utilizzare strutture dati come una mappa o una struttura dati complessa adatta a un algoritmo.Quando la logica è complessa, è necessaria un'attenzione particolare e l'equilibrio tra logica "comune" e logica "specifica enum" cambia.

    • evitare di mettere volumi codice eccessiva complessità, e ripete 'tagliato & incollare' sezioni accanto al valore di enumerazione
    • cercano di refactoring più logica possibile nella zona comune - possibilmente mettendo 100% della logica qui, ma se non possibile, impiegando il modello "Metodo di modello" di Gang Of Four per massimizzare la quantità di logica comune, ma in modo flessibile consentire una piccola quantità di logica specifica contro ciascun valore enum. cioè il più possibile di Opzione 1, con un po 'di Opzione 2 consentito

    Perché?

    • leggibilità, coerenza, manutenibilità: evita codice gonfiare, la duplicazione, la scarsa testuale formattazione con masse di codice intervallati tra valori enum, permette il set completo di valori enum per essere rapidamente visto e compreso
    • tempo di compilazione verificare se si utilizza il modello Metodo modello ei metodi astratti dichiarati nell'area comune, ma non specificato nell'area del valore enum

    • Nota: è possibile inserire TUTTA la logica in una classe di supporto separata, ma personalmente non vedo alcun vantaggio in questo (non prestazione/manutenibilità/leggibilità). Rompe un po 'l'incapsulamento e una volta che hai tutta la logica in un posto, che differenza fa aggiungere una semplice definizione enum alla cima della classe? La suddivisione del codice su più classi è una questione diversa e deve essere incoraggiata laddove appropriato.

Problemi correlati