Quando gli enum NON sono un odore di codice?


17

Dilemma

Ho letto molti libri di buone pratiche sulle pratiche orientate agli oggetti e quasi tutti i libri che ho letto hanno avuto una parte in cui affermano che gli enum sono un odore di codice. Penso che abbiano perso la parte in cui spiegano quando le enumerazioni sono valide.

Come tale, sto cercando linee guida e / o casi d'uso in cui gli enum NON sono un odore di codice e in realtà un costrutto valido.

fonti:

"AVVERTENZA Come regola generale, gli enum sono odori di codice e dovrebbero essere sottoposti a refactoring in classi polimorfiche. [8]" Seemann, Mark, Dependency Injection in .Net, 2011, p. 342

[8] Martin Fowler et al., Refactoring: migliorare la progettazione del codice esistente (New York: Addison-Wesley, 1999), 82.

Contesto

La causa del mio dilemma è un'API di trading. Mi danno un flusso di dati Tick inviando questo metodo:

void TickPrice(TickType tickType, double value)

dove enum TickType { BuyPrice, BuyQuantity, LastPrice, LastQuantity, ... }

Ho provato a creare un wrapper attorno a questa API perché interrompere le modifiche è il modo di vivere di questa API. Volevo tenere traccia del valore di ogni ultimo tipo di tick ricevuto sul mio wrapper e l'ho fatto usando un Dizionario di ticktypes:

Dictionary<TickType,double> LastValues

Per me, questo sembrava un uso corretto di un enum se venivano usati come chiavi. Ma sto ripensandoci perché ho un posto in cui prendo una decisione basata su questa raccolta e non riesco a pensare a un modo in cui potrei eliminare l'istruzione switch, potrei usare una fabbrica ma quella fabbrica avrà ancora un istruzione switch da qualche parte. Mi è sembrato di spostare le cose, ma ha ancora un odore.

È facile trovare le cose da non fare degli enum, ma le DO, non così facili, e lo apprezzerei se le persone potessero condividere le loro competenze, i pro ei contro.

Ripensamenti

Alcune decisioni e azioni si basano su queste TickTypee non riesco a pensare a un modo per eliminare le dichiarazioni enum / switch. La soluzione più pulita a cui riesco a pensare è usare una fabbrica e restituire un'implementazione basata su TickType. Anche allora avrò comunque un'istruzione switch che restituisce un'implementazione di un'interfaccia.

Di seguito è elencata una delle classi di esempio in cui ho dubbi sul fatto che potrei usare un enum sbagliato:

public class ExecutionSimulator
{
  Dictionary<TickType, double> LastReceived;
  void ProcessTick(TickType tickType, double value)
  {
    //Store Last Received TickType value
    LastReceived[tickType] = value;

    //Perform Order matching only on specific TickTypes
    switch(tickType)
    {
      case BidPrice:
      case BidSize:
        MatchSellOrders();
        break;
      case AskPrice:
      case AskSize:
        MatchBuyOrders();
        break;
    }       
  }
}

25
Non ho mai sentito parlare di enumerazioni come odori di codice. Potresti includere un riferimento? Penso che abbiano un grande senso per un numero limitato di potenziali valori
Richard Tingle,

24
Quali libri dicono che le enumerazioni hanno un odore di codice? Ottieni libri migliori.
Jacques B

3
Quindi la risposta alla domanda sarebbe "il più delle volte".
gnasher729,

5
Un valore simpatico e sicuro che trasmette significato? Ciò garantisce che le chiavi appropriate siano utilizzate in un dizionario? Quando è un odore di codice?

7
Senza contesto Credo che una formulazione migliore sarebbeenums as switch statements might be a code smell ...
pllee

Risposte:


31

Gli enum sono destinati ai casi d'uso quando hai letteralmente elencato ogni possibile valore che una variabile potrebbe assumere. Mai. Pensa a casi d'uso come giorni della settimana o mesi dell'anno o configura i valori di un registro hardware. Cose che sono sia altamente stabili che rappresentabili con un semplice valore.

Tieni presente che se stai creando un livello anticorruzione, non puoi evitare di avere un'istruzione switch da qualche parte , a causa del design che stai avvolgendo, ma se lo fai nel modo giusto puoi limitarlo a quel punto e usa il polimorfismo altrove.


3
Questo è il punto più importante: aggiungere un valore extra a un enum significa trovare ogni uso del tipo nel codice e potenzialmente dover aggiungere un nuovo ramo per il nuovo valore. Confronta con un tipo polimorfico, dove aggiungere una nuova possibilità significa semplicemente creare una nuova classe che implementa l'interfaccia: le modifiche sono concentrate in un unico posto, quindi più facili da effettuare. Se sei sicuro che non verrà mai aggiunto un nuovo tipo di tick, l'enum va bene, altrimenti dovrebbe essere racchiuso in un'interfaccia polimorfica.
Jules il

Questa è una delle risposte che stavo cercando. Non posso proprio evitarlo quando il metodo che sto avvolgendo usa un enum e lo scopo è centralizzare questi enum in un unico posto. Devo creare il wrapper in modo tale che se l'API dovesse mai cambiare questi enum, devo solo cambiare il wrapper e non i consumatori del wrapper.
Stromms,

@Jules - "Se sei sicuro che un nuovo tipo di tick non verrà mai aggiunto ..." Questa affermazione è sbagliata su così tanti livelli. Una classe per ogni singolo tipo, nonostante ogni tipo che abbia lo stesso identico comportamento sia tutt'altro che un approccio "ragionevole" che si potrebbe ottenere. Non è fino a quando non si hanno comportamenti diversi e si inizia a richiedere istruzioni "switch / if-then" in cui è necessario tracciare la linea. Certamente non si basa sul fatto se potrei aggiungere più valori enum in futuro.
Dunk il

@dunk Penso che tu abbia frainteso il mio commento. Non ho mai suggerito di creare più tipi con un comportamento identico - sarebbe folle.
Jules,

1
Anche se hai "elencato ogni possibile valore" ciò non significa che il tipo non avrà mai funzionalità aggiuntive per istanza. Anche qualcosa come Month può avere altre proprietà utili, come il numero di giorni. L'uso di un enum impedisce immediatamente l'estensione del concetto che stai modellando. È fondamentalmente un meccanismo / modello anti-OOP.
Dave Cousineau,

17

Innanzitutto, un odore di codice non significa che qualcosa non va. Significa che qualcosa potrebbe essere sbagliato. enumodora perché è spesso abusato, ma ciò non significa che devi evitarli. Ti ritrovi a digitare enum, fermarsi e verificare soluzioni migliori.

Il caso particolare che accade più spesso è quando i diversi enum corrispondono a tipi diversi con comportamenti diversi ma con la stessa interfaccia. Ad esempio, parlare con backend diversi, renderizzare pagine diverse, ecc. Questi sono implementati in modo molto più naturale usando classi polimorfiche.

Nel tuo caso, TickType non corrisponde a comportamenti diversi. Sono diversi tipi di eventi o diverse proprietà dello stato corrente. Quindi penso che questo sia il posto ideale per un enum.


Stai dicendo che quando enums contiene nomi come voci (ad es. Colori), probabilmente sono usati correttamente. E quando sono verbi (connessi, rendering), allora è un segno che viene usato in modo improprio?
Stromms,

2
@stromms, la grammatica è una terribile guida all'architettura del software. Se TickType fosse un'interfaccia anziché un enum, quali sarebbero i metodi? Non riesco a vederne nessuno, ecco perché mi sembra un caso enumatico. Altri casi, come stato, colori, back-end, ecc., Posso trovare metodi, ed è per questo che sono interfacce.
Winston Ewert,

@ WinstonEwert e con la modifica sembra che si tratti di comportamenti diversi.

Ohhh. Quindi, se riesco a pensare a un metodo per un enum, allora potrebbe essere un candidato per un'interfaccia? Questo potrebbe essere un ottimo punto.
Stromms

1
@stromms, puoi condividere altri esempi di switch, così posso avere un'idea migliore di come usi TickType?
Winston Ewert,

8

Durante la trasmissione di enumerazioni di dati non viene emesso alcun odore di codice

IMHO, quando si trasmettono dati usando enumsper indicare che un campo può avere un valore da un insieme limitato di valori (che cambia raramente) è buono.

Ritengo preferibile la trasmissione arbitraria stringso ints. Le stringhe possono causare problemi a causa delle variazioni di ortografia e maiuscole. Ints consentire la trasmissione di valori di intervallo e hanno poco semantica (ad esempio, ho ricevuto 3dal servizio di trading, che cosa significa? LastPrice? LastQuantity? Qualcos'altro?

La trasmissione di oggetti e l'utilizzo di gerarchie di classi non è sempre possibile; per esempio, non consente al destinatario di distinguere quale classe è stata inviata.

Nel mio progetto il servizio utilizza una gerarchia di classi per gli effetti delle operazioni, poco prima di trasmettere tramite un DataContractoggetto dalla gerarchia di classi viene copiato in un unionoggetto simile che contiene un enumper indicare il tipo. Il client riceve DataContracte crea un oggetto di una classe in una gerarchia usando il enumvalore switche crea un oggetto del tipo corretto.

Un altro motivo per cui non si vorrebbe trasmettere oggetti di classe è che il servizio potrebbe richiedere un comportamento completamente diverso per un oggetto trasmesso (come LastPrice) rispetto al client. In tal caso, l'invio della classe e dei suoi metodi non è desiderato.

Le istruzioni switch sono errate?

IMHO, un singolo switch stato che chiama costruttori diversi a seconda di un enumnon è un odore di codice. Non è necessariamente migliore o peggiore di altri metodi, come la base di riflessione su un nome tipografico; questo dipende dalla situazione reale.

Avere interruttori enum è un odore di codice, offre alternative che sono spesso migliori:

  • Utilizzare oggetti di diverse classi di una gerarchia con metodi sostituiti; cioè polimorfismo.
  • Utilizzare il modello di visitatore quando le classi nella gerarchia cambiano raramente e quando le (molte) operazioni devono essere liberamente associate alle classi nella gerarchia.

In realtà, TickType viene trasmesso attraverso il filo come un int. Il mio wrapper è quello che usa TickTypeche viene castato dal ricevuto int. Numerosi eventi utilizzano questo tipo di tick con firme variabili selvagge che sono risposte a varie richieste. È pratica comune avere un uso intcontinuo per funzioni diverse? ad esempio TickPrice(int type, double value)usa 1,3 e 6 per typementre TickSize(int type, double valueusa 2,4 e 5? Ha senso anche separarli in due eventi?
Stromms,

2

cosa succede se sei andato con un tipo più complesso:

    abstract class TickType
    {
      public abstract string Name {get;}
      public abstract double TickValue {get;}
    }

    class BuyPrice : TickType
    {
      public override string Name { get { return "Buy Price"; } }
      public override double TickValue { get { return 2.35d; } }
    }

    class BuyQuantity : TickType
    {
      public override string Name { get { return "Buy Quantity"; } }
      public override double TickValue { get { return 4.55d; } }
    }
//etcetera

allora potresti caricare i tuoi tipi dalla riflessione o costruirlo tu stesso, ma la cosa principale da fare qui è che stai trattenendo Open Close Principle di SOLID


1

TL; DR

Per rispondere alla domanda, ora faccio fatica a pensare a un momento in cui gli enum non sono un odore di codice su un certo livello. C'è un certo intento che dichiarano in modo efficiente (esiste un numero chiaramente limitato e limitato di possibilità per questo valore), ma la loro natura intrinsecamente chiusa li rende architettonicamente inferiori.

Mi scusi mentre refactoring tonnellate del mio codice legacy. / sospiro; ^ D


Ma perché?

Recensione abbastanza buona del perché sia ​​il caso di LosTechies qui :

// calculate the service fee
public double CalculateServiceFeeUsingEnum(Account acct)
{
    double totalFee = 0;
    foreach (var service in acct.ServiceEnums) { 

        switch (service)
        {
            case ServiceTypeEnum.ServiceA:
                totalFee += acct.NumOfUsers * 5;
                break;
            case ServiceTypeEnum.ServiceB:
                totalFee += 10;
                break;
        }
    }
    return totalFee;
} 

Questo ha tutti gli stessi problemi del codice sopra. Man mano che l'applicazione si ingrandisce, aumenteranno le possibilità di avere simili succursali. Inoltre, man mano che si implementano altri servizi premium, è necessario modificare continuamente questo codice, il che viola il principio aperto-chiuso. Anche qui ci sono altri problemi. La funzione per calcolare la tariffa del servizio non dovrebbe avere bisogno di conoscere gli importi effettivi di ciascun servizio. Queste sono informazioni che devono essere incapsulate.

Un piccolo inconveniente: gli enum sono una struttura di dati molto limitata. Se non stai usando un enum per quello che è realmente, un intero etichettato, hai bisogno di una classe per modellare veramente l'astrazione correttamente. Puoi usare la fantastica classe di enumerazione di Jimmy per usare le classi e usarle anche come etichette.

Riflettiamo questo per usare il comportamento polimorfico. Ciò di cui abbiamo bisogno è l'astrazione che ci permetterà di contenere il comportamento necessario per calcolare la tariffa per un servizio.

public interface ICalculateServiceFee
{
    double CalculateServiceFee(Account acct);
}

...

Ora possiamo creare le nostre implementazioni concrete dell'interfaccia e collegarle [all'account].

public class Account{
    public int NumOfUsers{get;set;}
    public ICalculateServiceFee[] Services { get; set; }
} 

public class ServiceA : ICalculateServiceFee
{
    double feePerUser = 5; 

    public double CalculateServiceFee(Account acct)
    {
        return acct.NumOfUsers * feePerUser;
    }
} 

public class ServiceB : ICalculateServiceFee
{
    double serviceFee = 10;
    public double CalculateServiceFee(Account acct)
    {
        return serviceFee;
    }
} 

Un altro caso di implementazione ...

La linea di fondo è che se si ha un comportamento che dipende dai valori enum, perché non avere implementazioni diverse di un'interfaccia simile o di una classe genitore che assicuri che quel valore esista? Nel mio caso, sto esaminando diversi messaggi di errore basati sui codici di stato REST. Invece di...

private static string _getErrorCKey(int statusCode)
{
    string ret;

    switch (statusCode)
    {
        case StatusCodes.Status403Forbidden:
            ret = "BRANCH_UNAUTHORIZED";
            break;

        case StatusCodes.Status422UnprocessableEntity:
            ret = "BRANCH_NOT_FOUND";
            break;

        default:
            ret = "BRANCH_GENERIC_ERROR";
            break;
    }

    return ret;
}

... forse dovrei racchiudere i codici di stato nelle classi.

public interface IAmStatusResult
{
    int StatusResult { get; }    // Pretend an int's okay for now.
    string ErrorKey { get; }
}

Quindi ogni volta che ho bisogno di un nuovo tipo di IAmStatusResult, lo codifico ...

public class UnauthorizedBranchStatusResult : IAmStatusResult
{
    public int StatusResult => 403;
    public string ErrorKey => "BRANCH_UNAUTHORIZED";
}

... e ora posso assicurarmi che il codice precedente capisca che ha un IAmStatusResultambito di applicazione e fare riferimento al suo entity.ErrorKeyinvece che al più contorto, deadend _getErrorCode(403).

E, soprattutto, ogni volta che aggiungo un nuovo tipo di valore restituito, non è necessario aggiungere altro codice per gestirlo . Whaddya lo sa, enume switchprobabilmente erano odori di codice.

Profitto.


Come circa il caso in cui, ad esempio, io ho classi polimorfiche e voglio configurare quale quella che sto usando tramite un parametro di riga di comando? Riga di comando -> enum -> switch / map -> l'implementazione sembra un caso d'uso abbastanza legittimo. Penso che la cosa chiave sia che l'enum sia usato per l'orchestrazione, non come implementazione della logica condizionale.
Ant P,

@AntP Perché, in questo caso, non utilizzare il StatusResultvalore? Potresti sostenere che l'enum è utile come scorciatoia memorabile dall'uomo in quel caso d'uso, ma probabilmente chiamerei ancora un odore di codice, poiché ci sono buone alternative che non richiedono la raccolta chiusa.
ruffin,

Quali alternative? Una stringa? Questa è una distinzione arbitraria dalla prospettiva di "enum dovrebbe essere sostituito con polimorfismo" - entrambi gli approcci richiedono la mappatura di un valore su un tipo; evitare l'enum in questo caso non ottiene nulla.
Ant P,

@AntP Non sono sicuro di dove i fili si incrociano qui. Se si fa riferimento a una proprietà su un'interfaccia, è possibile utilizzare tale interfaccia ovunque sia necessaria e non aggiornare mai quel codice quando si crea una nuova (o si rimuove un'implementazione esistente) di tale interfaccia . Se si dispone di una enum, si fa necessario il codice di aggiornamento ogni volta che si aggiunge o rimuove un valore, e potenzialmente un sacco di posti, a seconda di come si strutturato il codice. Usare il polimorfismo al posto di un enum è come fare in modo che il tuo codice sia in forma normale di Boyce-Codd .
ruffin,

Sì. Supponiamo ora di avere N implementazioni polimorfiche. Nell'articolo di Los Techies, stanno semplicemente ripetendo tutti. E se volessi applicare in modo condizionale solo un'implementazione basata, ad esempio, sui parametri della riga di comando? Ora dobbiamo definire una mappatura da un valore di configurazione a un tipo di implementazione iniettabile. Un enum in questo caso è buono come qualsiasi altro tipo di configurazione. Questo è un esempio di una situazione in cui non vi è alcuna ulteriore possibilità di sostituire il "sporco enum" con il polimorfismo. Il ramo per astrazione può portarti solo così lontano.
Ant P

0

Se usare un enum è un odore di codice o meno dipende dal contesto. Penso che puoi prendere alcune idee per rispondere alla tua domanda se consideri il problema dell'espressione . Quindi, hai una raccolta di diversi tipi e una raccolta di operazioni su di essi e devi organizzare il tuo codice. Esistono due semplici opzioni:

  • Organizza il codice in base alle operazioni. In questo caso è possibile utilizzare un'enumerazione per taggare diversi tipi e disporre di un'istruzione switch in ogni procedura che utilizza i dati con tag.
  • Organizza il codice in base ai tipi di dati. In questo caso è possibile sostituire l'enumerazione con un'interfaccia e utilizzare una classe per ciascun elemento dell'enumerazione. Quindi implementare ogni operazione come metodo in ogni classe.

Quale soluzione è migliore?

Se, come ha sottolineato Karl Bielefeldt, i tuoi tipi sono fissi e ti aspetti che il sistema cresca principalmente aggiungendo nuove operazioni su questi tipi, quindi usare un enum e avere un'istruzione switch è una soluzione migliore: ogni volta che hai bisogno di una nuova operazione devi basta implementare una nuova procedura mentre usando le classi dovresti aggiungere un metodo ad ogni classe.

D'altra parte, se ti aspetti di avere un set di operazioni piuttosto stabile ma pensi di dover aggiungere più tipi di dati nel tempo, utilizzare una soluzione orientata agli oggetti è più conveniente: poiché è necessario implementare nuovi tipi di dati, basta continua ad aggiungere nuove classi implementando la stessa interfaccia mentre se stavi usando un enum dovresti aggiornare tutte le istruzioni switch in tutte le procedure usando l'enum.

Se non riesci a classificare il tuo problema in una delle due opzioni sopra, puoi guardare a soluzioni più sofisticate (vedi ad esempio di nuovo la pagina di Wikipedia citata sopra per una breve discussione e qualche riferimento per ulteriori letture).

Quindi, dovresti cercare di capire in quale direzione potrebbe evolvere la tua applicazione e quindi scegliere una soluzione adatta.

Dal momento che i libri a cui ti riferisci riguardano il paradigma orientato agli oggetti, non sorprende che siano di parte rispetto all'utilizzo degli enum. Tuttavia, una soluzione orientata agli oggetti non è sempre l'opzione migliore.

In conclusione: gli enum non sono necessariamente un odore di codice.


si noti che la domanda è stata posta nel contesto di C #, un linguaggio OOP. inoltre, che raggruppare per operazione o per tipo sono due facce della stessa medaglia è interessante, ma non vedo l'uno come "migliore" dell'altro come descrivi. la quantità di codice risultante è la stessa e le lingue OOP facilitano già l'organizzazione per tipo. produce anche un codice sostanzialmente più intuitivo (facile da leggere).
Dave Cousineau,

@DaveCousineau: "Non vedo l'uno come" migliore "dell'altro come descrivi": non ho detto che uno sia sempre migliore dell'altro. Ho detto che uno è migliore dell'altro a seconda del contesto. Ad esempio, se le operazioni sono più o meno fisso e si pensa di aggiungere nuovi tipi (ad esempio, una GUI con fisso paint(), show(), close() resize()operazioni e widget personalizzati) allora l'approccio object-oriented è meglio, nel senso che permette di aggiungere un nuovo tipo senza influire troppo molto codice esistente (praticamente si implementa una nuova classe, che è una modifica locale).
Giorgio,

Non vedo neanche come "migliore" come hai descritto, nel senso che non vedo che dipende dalla situazione. La quantità di codice che devi scrivere è esattamente la stessa in entrambi gli scenari. L'unica differenza è che un modo è antitetico con OOP (enum) e uno no.
Dave Cousineau,

@DaveCousineau: la quantità di codice che devi scrivere è probabilmente la stessa, la località (posizioni nel codice sorgente che devi modificare e ricompilare) cambia drasticamente. Ad esempio in OOP, se si aggiunge un nuovo metodo a un'interfaccia, è necessario aggiungere un'implementazione per ogni classe che implementa tale interfaccia.
Giorgio,

Non è solo una questione di ampiezza VS profondità? Aggiunta di 1 metodo a 10 file o 10 metodi a 1 file.
Dave Cousineau,
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.