As enums criam interfaces quebradiças?


17

Considere o exemplo abaixo. Qualquer alteração na enumeração ColorChoice afeta todas as subclasses IWindowColor.

As enums tendem a causar interfaces quebradiças? Existe algo melhor que um enum para permitir mais flexibilidade polimórfica?

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

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

Edit: desculpe por usar cores como meu exemplo, não é disso que se trata. Aqui está um exemplo diferente que evita o arenque vermelho e fornece mais informações sobre o que quero dizer com flexibilidade.

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

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

Além disso, imagine que os identificadores da subclasse ISomethingThatNeedsToKnowWhatTypeOfCharacter apropriado sejam distribuídos por um padrão de design de fábrica. Agora eu tenho uma API que não pode ser estendida no futuro para um aplicativo diferente em que os tipos de caracteres permitidos sejam {human, dwarf}.

Edit: Apenas para ser mais concreto sobre o que estou trabalhando. Estou projetando uma forte ligação a essa especificação ( MusicXML ) e estou usando classes enum para representar os tipos na especificação que são declarados com xs: enumeração. Estou tentando pensar no que acontece quando a próxima versão (4.0) for lançada. Minha biblioteca de classes poderia funcionar no modo 3.0 e no modo 4.0? Se a próxima versão for 100% compatível com versões anteriores, talvez seja. Mas se os valores de enumeração foram removidos da especificação, eu estou morto na água.


2
Quando você diz "flexibilidade polimórfica", quais recursos você tem em mente exatamente?
Ixrec 16/03/2015


3
O uso de uma enumeração para cores cria interfaces quebradiças, não apenas "a utilização de uma enumeração".
Doc Brown

3
A adição de uma nova variante de enumeração quebra o código usando essa enumeração. A adição de uma nova operação a uma enumeração é bastante independente, por outro lado, pois todos os casos que precisam ser tratados estão ali (contraste com interfaces e superclasses, onde a adição de um método não padrão é uma mudança séria). Depende do tipo de mudanças que são realmente necessárias.

1
Re MusicXML: Se não há uma maneira fácil de dizer a partir dos arquivos XML qual versão do esquema cada um está usando, isso me parece uma falha crítica de design na especificação. Se você precisar contorná-lo de alguma forma, provavelmente não há como ajudarmos até sabermos exatamente o que eles escolherão quebrar no 4.0 e você poderá perguntar sobre um problema específico que ele causa.
Ixrec 16/03/2015

Respostas:


25

Quando usadas corretamente, as enumerações são muito mais legíveis e robustas do que os "números mágicos" que substituem. Normalmente, não os vejo tornando o código mais frágil. Por exemplo:

  • setColor () não precisa perder tempo verificando se valueé ou não um valor de cor válido. O compilador já fez isso.
  • Você pode escrever setColor (Color :: Red) em vez de setColor (0). Acredito que o enum classrecurso em C ++ moderno ainda permite que você force as pessoas a escrever sempre o primeiro em vez do último.
  • Geralmente, isso não é importante, mas a maioria das enums pode ser implementada com qualquer tipo de tamanho integral, para que o compilador possa escolher o tamanho que for mais conveniente sem forçar você a pensar sobre essas coisas.

No entanto, o uso de uma enumeração para cores é questionável, porque em muitas (a maioria?) Situações não há razão para limitar o usuário a um conjunto tão pequeno de cores; é melhor deixá-los passar valores RGB arbitrários. Nos projetos com os quais trabalho, uma pequena lista de cores como essa só surgiria como parte de um conjunto de "temas" ou "estilos" que deveriam atuar como uma fina abstração sobre cores concretas.

Não sei ao certo qual é a sua pergunta sobre "flexibilidade polimórfica". As enums não possuem código executável; portanto, não há nada a fazer polimórfico. Talvez você esteja procurando pelo padrão de comando ?

Edit: Pós-edição, ainda não estou claro sobre que tipo de extensibilidade você está procurando, mas ainda acho que o padrão de comando é a coisa mais próxima que você pode obter de um "enum polimórfico".


Onde posso descobrir mais sobre como não permitir que 0 passe como enum?
TankorSmash

5
O @TankorSmash C ++ 11 introduziu a "classe enum", também chamada de "enums com escopo definido", que não pode ser implicitamente convertida em seu tipo numérico subjacente. Eles também evitam poluir o espaço para nome como os antigos tipos de "enum" no estilo C.
Matthew James Briggs

2
As enums geralmente são apoiadas por números inteiros. Há muitas coisas estranhas que podem acontecer na serialização / desserialização ou conversão entre números inteiros e enumerações. Nem sempre é seguro assumir que uma enumeração sempre terá um valor válido.
Eric

1
Você está certo, Eric (e esse é um problema que eu já encontrei várias vezes). No entanto, você só precisa se preocupar com um valor possivelmente inválido no estágio de desserialização. Nas outras vezes em que você usa enumerações, você pode assumir que o valor é válido (pelo menos para enumerações em idiomas como Scala - alguns idiomas não têm verificação de tipo muito forte para enumerações).
Kat

1
@Ixrec "porque em muitas situações (a maioria?) Não há razão para limitar o usuário a um conjunto tão pequeno de cores" Existem casos legítimos. O .Net Console emula o console do Windows antigo, que só pode ter texto em uma das 16 cores (as 16 cores do padrão CGA). Msdn.microsoft.com/en-us/library/…
Pharap

15

Qualquer alteração na enumeração ColorChoice afeta todas as subclasses IWindowColor.

Não, não faz. Existem dois casos: os implementadores irão

  • armazenar, retornar e encaminhar valores de enumeração, nunca operando neles, caso em que não são afetados por alterações na enumeração, ou

  • operar em valores de enum individuais; nesse caso, qualquer alteração no enum deve, naturalmente, naturalmente, inevitavelmente, necessariamente , ser contabilizada com uma alteração correspondente na lógica do implementador.

Se você colocar "Esfera", "Retângulo" e "Pirâmide" em uma enumeração "Shape" e passar essa enumeração para alguma drawSolid()função que você escreveu para desenhar o sólido correspondente, e então uma manhã você decide adicionar uma " Ikositetrahedron "para o enum, você não pode esperar que a drawSolid()função permaneça inalterada; Se você esperava que, de alguma forma, desenhasse icositetrahedrons sem precisar primeiro escrever o código real para desenhar icositetrahedrons, a culpa é sua, não a culpa da enum. Então:

As enums tendem a causar interfaces quebradiças?

Não, eles não. O que causa interfaces quebradiças são os programadores que se consideram ninjas, tentando compilar seu código sem ter avisos suficientes ativados. Em seguida, o compilador não avisa que sua drawSolid()função contém uma switchinstrução que está faltando uma casecláusula para o valor de enumeração "Ikositetrahedron" recém-adicionado.

A maneira como ele deve funcionar é análoga à adição de um novo método virtual puro a uma classe base: você precisa implementar esse método em todos os herdeiros, caso contrário, o projeto não cria, nem deve construir.


Agora, verdade seja dita, enums não são uma construção orientada a objetos. Eles são mais um compromisso pragmático entre o paradigma orientado a objetos e o paradigma de programação estruturada.

A maneira pura e orientada a objetos de fazer as coisas não é ter enumerações, e sim objetos a todo momento.

Portanto, a maneira puramente orientada a objetos de implementar o exemplo com os sólidos é, obviamente, usar o polimorfismo: em vez de ter um único método monstruoso centralizado que sabe desenhar tudo e que precisa saber qual sólido desenhar, você declara " Classe "sólida" com um draw()método abstrato (virtual puro) e, em seguida, você adiciona as subclasses "Esfera", "Retângulo" e "Pirâmide", cada uma com sua própria implementação, draw()que sabe como se desenhar.

Dessa forma, quando você introduzir a subclasse "Ikositetrahedron", você precisará fornecer uma draw()função para ela, e o compilador o lembrará de fazer isso, não permitindo que você instale o "Icositetrahedron" caso contrário.


Alguma dica sobre como lançar um aviso de tempo de compilação para esses comutadores? Eu normalmente apenas lancei exceções de tempo de execução; mas o tempo de compilação pode ser ótimo! Não é tão bom .. mas os testes de unidade vêm à mente.
Vaughan Empunhaduras

1
Já faz um tempo desde a última vez que usei o C ++, por isso não tenho certeza, mas espero que o fornecimento do parâmetro -Wall ao compilador e a omissão da default:cláusula o façam. Uma pesquisa rápida sobre o assunto não produziu resultados mais definitivos; portanto, isso pode ser adequado como o assunto de outra programmers SEpergunta.
quer

No C ++, pode haver uma verificação do tempo de compilação, se você ativá-lo. Em C #, qualquer verificação teria que ser em tempo de execução. Sempre que eu uso um enum não sinalizador como parâmetro, certifique-se de validá-lo com Enum.IsDefined, mas isso ainda significa que é necessário atualizar todos os usos do enum manualmente quando você adiciona um novo valor. Veja: stackoverflow.com/questions/12531132/…
Mike suporta Monica

1
Eu espero que um programador orientado a objetos nunca faça seu objeto de transferência de dados, como Pyramidrealmente saiba como fazer draw()uma pirâmide. Na melhor das hipóteses, pode derivar Solide ter umaGetTriangles() método, e você pode passá-lo para um SolidDrawerserviço. Eu pensei que estávamos nos afastando dos exemplos de objetos físicos como exemplos de objetos no POO.
Scott Whitlock

1
Está tudo bem, só não gostei de ninguém baseando a boa e velha programação orientada a objetos. :) Especialmente porque a maioria de nós combina programação funcional e POO mais do que estritamente OOP.
21315 Scott Scottlock

14

Enums não criam interfaces quebradiças. O uso indevido de enums faz.

Para que servem enums?

As enums são projetadas para serem usadas como conjuntos de constantes nomeadas de forma significativa. Eles devem ser usados ​​quando:

  • Você sabe que nenhum valor será removido.
  • (E) Você sabe que é altamente improvável que um novo valor seja necessário.
  • (Ou) Você aceita que um novo valor será necessário, mas raramente o suficiente para garantir a correção de todo o código que quebra por causa dele.

Bons usos de enums:

  • Dias da semana: (conforme .Net System.DayOfWeek) A menos que você esteja lidando com um calendário incrivelmente obscuro, haverá apenas 7 dias da semana.
  • Cores não extensíveis: (conforme o .Net System.ConsoleColor) Alguns podem discordar disso, mas o .Net optou por fazer isso por um motivo. No sistema de console da .Net, existem apenas 16 cores disponíveis para uso pelo console. Essas 16 cores correspondem à paleta de cores herdada conhecida como CGA ou 'Color Graphics Adapter' . Nenhum novo valor será adicionado, o que significa que essa é de fato uma aplicação razoável de um enum.
  • Enumerações representando estados fixos: (conforme Java Thread.State) Os designers de Java decidiram que no modelo de encadeamento Java apenas haverá um conjunto fixo de estados nos quais a Threadpode estar e, para simplificar, esses diferentes estados são representados como enumerações . Isso significa que muitas verificações baseadas em estado são simples ifs e switches que, na prática, operam com valores inteiros, sem que o programador precise se preocupar com o que os valores realmente são.
  • Bitflags representando opções não mutuamente exclusivas: (conforme .Net System.Text.RegularExpressions.RegexOptions) Os bitflags são um uso muito comum de enumerações. Tão comum, de fato, que em .Net todas as enumerações têm um HasFlag(Enum flag)método incorporado. Elas também suportam os operadores bit a bit e há umaFlagsAttribute necessário marcar uma enumeração como destinada a ser usada como um conjunto de bitflags. Usando uma enum como um conjunto de sinalizadores, você pode representar um grupo de valores booleanos em um único valor, além de ter os sinalizadores claramente nomeados por conveniência. Isso seria extremamente benéfico para representar os sinalizadores de um registro de status em um emulador ou representar as permissões de um arquivo (leitura, gravação, execução) ou praticamente qualquer situação em que um conjunto de opções relacionadas não seja mutuamente exclusivo.

Maus usos de enums:

  • Classes / tipos de personagens em um jogo: A menos que o jogo seja uma demonstração única que é improvável que você use novamente, as enumerações não devem ser usadas nas classes de personagens, porque é provável que você queira adicionar muito mais classes. É melhor ter uma única classe representando o personagem e ter um tipo de personagem no jogo representado de outra forma. Uma maneira de lidar com isso é o TypeObject padrão ; outras soluções incluem o registro de tipos de caracteres com um dicionário / registro de tipo ou uma mistura dos dois.
  • Cores extensíveis: se você estiver usando um enum para cores que poderão ser adicionadas posteriormente, não é uma boa ideia representá-lo na forma de enum, caso contrário, você ficará para sempre adicionando cores. Isso é semelhante ao problema acima, portanto, uma solução semelhante deve ser usada (ou seja, uma variação do TypeObject).
  • Estado extensível: se você possui uma máquina de estados que pode acabar introduzindo muitos outros estados, não é uma boa ideia representá-los por meio de enumerações. O método preferido é definir uma interface para o estado da máquina, fornecer uma implementação ou classe que agrupe a interface e delegue suas chamadas de método (semelhante ao padrão Estratégia ) e, em seguida, altere seu estado alterando a implementação fornecida atualmente.
  • Bitflags representando opções mutuamente exclusivas: se você estiver usando enumerações para representar sinalizadores e dois desses sinalizadores nunca ocorrerem juntos, você terá atingido o pé. Qualquer coisa programada para reagir a um dos sinalizadores reagirá repentinamente a qualquer sinalizador programado para responder primeiro - ou pior, pode responder a ambos. Esse tipo de situação está apenas pedindo problemas. A melhor abordagem é tratar a ausência de uma bandeira como condição alternativa, se possível (isto é, a ausência da Truebandeira implica False). Esse comportamento pode ser ainda mais suportado pelo uso de funções especializadas (ie IsTrue(flags)e IsFalse(flags)).

Adicionarei exemplos de bitflag se alguém puder encontrar um exemplo funcional ou bem conhecido de enumerações sendo usadas como bitflags. Estou ciente de que eles existem, mas infelizmente não consigo me lembrar de nada no momento.
Pharap


Excelente exemplo. Não posso dizer que já os usei, mas eles existem por um motivo.
Pharap

4

As enums são uma grande melhoria em relação aos números de identificação mágica para conjuntos fechados de valores que não possuem muitas funcionalidades associadas a eles. Geralmente você não se importa com o número realmente associado ao enum; Nesse caso, é fácil estender adicionando novas entradas no final, sem fragilidade.

O problema é quando você possui uma funcionalidade significativa associada à enumeração. Ou seja, você tem esse tipo de código:

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

Um ou dois deles está ok, mas assim que você tem esse tipo de enum significativo, essas switchdeclarações tendem a se multiplicar como tribbles. Agora, toda vez que você estende a enumeração, precisa caçar todas as switches associadas para garantir que cobriu tudo. Isso é quebradiço. Fontes como o Clean Code sugerem que você tenha umswitch por enum, no máximo.

Nesse caso, o que você deve fazer é usar os princípios de OO e criar uma interface para esse tipo. Você ainda pode manter a enumeração para comunicar esse tipo, mas assim que precisar fazer alguma coisa, cria um objeto associado à enumeração, possivelmente usando um Factory. Isso é muito mais fácil de manter, pois você só precisa procurar um local para atualizar: sua Fábrica e adicionando uma nova classe.


1

Se você for cuidadoso em como usá-los, eu não consideraria as enums prejudiciais. Mas há algumas coisas a considerar se você deseja usá-las em algum código da biblioteca, em oposição a um único aplicativo.

  1. Nunca remova ou reordene valores. Se em algum momento você tiver um valor enum em sua lista, esse valor deverá ser associado a esse nome por toda a eternidade. Se você quiser, em algum momento poderá renomear valores para o que deprecated_orcquer que seja, mas, ao não removê-los, você poderá manter a compatibilidade com mais facilidade com o código antigo compilado com o conjunto antigo de enumerações. Se algum código novo não puder lidar com constantes enum antigas, gere um erro adequado lá ou verifique se esse valor não atinge esse trecho de código.

  2. Não faça aritmética neles. Em particular, não faça comparações de pedidos. Por quê? Porque então você pode encontrar uma situação em que não pode manter os valores existentes e preservar uma ordenação são ao mesmo tempo. Tomemos, por exemplo, um enum para as direções da bússola: N = 0, NE = 1, E = 2, SE = 3,… Agora, se após alguma atualização você incluir NNE e assim por diante entre as direções existentes, você poderá adicioná-las ao final da lista e, portanto, quebre a ordem ou você as intercale com as chaves existentes e, assim, quebre os mapeamentos estabelecidos usados ​​no código legado. Ou você descontinua todas as chaves antigas e possui um conjunto de chaves completamente novo, junto com algum código de compatibilidade que se traduz entre chaves antigas e novas por causa do código legado.

  3. Escolha um tamanho suficiente. Por padrão, o compilador usará o menor número inteiro que pode conter todos os valores de enumeração. O que significa que se, em alguma atualização, seu conjunto de enumerações possíveis se expandir de 254 para 259, você precisará repentinamente de 2 bytes para cada valor de enumeração, em vez de um. Isso pode danificar os layouts de estrutura e classe em todo o lugar, portanto, evite isso usando um tamanho suficiente no primeiro design. O C ++ 11 oferece muito controle aqui, mas especificar uma entrada também LAST_SPECIES_VALUE=65535deve ajudar.

  4. Tenha um registro central. Como você deseja corrigir o mapeamento entre nomes e valores, é ruim se você permitir que usuários de seu código adicionem novas constantes. Nenhum projeto usando seu código deve ter permissão para alterar esse cabeçalho para adicionar novos mapeamentos. Em vez disso, eles devem incomodá-lo para adicioná-los. Isso significa que, para o exemplo humano e anão que você mencionou, as enums são de fato um ajuste inadequado. Lá, seria melhor ter algum tipo de registro em tempo de execução, em que os usuários do seu código possam inserir uma string e recuperar um número único, agradavelmente envolvido em algum tipo opaco. O "número" pode até ser um ponteiro para a string em questão, não importa.

Apesar do fato de que o meu último ponto acima faz da enums um ajuste inadequado para sua situação hipotética, sua situação real em que algumas especificações podem mudar e você teria que atualizar algum código parece se encaixar muito bem com um registro central da sua biblioteca. Portanto, as enums devem ser apropriadas se você levar minhas outras sugestões a sério.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.