Quando as enums NÃO são um cheiro de código?

17

Dilema

Eu tenho lido muitos livros de práticas recomendadas sobre práticas orientadas a objetos, e quase todos os livros que li tiveram uma parte em que eles dizem que as enums são um cheiro de código. Eu acho que eles perderam a parte em que explicam quando as enums são válidas.

Como tal, estou procurando diretrizes e / ou casos de uso em que as enumerações NÃO são um cheiro de código e, de fato, são uma construção válida.

Fontes:

"AVISO Como regra geral, as enums são odores de código e devem ser refatoradas para classes polimórficas. [8]" Seemann, Mark, Injection Dependency in .Net, 2011, p. 342

[8] Martin Fowler et al., Refatoração: Melhorando o Design do Código Existente (Nova York: Addison-Wesley, 1999), 82.

Contexto

A causa do meu dilema é uma API de negociação. Eles me fornecem um fluxo de dados do Tick enviando este método:

void TickPrice(TickType tickType, double value)

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

Eu tentei criar um invólucro em torno dessa API porque quebrar alterações é o modo de vida dessa API. Eu queria acompanhar o valor de cada último tipo de tick recebido no meu wrapper e fiz isso usando um Dictionary of ticktypes:

Dictionary<TickType,double> LastValues

Para mim, isso parecia um uso adequado de um enum se eles fossem usados ​​como chaves. Mas estou pensando duas vezes porque tenho um lugar onde tomo uma decisão com base nesta coleção e não consigo pensar em como eliminar a declaração do interruptor, poderia usar uma fábrica, mas essa fábrica ainda terá um mude a declaração em algum lugar. Pareceu-me que estou apenas mudando as coisas, mas ainda cheira.

É fácil encontrar o NÃO das enumerações, mas as DOs, não tão fáceis, e eu apreciaria se as pessoas pudessem compartilhar seus conhecimentos, os prós e os contras.

Segundas intenções

Algumas decisões e ações são baseadas nelas TickTypee não consigo pensar em uma maneira de eliminar enumerações / alternar declarações. A solução mais limpa que consigo pensar é usar uma fábrica e retornar uma implementação com base TickType. Mesmo assim, ainda terei uma instrução switch que retorna a implementação de uma interface.

Listada abaixo está uma das classes de exemplo em que estou com dúvidas de que possa estar usando uma enumeração errada:

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;
    }       
  }
}
stromms
fonte
25
Eu nunca ouvi falar de enums sendo um cheiro de código. Você poderia incluir uma referência? Eu acho que eles fazem grande sentido para um número limitado de valores possíveis
Richard Tingle
24
Quais livros estão dizendo que enums são um cheiro de código? Obtenha livros melhores.
JacquesB
3
Portanto, a resposta para a pergunta seria "na maioria das vezes".
gnasher729
5
Um bom valor seguro de tipo que transmite significado? Isso garante que as chaves apropriadas sejam usadas em um dicionário? Quando é um cheiro de código?
7
Sem contexto, penso uma formulação melhor seriaenums as switch statements might be a code smell ...
pllee

Respostas:

31

As enumerações destinam-se a casos de uso quando você literalmente enumerou todos os valores possíveis que uma variável poderia receber. Sempre. Pense em casos de uso como dias da semana ou meses do ano ou valores de configuração de um registro de hardware. Coisas que são altamente estáveis ​​e representáveis ​​por um valor simples.

Lembre-se de que, se você estiver criando uma camada anticorrupção, não poderá evitar ter uma declaração de opção em algum lugar , devido ao design que está envolto, mas se fizer isso corretamente, poderá limitá-la a esse local e use polimorfismo em outro lugar.

Karl Bielefeldt
fonte
3
Esse é o ponto mais importante - adicionar um valor extra a uma enumeração significa encontrar todo uso do tipo em seu código e potencialmente precisar adicionar uma nova ramificação para o novo valor. Compare com um tipo polimórfico, onde adicionar uma nova possibilidade significa simplesmente criar uma nova classe que implemente a interface - as alterações são concentradas em um único local, de maneira mais fácil de fazer. Se você tem certeza de que um novo tipo de marca nunca será adicionado, a enumeração é boa, caso contrário, ela deve ser encapsulada em uma interface polimórfica.
Jules
Essa é uma das respostas que eu estava procurando. Eu simplesmente não posso evitá-lo quando o método que estou agrupando usa um enum e o objetivo é centralizar esses enum em um só lugar. Eu tenho que fazer o wrapper de tal forma que, se alguma vez a API alterar essas enumerações, só preciso alterar o wrapper, e não os consumidores do wrapper.
Stromms
@Jules - "Se você tem certeza de que um novo tipo de tick nunca será adicionado ..." Essa declaração está errada em muitos níveis. Uma classe para cada tipo, apesar de cada tipo ter exatamente o mesmo comportamento, está o mais longe possível de uma abordagem "razoável". Não é até que você tenha comportamentos diferentes e comece a precisar de instruções "switch / if-then" onde a linha precisa ser desenhada. Certamente não se baseia se devo ou não adicionar mais valores enum no futuro.
Dunk
@ Dunk Eu acho que você não entendeu meu comentário. Eu nunca sugeri criar vários tipos com comportamento idêntico - isso seria loucura.
Jules
1
Mesmo se você "enumerou todos os valores possíveis", isso não significa que o tipo nunca terá funcionalidade adicional por instância. Mesmo algo como Month pode ter outras propriedades úteis, como número de dias. Usar uma enumeração imediatamente impede que o conceito que você está modelando seja estendido. É basicamente um mecanismo / padrão anti-OOP.
Dave Cousineau
17

Em primeiro lugar, um cheiro de código não significa que algo está errado. Isso significa que algo pode estar errado. enumcheira porque é frequentemente abusado, mas isso não significa que você precise evitá-los. Apenas você se encontra digitando enum, pare e procure por melhores soluções.

O caso específico que ocorre com mais freqüência é quando as diferentes enumerações correspondem a tipos diferentes com comportamentos diferentes, mas com a mesma interface. Por exemplo, conversando com back-end diferentes, renderizando páginas diferentes etc. Isso é muito mais implementado naturalmente usando classes polimórficas.

No seu caso, o TickType não corresponde a diferentes comportamentos. Eles são tipos diferentes de eventos ou propriedades diferentes do estado atual. Então eu acho que este é o lugar ideal para um enum.

Winston Ewert
fonte
Você está dizendo que quando as enumerações contêm substantivos como entradas (por exemplo, cores), elas provavelmente são usadas corretamente. E quando são verbos (Conectado, Renderização), é sinal de que está sendo mal utilizado?
Stromms
2
@ stromms, gramática é um péssimo guia para arquitetura de software. Se TickType fosse uma interface em vez de uma enumeração, quais seriam os métodos? Não vejo nada, é por isso que me parece um caso enum. Em outros casos, como status, cores, back-end, etc., posso criar métodos, e é por isso que eles são interfaces.
Winston Ewert
@WinstonEwert e com a edição, parece que eles são comportamentos diferentes.
Ohhh. Então, se eu puder pensar em um método para uma enumeração, ele pode ser candidato a uma interface? Esse pode ser um argumento muito bom.
stromms
1
@ stromms, você pode compartilhar mais exemplos de opções, para que eu tenha uma idéia melhor de como você usa o TickType?
Winston Ewert
8

Ao transmitir enums de dados, não há cheiro de código

IMHO, ao transmitir dados usando enumspara indicar que um campo pode ter um valor de um conjunto de valores restrito (que raramente muda) é bom.

Considero preferível transmitir arbitrariamente stringsou ints. Seqüências de caracteres podem causar problemas por variações de ortografia e letras maiúsculas. Ints permitir a transmissão de valores fora da faixa e têm pouca semântica (por exemplo, eu recebo 3de seu serviço de negociação, o que significa isso? LastPrice? LastQuantity? Algo mais?

Transmitir objetos e usar hierarquias de classes nem sempre é possível; por exemplo, o não permite que o destinatário receba a distinção de qual classe foi enviada.

No meu projeto, o serviço usa uma hierarquia de classes para efeitos de operações, pouco antes de transmitir através de um DataContractobjeto da hierarquia de classes ser copiado em um unionobjeto semelhante que contém um enumpara indicar o tipo. O cliente recebe DataContracte cria um objeto de uma classe em uma hierarquia usando o enumvalor switche cria um objeto do tipo correto.

Uma outra razão pela qual não se deseja transmitir objetos de classe é que o serviço pode exigir um comportamento completamente diferente para um objeto transmitido (como LastPrice) que o cliente. Nesse caso, enviar a classe e seus métodos é indesejável.

As instruções do switch são ruins?

IMHO, uma única switch declaração que chama diferentes construtores dependendo de um enumnão é um cheiro de código. Não é necessariamente melhor ou pior que outros métodos, como a base de reflexão em um nome de tipo; isso depende da situação real.

Ter interruptores em enumtodo o lugar é um cheiro de código, fornece alternativas que geralmente são melhores:

  • Use objeto de diferentes classes de uma hierarquia que substituíram métodos; isto é polimorfismo.
  • Use o padrão de visitante quando as classes na hierarquia raramente mudam e quando as (muitas) operações devem ser fracamente acopladas às classes na hierarquia.
Kasper van den Berg
fonte
Na verdade, o TickType está sendo transmitido através do fio como um int. Meu wrapper é aquele que usa o TickTypeque é convertido do recebido int. Vários eventos usam esse tipo de escala com assinaturas variadas e selvagens que são respostas a várias solicitações. É prática comum ter um uso intcontínuo para diferentes funções? por exemplo, TickPrice(int type, double value)usa 1,3 e 6 para typeenquanto TickSize(int type, double valueusa 2,4 e 5? Faz mesmo sentido separá-los em dois eventos?
stromms
2

e se você fosse com um tipo mais complexo:

    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

então você pode carregar seus tipos de reflexão ou construí-lo você mesmo, mas a principal coisa a ser feita aqui é que você está mantendo aberto para abrir o princípio próximo do SOLID

Chris
fonte
1

TL; DR

Para responder à pergunta, agora estou tendo dificuldade em pensar que as enums de tempo não são um cheiro de código em algum nível. Há uma certa intenção que eles declaram eficientemente (há um número limitado e limitado de possibilidades para esse valor), mas sua natureza inerentemente fechada os torna arquitetonicamente inferiores.

Com licença, refatorando toneladas do meu código legado. / suspiro; ^ D


Mas por que?

Muito boa revisão por que esse é o caso da LosTechies aqui :

// 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;
} 

Isso tem todos os mesmos problemas que o código acima. À medida que o aplicativo aumenta, as chances de ter instruções de ramificação semelhantes aumentam. Além disso, ao implementar mais serviços premium, você precisará modificar continuamente esse código, o que viola o Princípio Aberto-Fechado. Existem outros problemas aqui também. A função para calcular a taxa de serviço não deve precisar conhecer os valores reais de cada serviço. Essa é a informação que precisa ser encapsulada.

Um pequeno aspecto: as enumerações são uma estrutura de dados muito limitada. Se você não estiver usando uma enumeração para o que realmente é, um número inteiro rotulado, precisará de uma classe para modelar realmente a abstração corretamente. Você pode usar a incrível classe de enumeração de Jimmy para usar classes e também usá-las como rótulos.

Vamos refatorar isso para usar um comportamento polimórfico. O que precisamos é de abstração que nos permita conter o comportamento necessário para calcular a taxa de um serviço.

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

...

Agora podemos criar nossas implementações concretas da interface e anexá-las [à] conta.

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;
    }
} 

Outro caso de implementação ...

A linha inferior é que, se você tem um comportamento que depende dos valores da enumeração, por que não ter implementações diferentes de uma interface semelhante ou classe pai que garante que esse valor exista? No meu caso, estou vendo diferentes mensagens de erro com base nos códigos de status REST. Ao invés de...

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;
}

... talvez eu deva agrupar códigos de status nas aulas.

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

Cada vez que preciso de um novo tipo de IAmStatusResult, eu o codifico ...

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

... e agora posso garantir que o código anterior perceba que ele tem um IAmStatusResultescopo e faça referência ao seu, em entity.ErrorKeyvez do beco mais complicado _getErrorCode(403).

E, mais importante, toda vez que adiciono um novo tipo de valor de retorno, nenhum outro código precisa ser adicionado para lidar com isso . O que sabíamos, enume switchprovavelmente eram cheiros de código.

Lucro.

ruffin
fonte
E o caso em que, por exemplo, eu tenho classes polimórficas e quero configurar qual delas estou usando por meio de um parâmetro de linha de comando? Linha de comando -> enum -> switch / map -> implementação parece um caso de uso bastante legítimo. Eu acho que o principal é que o enum seja usado para orquestração, não como uma implementação da lógica condicional.
Ant P
@ Antt Por que não, neste caso, use o StatusResultvalor? Você poderia argumentar que o enum é útil como um atalho memorável para humanos nesse caso de uso, mas provavelmente eu ainda chamaria isso de cheiro de código, pois existem boas alternativas que não exigem a coleção fechada.
Ruffin
Que alternativas? Uma linha? Essa é uma distinção arbitrária da perspectiva de "enumerações devem ser substituídas por polimorfismos" - qualquer uma das abordagens requer o mapeamento de um valor para um tipo; evitar o enum nesse caso não alcança nada.
Ant P
@ AntP Não tenho certeza de onde os fios estão cruzando aqui. Se você referenciar uma propriedade em uma interface, poderá usá-la em qualquer lugar que seja necessário e nunca atualizar esse código quando criar uma nova implementação (ou remover uma implementação existente) dessa interface . Se você tem um enum, você não tem que código de atualização cada vez que você adicionar ou remover um valor, e potencialmente muitos lugares, dependendo de como você estruturou seu código. Usar polimorfismo em vez de um enum é como garantir que seu código esteja no formato normal Boyce-Codd .
Ruffin
Sim. Agora, suponha que você tenha N tais implementações polimórficas. No artigo Los Techies, eles estão simplesmente repetindo todos eles. Mas e se eu quiser aplicar condicionalmente apenas uma implementação com base em, por exemplo, parâmetros de linha de comando? Agora devemos definir um mapeamento de algum valor de configuração para algum tipo de implementação injetável. Um enum nesse caso é tão bom quanto qualquer outro tipo de configuração. Este é um exemplo de uma situação em que não há mais oportunidade de substituir o "enum sujo" por polimorfismo. Ramificar por abstração só pode levá-lo tão longe.
Ant P
0

Se o uso de uma enumeração é um cheiro de código ou não depende do contexto. Acho que você pode ter algumas idéias para responder sua pergunta se considerar o problema da expressão . Portanto, você tem uma coleção de diferentes tipos e uma coleção de operações e precisa organizar seu código. Existem duas opções simples:

  • Organize o código de acordo com as operações. Nesse caso, você pode usar uma enumeração para marcar tipos diferentes e ter uma instrução switch em cada procedimento que usa os dados marcados.
  • Organize o código de acordo com os tipos de dados. Nesse caso, você pode substituir a enumeração por uma interface e usar uma classe para cada elemento da enumeração. Você implementa cada operação como um método em cada classe.

Qual solução é melhor?

Se, como Karl Bielefeldt apontou, seus tipos são fixos e você espera que o sistema cresça principalmente adicionando novas operações nesses tipos, usar uma enumeração e ter uma instrução switch é uma solução melhor: sempre que você precisar de uma nova operação, basta implementar um novo procedimento, ao usar classes, você precisará adicionar um método a cada classe.

Por outro lado, se você espera ter um conjunto de operações bastante estável, mas acha que precisará adicionar mais tipos de dados ao longo do tempo, usar uma solução orientada a objetos é mais conveniente: à medida que novos tipos de dados devem ser implementados, você apenas continue adicionando novas classes implementando a mesma interface, enquanto que se você estivesse usando uma enum, teria que atualizar todas as instruções de opção em todos os procedimentos usando a enum.

Se você não conseguir classificar seu problema em uma das duas opções acima, poderá procurar soluções mais sofisticadas (consulte, por exemplo, novamente a página da Wikipedia) citada acima para uma breve discussão e algumas referências para leitura adicional).

Portanto, você deve tentar entender em que direção seu aplicativo pode evoluir e escolher uma solução adequada.

Como os livros que você se refere lidam com o paradigma orientado a objetos, não é de surpreender que sejam influenciados pelo uso de enums. No entanto, uma solução de orientação a objetos nem sempre é a melhor opção.

Bottomline: enums não são necessariamente um cheiro de código.

Giorgio
fonte
Observe que a pergunta foi feita no contexto de C #, uma linguagem OOP. Além disso, é interessante agrupar por operação ou por tipo dois lados da mesma moeda, mas não vejo um como "melhor" que o outro, como você descreve. a quantidade resultante de código é a mesma e os idiomas OOP já facilitam a organização por tipo. também produz um código substancialmente mais intuitivo (fácil de ler).
Dave Cousineau
@ DaveCousineau: "Eu não vejo um como sendo" melhor "que o outro como você descreve": eu não disse que um é sempre melhor que o outro. Eu disse que um é melhor que o outro, dependendo do contexto. Por exemplo, se as operações são mais ou menos fixo e você planeja adicionar novos tipos (por exemplo, uma interface gráfica com o fixo paint(), show(), close() resize()operações e widgets personalizados), em seguida, a abordagem orientada a objetos é melhor no sentido de que permite adicionar um novo tipo sem afetar demais muito código existente (você basicamente implementa uma nova classe, que é uma alteração local).
Giorgio
Eu também não vejo como "melhor", como você descreveu o que significa que não vejo que isso dependa da situação. A quantidade de código que você precisa escrever é exatamente a mesma nos dois cenários. A única diferença é que uma maneira é contrária à OOP (enumeração) e outra não.
Dave Cousineau
@DaveCousineau: a quantidade de código que você precisa escrever é provavelmente a mesma, a localidade (lugares no código-fonte que você precisa alterar e recompilar) muda drasticamente. Por exemplo, no OOP, se você adicionar um novo método a uma interface, precisará adicionar uma implementação para cada classe que implementa essa interface.
Giorgio
Não é apenas uma questão de profundidade VS profundidade? Adicionando 1 método a 10 arquivos ou 10 métodos a 1 arquivo.
Dave Cousineau