Le enumerazioni creano interfacce fragili?

17

Considera l'esempio qui sotto. Qualsiasi modifica all'enumerazione ColorChoice interessa tutte le sottoclassi di IWindowColor.

Le enumerazioni 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: mi dispiace per aver usato il colore come esempio, non è di questo che si tratta. Ecco un altro esempio che evita le aringhe rosse 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 sottoclassi appropriate di ISomethingThatNeedsToKnowWhatTypeOfCharacter vengano 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 {human, dwarf}.

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

    
posta Matthew James Briggs 16.03.2015 - 21:39
fonte

5 risposte

25

Se usate correttamente, le enumerazioni sono molto più leggibili e robuste dei "numeri magici" che sostituiscono. Di solito non li vedo rendere il codice più fragile. Ad esempio:

  • setColor () non deve perdere tempo a controllare se value è un valore di colore valido o meno. Il compilatore lo ha già fatto.
  • È possibile scrivere setColor (Color :: Red) anziché setColor (0). Credo che la funzionalità enum class nel C ++ moderno ti permetta di costringere le persone a scrivere sempre il primo anziché il secondo.
  • Solitamente non è importante, ma la maggior parte delle enumerazioni può essere implementata con qualsiasi tipo di dimensione integrale, quindi il compilatore può scegliere qualsiasi dimensione sia la più conveniente senza costringerti a pensare a queste cose.

Tuttavia, l'uso di un'enumerazione per il colore è discutibile perché in molte (più?) situazioni non c'è motivo di limitare l'utente a un così piccolo insieme di colori; potresti anche lasciarli passare in qualsiasi valore RGB arbitrario. Sui progetti con cui lavoro, un piccolo elenco di colori come questo apparirebbe sempre e solo come parte di un insieme di "temi" o "stili" che dovrebbe agire come una sottile astrazione su colori concreti.

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

Modifica: post-modifica, non sono ancora chiaro sul tipo di estendibilità che stai cercando, ma continuo a pensare che lo schema di comando sia la cosa più vicina a un "enfolo polimorfico".

    
risposta data 16.03.2015 - 22:18
fonte
15

Any change to the ColorChoice enum affects all IWindowColor subclasses.

No, non lo è. Ci sono due casi: gli implementatori o

  • memorizza, restituisce e inoltra i valori enum, non operando mai su di essi, nel qual caso non sono influenzati dalle modifiche nell'enumerazione o

  • operano su valori di enum individuali, nel qual caso qualsiasi cambiamento nell'enum deve naturalmente, naturalmente, inevitabilmente, necessariamente , essere giustificato con un corrispondente cambiamento nella logica del implementatore .

Se metti "Sfera", "Rettangolo" e "Piramide" in un enum di "Forma", e passa tale enum a una funzione drawSolid() che hai scritto per disegnare il solido corrispondente, e poi una mattina decidi di aggiungere un valore "Ikositetraedro" all'enumerazione, non puoi aspettarti che la funzione drawSolid() rimanga inalterata; Se ti aspettavi che in qualche modo disegnasse icositetraedri senza che prima dovessi scrivere il codice reale per disegnare icositetraedri, è colpa tua, non è colpa dell'enum. Quindi:

Do enums tend to cause brittle interfaces?

No, non lo fanno. Ciò che causa interfacce fragili sono i programmatori che pensano a se stessi come ninja, cercando di compilare il loro codice senza che siano attivati avvisi sufficienti. Quindi, il compilatore non li avvisa che la loro funzione drawSolid() contiene un'istruzione switch che manca una clausola case 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: devi quindi implementare questo metodo su ogni singolo ereditario, altrimenti il progetto non deve, e non dovrebbe, costruire.

A dire il vero, le enumerazioni non sono un costrutto orientato agli oggetti. Sono più di un compromesso pragmatico tra il paradigma orientato agli oggetti e il paradigma di programmazione strutturata.

Il puro modo di fare le cose orientato agli oggetti non è avere affatto enumerazioni e invece 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 singolo metodo mostruoso centralizzato che sa disegnare tutto e dover sapere quale solido disegnare, tu dichiara una classe "Solid" con un metodo astratto (puro virtuale) draw() , quindi aggiungi le sottoclassi "Sphere", "Rectangle" e "Pyramid", ciascuna con la propria implementazione di draw() che sa come disegnare .

In questo modo, quando introduci la sottoclasse "Ikositetrahedron", devi solo fornire una funzione draw() per esso, e il compilatore ti ricorderà di farlo, non permettendoti di creare un'istanza "Icositetraedro" altrimenti.

    
risposta data 16.03.2015 - 22:22
fonte
14

Le enumerazioni non creano interfacce fragili. Uso improprio di enumerazioni.

Per cosa sono le enumerazioni?

Le enumerazioni sono progettate per essere utilizzate come insiemi di costanti con nome significativo. Devono essere usati quando:

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

Buoni usi delle enumerazioni:

  • Giorni della settimana: (come per. Net's System.DayOfWeek ) A meno che tu non lo sia occupandosi di un calendario incredibilmente oscuro, ci sarà sempre e solo essere 7 giorni della settimana.
  • Colori non estensibili: (come per. Net System.ConsoleColor ) Alcuni potrebbe non essere d'accordo con questo, ma .Net ha scelto di farlo per un motivo. Nel .Net's console system, ci sono solo 16 colori disponibili per l'uso da la console. Questi 16 colori corrispondono alla tavolozza dei colori legacy noto come CGA o "Adattatore grafico a colori" . Non ci saranno nuovi valori mai aggiunto, il che significa che questo è in realtà un ragionevole applicazione di un enum.
  • Enumerazioni che rappresentano stati fissi: (come per Java Thread.State ) I progettisti di Java hanno deciso che in Java il modello di threading sarà sempre e solo un insieme fisso di stati che a Thread può essere in, e quindi per semplificare le cose, questi diversi gli stati sono rappresentati come enumerazioni. Questo significa che molti i controlli basati sullo stato sono semplici if e switch che in pratica operare su valori interi, senza che il programmatore debba preoccuparsi su cosa siano effettivamente i valori.
  • Bitflags che rappresentano opzioni non mutuamente esclusive: (come per .Net System.Text.RegularExpressions.RegexOptions ) I bitflags sono un uso molto comune di enumerazioni. Così comune, infatti, in .Net tutte le enumerazioni avere un metodo HasFlag(Enum flag) integrato. Supportano anche il operatori bit a bit e c'è un FlagsAttribute per contrassegnare un enum come essere inteso come un insieme di bitflags. Usando un enum come a insieme di flag, è possibile rappresentare un gruppo di valori booleani in un singolo valore, così come avere le bandiere chiaramente nominate per comodità. Ciò sarebbe estremamente vantaggioso per rappresentare le bandiere di a registro di stato in un emulatore o per rappresentare i permessi di un file (leggi, scrivi, esegui), o praticamente qualsiasi situazione in cui a la serie di opzioni correlate non si escludono a vicenda.

Cattivi usi dell'enumerazione:

  • Classi / tipi di personaggi in un gioco: a meno che il gioco non sia una demo one-shot che è improbabile che si usi di nuovo, non si dovrebbe usare enumerazioni classi di personaggi perché è probabile che vorrai aggiungere un molte più lezioni. È meglio avere una singola classe che rappresenti carattere e ha un carattere di "gioco" nel gioco rappresentato diversamente. Un modo per gestirlo è il pattern TypeObject , altro le soluzioni includono la registrazione di tipi di caratteri con un dizionario / tipo registro, o una combinazione dei due.
  • Colori estensibili: se utilizzi un enum per i colori che possono in seguito verrà aggiunto a questo non è una buona idea rappresentarlo in enum forma, altrimenti rimarrai per sempre aggiungendo colori. Questo è simile al problema di cui sopra, quindi dovrebbe essere utilizzata una soluzione simile (ad esempio a variazione di TypeObject).
  • Stato estensibile: se hai una macchina a stati che potrebbe finire introducendo molti altri stati, non è una buona idea rappresentare questi stati tramite enumerazioni. Il metodo preferito è definire un interfaccia per lo stato della macchina, fornire un'implementazione o una classe che avvolge l'interfaccia e delega le sue chiamate al metodo (simile al Strategy pattern), e quindi altera il suo stato cambiando quale l'implementazione fornita è attualmente attiva.
  • Bitflags che rappresentano opzioni mutuamente esclusive: se utilizzi le enumerazioni per rappresentare i flag e due di questi flag non dovrebbero mai verificarsi insieme poi ti sei sparato ai piedi. Tutto ciò che è programmato per reagire a una delle bandiere a cui reagirà improvvisamente qualunque bandiera sia programmata per rispondere prima - o peggio, essa potrebbe rispondere ad entrambi. Questo tipo di situazione è solo chiedendo guaio. L'approccio migliore consiste nel trattare l'assenza di un flag come condizione alternativa, se possibile (vale a dire l'assenza del flag True implica False ). Questo comportamento può essere ulteriormente supportato mediante l'uso di funzioni specializzate (ad esempio IsTrue(flags) e IsFalse(flags) ).
risposta data 17.03.2015 - 03:33
fonte
4

Le enumerazioni sono un grande miglioramento rispetto ai numeri di identificazione magici per insiemi chiusi di valori che non hanno molte funzionalità ad essi associati. Di solito non ti importa di quale numero sia effettivamente associato all'enum; in questo caso è facile estendere aggiungendo nuove voci alla fine, non dovrebbe risultare alcuna fragilità.

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

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

Uno o due di questi è ok, ma non appena hai questo tipo di enume significativo, queste dichiarazioni di switch tendono a moltiplicarsi come triboli. Ora ogni volta che estendi l'enum, devi cercare tutti gli switch es associati per assicurarti di aver coperto tutto. Questo è fragile. Fonti come Pulisci codice suggeriscono che dovresti avere una switch per enum, al massimo.

In questo caso ciò che dovresti fare è usare i principi OO e creare un'interfaccia per questo tipo. Puoi ancora mantenere l'enum per comunicare questo tipo, ma non appena hai bisogno di fare qualcosa con esso, crei un oggetto associato all'enumerazione, possibilmente usando una Fabbrica. È molto più semplice da mantenere, poiché devi solo cercare un posto per aggiornare: il tuo Factory e aggiungendo una nuova classe.

    
risposta data 17.03.2015 - 01:07
fonte
1

Se fai attenzione a come li usi, non considererei le enumerazioni dannose. Ma ci sono alcune cose da considerare se vuoi usarle in qualche codice di libreria, al contrario di una singola applicazione.

  1. Non rimuovere o riordinare i valori. Se a un certo punto hai un valore enum nella tua lista, quel valore dovrebbe essere associato a quel nome per tutta l'eternità. Se lo desideri, puoi a un certo punto rinominare i valori in deprecated_orc o qualsiasi altra cosa, ma non rimuovendoli puoi più facilmente mantenere la compatibilità con il vecchio codice compilato con il vecchio set di enumerazioni. Se alcuni nuovi codici non riescono a gestire le vecchie costanti di enum, o generano un errore appropriato lì o assicurati che nessun valore raggiunga quel pezzo di codice.

  2. Non fare aritmetica su di essi. In particolare, non fare confronti tra ordini. Perché? Perché in questo caso potresti incontrare una situazione in cui non è possibile mantenere i valori esistenti e conservare un ordine sano allo stesso tempo. Prendiamo 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 della lista e quindi interrompere l'ordine, oppure li si interlaccia con le chiavi esistenti e quindi interrompere le mappature stabilite utilizzate nel codice legacy. Oppure deprecate tutte le vecchie chiavi, e avete un set di chiavi completamente nuovo, insieme ad un codice di compatibilità che traduce tra vecchie e nuove chiavi per motivi di codice legacy.

  3. Scegli una dimensione sufficiente. Di default il compilatore usa il numero intero più piccolo che può contenere tutti i valori enum. Il che significa che se, ad un certo aggiornamento, il tuo insieme di possibili enumerri si espande da 254 a 259, all'improvviso ti servono 2 byte per ogni valore enum invece di uno. In questo modo si potrebbero rompere strutture e layout di classe ovunque, quindi cerca di evitarlo utilizzando una dimensione sufficiente nel primo progetto. C ++ 11 ti dà un sacco di controllo qui, ma altrimenti sarebbe utile anche specificare una voce LAST_SPECIES_VALUE=65535 .

  4. Avere un registro centrale. Dal momento che si desidera correggere il mapping tra nomi e valori, è sbagliato se si consente agli utenti di terze parti del 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 insinuarvi per aggiungerli. Ciò significa che per l'esempio umano e nano che hai menzionato, le enumerazioni sono davvero inadatte. In questo caso sarebbe meglio disporre di una sorta di registro in fase di esecuzione, in cui gli utenti del codice possono inserire una stringa e ottenere 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 sopra faccia rendere le enumerazioni inadatte alla tua situazione ipotetica, 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 il tuo biblioteca. Quindi le enumerazioni dovrebbero essere appropriate lì se prendi a cuore i miei altri suggerimenti.

    
risposta data 17.03.2015 - 00:21
fonte

Leggi altre domande sui tag