Gli enum creano interfacce fragili?


17

Considera l'esempio di seguito. Qualsiasi modifica all'enumerazione ColorChoice influisce su tutte le sottoclassi IWindowColor.

Gli enum tendono a causare interfacce fragili? C'è qualcosa di meglio di un enum per consentire una maggiore flessibilità polimorfica?

enum class ColorChoice
{
    Blue = 0,
    Red = 1
};

class IWindowColor
{
public:
    ColorChoice getColor() const=0;
    void setColor( const ColorChoice value )=0;
};

Modifica: scusate per aver usato il colore come mio esempio, non è questo il problema. Ecco un esempio diverso che evita l'aringa rossa e fornisce maggiori informazioni su cosa intendo per flessibilità.

enum class CharacterType
{
    orc = 0,
    elf = 1
};

class ISomethingThatNeedsToKnowWhatTypeOfCharacter
{
public:
    CharacterType getCharacterType() const;
    void setCharacterType( const CharacterType value );
};

Inoltre, immagina che le maniglie per la sottoclasse ISomethingThatNeedsToKnowWhatTypeOfCharacter appropriata siano distribuite da un modello di progettazione di fabbrica. Ora ho un'API che non può essere estesa in futuro per un'applicazione diversa in cui i tipi di caratteri consentiti sono {umano, nano}.

Modifica: solo per essere più concreti su ciò su cui sto lavorando. Sto progettando un forte legame di questa specifica ( MusicXML ) e sto usando le classi enum per rappresentare quei tipi nella specifica che sono dichiarati con xs: enumeration. Sto cercando di pensare a cosa succede quando esce la prossima versione (4.0). La mia libreria di classi potrebbe funzionare in modalità 3.0 e 4.0? Se la prossima versione è compatibile al 100% con le versioni precedenti, forse. Ma se i valori di enumerazione fossero rimossi dalle specifiche, allora sarei morto nell'acqua.


2
Quando dite "flessibilità polimorfica", quali capacità avete esattamente in mente?
Ixrec,


3
L'uso di un enum per i colori crea interfacce fragili, non solo "usando un enum".
Doc Brown

3
L'aggiunta di una nuova variante enum rompe il codice usando quell'enum. L'aggiunta di una nuova operazione a un enum è piuttosto autonoma, d'altra parte, poiché tutti i casi che devono essere gestiti sono proprio lì (contrasto con interfacce e superclassi, in cui l'aggiunta di un metodo non predefinito è un grave cambiamento). Dipende dal tipo di modifiche che sono davvero necessarie.

1
Per quanto riguarda MusicXML: se non esiste un modo semplice per capire dai file XML quale versione dello schema viene utilizzata da ognuno, ciò mi sembra un difetto di progettazione critico nelle specifiche. Se devi risolvere il problema in qualche modo, probabilmente non c'è modo per noi di aiutare fino a quando non sapremo esattamente cosa sceglieranno di rompere in 4.0 e puoi chiedere un problema specifico che causa.
Ixrec,

Risposte:


25

Se usati correttamente, gli enum sono molto più leggibili e robusti dei "numeri magici" che sostituiscono. Normalmente non li vedo rendere il codice più fragile. Per esempio:

  • setColor () non deve perdere tempo a verificare se il valuevalore del colore è valido o meno. Il compilatore l'ha già fatto.
  • È possibile scrivere setColor (Color :: Red) invece di setColor (0). Credo che la enum classfunzionalità del C ++ moderno ti permetta persino di forzare le persone a scrivere sempre la prima anziché la seconda.
  • Di solito non è importante, ma la maggior parte degli enum può essere implementata con qualsiasi tipo integrale di dimensioni, quindi il compilatore può scegliere qualsiasi dimensione sia più conveniente senza costringerti a pensare a tali cose.

Tuttavia, uso di un enum per il colore è discutibile perché in molte (la maggior parte?) Situazioni non c'è motivo di limitare l'utente a un insieme così piccolo di colori; potresti anche lasciarli passare in qualsiasi valore RGB arbitrario. Sui progetti con cui lavoro, un piccolo elenco di colori come questo verrebbe fuori solo come parte di una serie di "temi" o "stili" che dovrebbero agire come una sottile astrazione su colori concreti.

Non sono sicuro di quale sia la tua domanda sulla "flessibilità polimorfica". Gli enum non hanno alcun codice eseguibile, quindi non c'è nulla da rendere polimorfico. Forse stai cercando il modello di comando ?

Modifica: dopo la modifica, non sono ancora chiaro su quale tipo di estensibilità stai cercando, ma penso ancora che il modello di comando sia la cosa più vicina a un "enum polimorfico".


Dove posso trovare ulteriori informazioni sul non consentire a 0 di passare come enum?
TankorSmash

5
@TankorSmash C ++ 11 ha introdotto la "classe enum", chiamata anche "enum con ambito" che non può essere implicitamente convertita nel tipo numerico sottostante. Evitano anche di inquinare lo spazio dei nomi come facevano i vecchi tipi "enum" in stile C.
Matthew James Briggs,

2
Gli enum sono generalmente supportati da numeri interi. Ci sono molte cose strane che possono accadere nella serializzazione / deserializzazione o nel casting tra numeri interi ed enumerazioni. Non è sempre sicuro supporre che un enum abbia sempre un valore valido.
Eric

1
Hai ragione Eric (e questo è un problema che ho riscontrato diverse volte da solo). Tuttavia, devi solo preoccuparti di un valore eventualmente non valido nella fase di deserializzazione. Tutte le altre volte che usi enum, puoi assumere che il valore sia valido (almeno per gli enum in lingue come Scala - alcune lingue non hanno un controllo del tipo molto forte per gli enum).
Kat

1
@Ixrec "perché in molte (la maggior parte?) Situazioni non c'è motivo di limitare l'utente a un insieme così piccolo di colori" Ci sono casi legittimi. Il Console .Net emula la console di Windows vecchio stile, che può avere solo il testo in uno dei 16 colori (16 colori dello standard CGA) msdn.microsoft.com/en-us/library/...
Pharap

15

Qualsiasi modifica all'enumerazione ColorChoice influisce su tutte le sottoclassi IWindowColor.

No non lo fa. Esistono due casi: gli implementatori lo faranno

  • memorizzare, restituire e inoltrare i valori enum, senza mai operare su di essi, nel qual caso non sono interessati dalle modifiche nell'enum, oppure

  • operare su singoli valori di enum, nel qual caso qualsiasi cambiamento nell'enum deve ovviamente, naturalmente, inevitabilmente, necessariamente , essere considerato con un corrispondente cambiamento nella logica dell'implementatore.

Se inserisci "Sfera", "Rettangolo" e "Piramide" in un enum "Shape" e passi un tale enum a una drawSolid()funzione che hai scritto per disegnare il solido corrispondente, e poi una mattina decidi di aggiungere un " Ikositetrahedron "valore per l'enum, non puoi aspettarti che la drawSolid()funzione rimanga inalterata; Se ti aspettavi che in qualche modo disegnasse icositetraedri senza che tu debba prima scrivere il codice effettivo per disegnare icositetraedri, questa è colpa tua, non colpa dell'enum. Così:

Gli enum tendono a causare interfacce fragili?

No, non lo fanno. Ciò che causa interfacce fragili è che i programmatori pensano a se stessi come ninja, provando a compilare il loro codice senza avere sufficienti avvisi abilitati. Quindi, il compilatore non li avverte che la loro drawSolid()funzione contiene switchun'istruzione che manca acase clausola per il valore enum "Ikositetrahedron" appena aggiunto.

Il modo in cui dovrebbe funzionare è analogo all'aggiunta di un nuovo metodo virtuale puro a una classe base: è quindi necessario implementare questo metodo su ogni singolo erede, altrimenti il ​​progetto non costruisce e non dovrebbe costruirlo.


A dire il vero, gli enum non sono un costrutto orientato agli oggetti. Sono più di un compromesso pragmatico tra il paradigma orientato agli oggetti e il paradigma di programmazione strutturato.

Il puro modo orientato agli oggetti di fare le cose non è avere enumerazioni, ma avere oggetti fino in fondo.

Quindi, il modo puramente orientato agli oggetti per implementare l'esempio con i solidi è ovviamente quello di usare il polimorfismo: invece di avere un unico mostruoso metodo centralizzato che sa come disegnare tutto e che deve essere detto quale solido disegnare, si dichiara un " Solida "classe con un draw()metodo astratto (puro virtuale) , e quindi aggiungi le sottoclassi" Sfera "," Rettangolo "e" Piramide ", ognuna con la propria implementazione di draw()cui sa disegnare.

In questo modo, quando si introduce la sottoclasse "Ikositetrahedron", sarà sufficiente fornire una draw()funzione per esso e il compilatore ti ricorderà di farlo, non lasciandoti istanziare "Icositetrahedron" altrimenti.


Qualche consiglio su come lanciare un avviso di compilazione per quegli switch? Di solito lancio solo eccezioni di runtime; ma il tempo di compilazione potrebbe essere fantastico! Non altrettanto eccezionale .. ma mi vengono in mente i test unitari.
Vaughan Hilts,

1
È passato un po 'di tempo dall'ultima volta che ho usato C ++, quindi non sono sicuro, ma mi aspetto che fornire il parametro -Wall al compilatore e omettere la default:clausola dovrebbe farlo. Una rapida ricerca sull'argomento non ha prodotto risultati più definiti, quindi potrebbe essere adatto come argomento di un'altra programmers SEdomanda.
Mike Nakis,

In C ++ può esserci un controllo del tempo di compilazione se lo si abilita. In C # qualsiasi controllo dovrebbe essere in fase di esecuzione. Ogni volta che prendo un enum senza flag come parametro, mi assicuro di convalidarlo con Enum.IsDefined, ma ciò significa comunque che è necessario aggiornare manualmente tutti gli usi dell'enum quando si aggiunge un nuovo valore. Vedi: stackoverflow.com/questions/12531132/…
Mike supporta Monica il

1
Spero che un programmatore orientato agli oggetti non farebbe mai il loro oggetto di trasferimento di dati, come in Pyramidrealtà sa come fare a draw()una piramide. Nella migliore delle ipotesi potrebbe derivare Solide avere un GetTriangles()metodo e potresti passarlo a un SolidDrawerservizio. Pensavo che ci stessimo allontanando dagli esempi di oggetti fisici come esempi di oggetti in OOP.
Scott Whitlock,

1
Va bene, proprio non piaceva a nessuno colpire la buona vecchia programmazione orientata agli oggetti. :) Soprattutto dal momento che molti di noi combinano più la programmazione funzionale e OOP più che rigorosamente OOP.
Scott Whitlock,

14

Gli enum non creano interfacce fragili. Lo fa l' abuso di enum.

A cosa servono gli enum?

Gli enum sono progettati per essere utilizzati come insiemi di costanti con un nome significativo. Devono essere usati quando:

  • Sai che nessun valore verrà rimosso.
  • (E) Sai che è altamente improbabile che sia necessario un nuovo valore.
  • (O) Accetti che sarà necessario un nuovo valore, ma raramente abbastanza da giustificare la correzione di tutto il codice che si rompe a causa di esso.

Buoni usi degli enum:

  • Giorni della settimana: (come da .Net System.DayOfWeek) A meno che tu non abbia a che fare con un calendario incredibilmente oscuro, ci saranno solo 7 giorni della settimana.
  • Colori non estensibili: (come da .Net System.ConsoleColor) Alcuni potrebbero non essere d'accordo con questo, ma .Net ha scelto di farlo per un motivo. Nel sistema di console .Net, ci sono solo 16 colori disponibili per l'uso da parte della console. Questi 16 colori corrispondono alla tavolozza dei colori legacy nota come CGA o "Color Graphics Adapter" . Non verranno mai aggiunti nuovi valori, il che significa che si tratta in realtà di un'applicazione ragionevole di un enum.
  • Enumerazioni che rappresentano stati fissi: (secondo Java Thread.State) I progettisti di Java hanno deciso che nel modello di threading Java ci sarà sempre e solo un insieme fisso di stati in cui Threadpuò essere presente un, e quindi per semplificare le cose, questi diversi stati sono rappresentati come enumerazioni . Ciò significa che molti controlli basati sullo stato sono semplici if e switch che in pratica operano su valori interi, senza che il programmatore debba preoccuparsi di quali siano effettivamente i valori.
  • Bitflags che rappresentano opzioni non reciprocamente esclusive: (come da .Net System.Text.RegularExpressions.RegexOptions) I bitflags sono un uso molto comune di enumerazioni. Così comune, infatti, che in .Net tutti gli enum hanno un HasFlag(Enum flag)metodo incorporato. Supportano anche gli operatori bit a bit e c'è un FlagsAttributesegno per enum come destinato ad essere usato come un insieme di bitflags. Usando un enum come un insieme di flag, puoi rappresentare un gruppo di valori booleani in un singolo valore, oltre ad avere i flag chiaramente indicati per comodità. Ciò sarebbe estremamente vantaggioso per rappresentare i flag di un registro di stato in un emulatore o per rappresentare le autorizzazioni di un file (lettura, scrittura, esecuzione) o per qualsiasi situazione in cui un insieme di opzioni correlate non si escludano a vicenda.

Cattivi usi degli enum:

  • Classi / tipi di personaggi in un gioco: a meno che il gioco non sia una demo one-shot che è improbabile che tu possa riutilizzare, le enumerazioni non dovrebbero essere usate per le classi di personaggi perché è probabile che tu voglia aggiungere molte più classi. È meglio avere una singola classe che rappresenti un personaggio e avere un "tipo" di personaggio in gioco rappresentato altrimenti. Un modo per gestirlo è il modello TypeObject , altre soluzioni includono la registrazione di tipi di carattere con un registro dizionario / tipo o una combinazione dei due.
  • Colori estensibili: se stai usando un enum per i colori che potrebbero essere aggiunti in seguito, non è una buona idea rappresentarlo in forma di enum, altrimenti rimarrai per sempre aggiungendo colori. Questo è simile al problema precedente, quindi dovrebbe essere usata una soluzione simile (cioè una variante di TypeObject).
  • Stato estensibile: se si dispone di una macchina a stati che potrebbe finire per introdurre molti più stati, non è una buona idea rappresentare questi stati tramite enumerazioni. Il metodo preferito consiste nel definire un'interfaccia per lo stato della macchina, fornire un'implementazione o una classe che avvolge l'interfaccia e deleghi le sue chiamate di metodo (simile al modello di strategia ), quindi altera il suo stato modificando quale implementazione implementata è attualmente attiva.
  • Bitflags che rappresentano opzioni reciprocamente esclusive: se stai usando enum per rappresentare le bandiere e due di quelle bandiere non dovrebbero mai verificarsi insieme, allora ti sei sparato al piede. Tutto ciò che è programmato per reagire a uno dei flag reagirà improvvisamente a qualsiasi flag a cui è programmato per rispondere per primo - o peggio, potrebbe rispondere a entrambi. Questo tipo di situazione richiede solo problemi. L'approccio migliore è considerare l'assenza di una bandiera come condizione alternativa, se possibile (cioè l'assenza della Truebandiera implica False). Questo comportamento può essere ulteriormente supportato mediante l'uso di funzioni specializzate (ie IsTrue(flags)e IsFalse(flags)).

Aggiungerò esempi di bitflag se qualcuno può trovare un esempio funzionante o ben noto di enum usati come bitflags. Sono consapevole che esistono, ma sfortunatamente non me ne ricordo nulla al momento.
Pharap,


@BLSully Ottimo esempio. Non posso dire di averli mai usati, ma esistono per un motivo.
Pharap

4

Gli enum sono un grande miglioramento rispetto ai numeri di identificazione magici per insiemi di valori chiusi a cui non sono associate molte funzionalità. Di solito non ti importa di quale numero sia effettivamente associato all'enum; in questo caso è facile estenderlo aggiungendo nuove voci alla fine, non dovrebbe derivarne fragilità.

Il problema è quando si hanno funzionalità significative associate all'enum. Cioè, hai questo tipo di codice in giro:

switch (my_enum) {
case orc: growl(); break;
case elf: sing(); break;
...
}

Uno o due di questi sono ok, ma non appena si ha questo tipo di enum significativo, queste switchaffermazioni tendono a moltiplicarsi come triplici. Ora ogni volta che estendi l'enum, devi dare la caccia a tutti gli switches associati per assicurarti di aver coperto tutto. Questo è fragile. Fonti come Clean Code suggeriranno che dovresti averne uno switchper enum, al massimo.

Quello che dovresti fare invece in questo caso è usare i principi OO e creare un'interfaccia per questo tipo. Puoi ancora conservare l'enum per comunicare questo tipo, ma non appena devi farci qualcosa, crei un oggetto associato all'enum, possibilmente usando una Factory. È molto più facile da mantenere, poiché è necessario cercare un solo posto per l'aggiornamento: la tua fabbrica e aggiungendo una nuova classe.


1

Se stai attento a come li usi, non considererei dannosi gli enum. Ma ci sono alcune cose da considerare se si desidera utilizzarli in alcuni codici di libreria, al contrario di una singola applicazione.

  1. Non rimuovere o riordinare mai i valori. Se ad un certo punto hai un valore enum nel tuo elenco, quel valore dovrebbe essere associato a quel nome per tutta l'eternità. Se lo desideri, puoi a un certo punto rinominare i valori indeprecated_orc qualsiasi modo, ma non rimuovendoli è possibile mantenere più facilmente la compatibilità con il vecchio codice compilato con il vecchio set di enumerazioni. Se un nuovo codice non è in grado di gestire le costanti enum precedenti, genera lì un errore adatto o assicurati che tale valore non raggiunga quel pezzo di codice.

  2. Non fare aritmetica su di loro. In particolare, non effettuare confronti tra ordini. Perché? Perché allora potresti imbatterti in una situazione in cui non puoi conservare i valori esistenti e preservare un ordinamento sano allo stesso tempo. Prendi ad esempio un enum per le direzioni della bussola: N = 0, NE = 1, E = 2, SE = 3, ... Ora se dopo un aggiornamento includi NNE e così via tra le direzioni esistenti, puoi aggiungerle alla fine dell'elenco, interrompendo così l'ordinamento oppure intercalandoli con le chiavi esistenti e interrompendo così le mappature stabilite utilizzate nel codice legacy. Oppure si deprecano tutte le vecchie chiavi e si dispone di un set di chiavi completamente nuovo, insieme a un codice di compatibilità che si traduce tra chiavi vecchie e nuove per motivi di codice legacy.

  3. Scegli una dimensione sufficiente. Per impostazione predefinita, il compilatore utilizzerà il numero intero più piccolo che può contenere tutti i valori enum. Il che significa che se, a un certo aggiornamento, il tuo set di possibili enum si espande da 254 a 259, hai improvvisamente bisogno di 2 byte per ogni valore di enum invece di uno. Ciò potrebbe interrompere la struttura e i layout delle classi in tutto il luogo, quindi cerca di evitarlo utilizzando una dimensione sufficiente nel primo progetto. C ++ 11 ti dà molto controllo qui, ma altrimenti specifica una voceLAST_SPECIES_VALUE=65535 dovrebbe aiutare.

  4. Avere un registro centrale. Dato che vuoi correggere il mapping tra nomi e valori, è male se permetti agli utenti di terze parti del tuo codice di aggiungere nuove costanti. Nessun progetto che utilizza il tuo codice dovrebbe essere autorizzato a modificare quell'intestazione per aggiungere nuovi mapping. Invece dovrebbero bug per aggiungerli. Ciò significa che per l'esempio umano e nano che hai citato, gli enum sono davvero inadatti. Lì sarebbe meglio avere un qualche tipo di registro in fase di esecuzione, in cui gli utenti del codice possono inserire una stringa e recuperare un numero univoco, ben racchiuso in un tipo opaco. Il "numero" potrebbe anche essere un puntatore alla stringa in questione, non importa.

Nonostante il fatto che il mio ultimo punto di cui sopra renda gli enumerati inadatti alla tua ipotetica situazione, la tua situazione attuale in cui alcune specifiche potrebbero cambiare e dovresti aggiornare un po 'di codice sembra adattarsi abbastanza bene con un registro centrale per la tua libreria. Quindi gli enum dovrebbero essere appropriati lì se prendi a cuore i miei altri suggerimenti.

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.