¿Las enumeraciones crean interfaces frágiles?

17

Considere el siguiente ejemplo. Cualquier cambio en la enumeración ColorChoice afecta a todas las subclases IWindowColor.

¿Las enumeraciones tienden a causar interfaces frágiles? ¿Hay algo mejor que una enumeración para permitir una mayor flexibilidad polimórfica?

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

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

Editar: perdón por usar el color como mi ejemplo, de eso no se trata la pregunta. Aquí hay un ejemplo diferente que evita el arenque rojo y proporciona más información sobre lo que quiero decir con flexibilidad.

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

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

Además, imagine que los manejadores de la subclase ISomethingThatNeedsToKnowWhatTypeOfCharacter apropiada se entregan mediante un patrón de diseño de fábrica. Ahora tengo una API que no se puede ampliar en el futuro para una aplicación diferente donde los tipos de caracteres permitidos son {humanos, enanos}.

Editar: Solo para ser más concreto sobre lo que estoy trabajando. Estoy diseñando un enlace fuerte de esta especificación ( MusicXML ) y estoy usando clases enum para representar esos tipos en la especificación que se declaran con xs: enumeration. Estoy tratando de pensar en lo que sucede cuando salga la próxima versión (4.0). ¿Podría mi biblioteca de clases funcionar en modo 3.0 y en modo 4.0? Si la próxima versión es 100% compatible con versiones anteriores, entonces tal vez. Pero si los valores de enumeración se eliminaron de la especificación, entonces estoy muerto en el agua.

Matthew James Briggs
fuente
2
Cuando dices "flexibilidad polimórfica", ¿qué capacidades tienes en mente exactamente?
Ixrec
3
Usar una enumeración para colores crea interfaces quebradizas, no solo "usar una enumeración".
Doc Brown
3
Agregar una nueva variante enum rompe el código usando esa enumeración. Agregar una nueva operación a una enumeración es bastante autónomo, por otro lado, ya que todos los casos que deben manejarse están allí (contraste esto con interfaces y superclases, donde agregar un método no predeterminado es un cambio importante). Depende del tipo de cambios que sean realmente necesarios.
1
Re MusicXML: si no hay una manera fácil de distinguir de los archivos XML qué versión del esquema está usando cada uno, eso me parece un defecto de diseño crítico en la especificación. Si tiene que solucionarlo de alguna manera, probablemente no hay forma de que lo ayudemos hasta que sepamos exactamente lo que elegirán dividir en 4.0 y puede preguntar sobre un problema específico que causa.
Ixrec

Respuestas:

25

Cuando se usan correctamente, las enumeraciones son mucho más legibles y robustas que los "números mágicos" que reemplazan. Normalmente no los veo haciendo que el código sea más frágil. Por ejemplo:

  • setColor () no tiene que perder el tiempo comprobando si valuees un valor de color válido o no. El compilador ya lo ha hecho.
  • Puede escribir setColor (Color :: Red) en lugar de setColor (0). Creo que la enum classfunción en C ++ moderno incluso le permite obligar a las personas a escribir siempre el primero en lugar del segundo.
  • Por lo general, no es importante, pero la mayoría de las enumeraciones se pueden implementar con cualquier tipo de tamaño integral, por lo que el compilador puede elegir el tamaño que sea más conveniente sin obligarlo a pensar en esas cosas.

Sin embargo, el uso de una enumeración para el color es cuestionable porque en muchas situaciones (¿la mayoría?) No hay razón para limitar al usuario a un conjunto de colores tan pequeño; también podría dejarlos pasar cualquier valor RGB arbitrario. En los proyectos con los que trabajo, una pequeña lista de colores como esta solo aparecería como parte de un conjunto de "temas" o "estilos" que se supone que actúan como una fina abstracción sobre colores concretos.

No estoy seguro de cuál es su pregunta de "flexibilidad polimórfica". Las enumeraciones no tienen ningún código ejecutable, por lo que no hay nada que hacer polimórfico. ¿Quizás estás buscando el patrón de comando ?

Editar: después de editar, todavía no tengo claro qué tipo de capacidad de ampliación está buscando, pero sigo pensando que el patrón de comando es lo más parecido a una "enumeración polimórfica".

Ixrec
fuente
¿Dónde puedo encontrar más información sobre no permitir que pase 0 como enumeración?
TankorSmash
55
@TankorSmash C ++ 11 introdujo la "clase enum", también llamada "enumeraciones con ámbito" que no se puede convertir implícitamente a su tipo numérico subyacente. También evitan contaminar el espacio de nombres como lo hacían los viejos tipos "enum" de estilo C.
Matthew James Briggs
2
Las enumeraciones suelen estar respaldadas por enteros. Hay muchas cosas extrañas que pueden suceder en la serialización / deserialización o conversión entre enteros y enumeraciones. No siempre es seguro asumir que una enumeración siempre tendrá un valor válido.
Eric
1
Tienes razón Eric (y este es un problema que me he encontrado varias veces). Sin embargo, solo debe preocuparse por un valor posiblemente no válido en la etapa de deserialización. Todas las otras veces que use enumeraciones, puede suponer que el valor es válido (al menos para enumeraciones en idiomas como Scala; algunos idiomas no tienen una verificación de tipo muy fuerte para enumeraciones).
Kat
1
@Ixrec "porque en muchas situaciones (¿la mayoría?) No hay razón para limitar al usuario a un conjunto de colores tan pequeño" Hay casos legítimos. .Net Console emula la consola de Windows de estilo antiguo, que solo puede tener texto en uno de los 16 colores (los 16 colores del estándar CGA) msdn.microsoft.com/en-us/library/…
Pharap
15

Cualquier cambio en la enumeración ColorChoice afecta a todas las subclases IWindowColor.

No, no lo hace. Hay dos casos: los implementadores

  • almacenar, devolver y reenviar valores de enumeración, nunca operando en ellos, en cuyo caso no se ven afectados por los cambios en la enumeración, o

  • operar con valores de enumeración individuales, en cuyo caso cualquier cambio en la enumeración debe, naturalmente, inevitablemente, necesariamente , explicarse con un cambio correspondiente en la lógica del implementador.

Si coloca "Esfera", "Rectángulo" y "Pirámide" en una enumeración de "Forma", y pasa dicha enumeración a alguna drawSolid()función que ha escrito para dibujar el sólido correspondiente, y luego una mañana decide agregar un " Ikositetrahedron "valor para la enumeración, no se puede esperar que la drawSolid()función no se vea afectada; Si esperaba que de alguna manera dibujara icositetraedros sin tener que escribir primero el código real para dibujar icositetraedros, es su culpa, no la culpa de la enumeración. Entonces:

¿Las enumeraciones tienden a causar interfaces frágiles?

No, ellos no. Lo que causa las interfaces frágiles es que los programadores piensan en sí mismos como ninjas, tratando de compilar su código sin tener suficientes advertencias habilitadas. Luego, el compilador no les advierte que su drawSolid()función contiene una switchdeclaración a la que le falta una casecláusula para el nuevo valor de enumeración "Ikositetrahedron".

La forma en que se supone que funciona es análoga a la adición de un nuevo método virtual puro a una clase base: luego debe implementar este método en cada heredero, de lo contrario, el proyecto no se construye, y no debería.


Ahora, la verdad sea dicha, las enumeraciones no son una construcción orientada a objetos. Son más un compromiso pragmático entre el paradigma orientado a objetos y el paradigma de programación estructurada.

La forma pura de hacer las cosas orientada a objetos es no tener enumeraciones en absoluto y, en cambio, tener objetos todo el camino.

Por lo tanto, la forma puramente orientada a objetos para implementar el ejemplo con los sólidos es, por supuesto, utilizar el polimorfismo: en lugar de tener un único método centralizado monstruoso que sepa dibujar todo y tener que saber qué sólido dibujar, declaras un " Sólida "clase con un draw()método abstracto (puro virtual) , y luego agrega las subclases" Esfera "," Rectángulo "y" Pirámide ", cada una con su propia implementación de las draw()cuales sabe cómo dibujarse.

De esta manera, cuando introduce la subclase "Ikositetrahedron", solo tendrá que proporcionarle una draw()función, y el compilador le recordará que haga eso, al no permitirle instanciar "Icositetrahedron" de lo contrario.

Mike Nakis
fuente
¿Algún consejo para lanzar una advertencia de tiempo de compilación para esos interruptores? Por lo general, solo lanzo excepciones de tiempo de ejecución; ¡pero el tiempo de compilación podría ser genial! No es tan bueno ... pero me vienen a la mente las pruebas unitarias.
Vaughan Hilts
1
Ha pasado un tiempo desde la última vez que usé C ++, por lo que no estoy seguro, pero esperaría que al proporcionar el parámetro -Wall al compilador y omitir la default:cláusula debería hacerlo. Una búsqueda rápida sobre el tema no arrojó más resultados definitivos, por lo que esto podría ser adecuado como tema de otra programmers SEpregunta.
Mike Nakis
En C ++ puede haber una comprobación de tiempo de compilación si lo habilita. En C #, cualquier verificación debería realizarse en tiempo de ejecución. Cada vez que tomo una enumeración sin marca como parámetro, me aseguro de validarla con Enum.IsDefined, pero eso aún significa que debe actualizar todos los usos de la enumeración manualmente cuando agrega un nuevo valor. Ver: stackoverflow.com/questions/12531132/…
Mike apoya a Monica el
1
Espero que un programador orientado a objetos nunca haga su objeto de transferencia de datos, como Pyramidsaber realmente draw()una pirámide. En el mejor de los casos, podría derivar Solidy tener un GetTriangles()método y podría pasarlo a un SolidDrawerservicio. Pensé que nos estábamos alejando de los ejemplos de objetos físicos como ejemplos de objetos en OOP.
Scott Whitlock
1
Está bien, simplemente no me gustaba que nadie criticara la buena programación orientada a objetos. :) Especialmente porque la mayoría de nosotros combinamos programación funcional y OOP más que estrictamente OOP.
Scott Whitlock
14

Las enumeraciones no crean interfaces frágiles. El mal uso de las enumeraciones sí.

¿Para qué son las enumeraciones?

Las enumeraciones están diseñadas para usarse como conjuntos de constantes con nombres significativos. Deben usarse cuando:

  • Sabes que no se eliminarán valores.
  • (Y) Usted sabe que es muy poco probable que se necesite un nuevo valor.
  • (O) Usted acepta que se necesitará un nuevo valor, pero rara vez lo suficiente como para garantizar la reparación de todo el código que se rompe debido a él.

Buenos usos de las enumeraciones:

  • Días de la semana: (según .Net's System.DayOfWeek) A menos que esté lidiando con un calendario increíblemente oscuro, solo habrá 7 días de la semana.
  • Colores no extensibles: (según .Net System.ConsoleColor) Algunos pueden estar en desacuerdo con esto, pero .Net eligió hacer esto por una razón. En el sistema de consola de .Net, solo hay 16 colores disponibles para su uso por la consola. Estos 16 colores corresponden a la paleta de colores heredada conocida como CGA o 'Adaptador de gráficos en color' . Nunca se agregarán nuevos valores, lo que significa que, de hecho, esta es una aplicación razonable de una enumeración.
  • Enumeraciones que representan estados fijos: (según Java Thread.State) Los diseñadores de Java decidieron que en el modelo de subprocesamiento de Java solo habrá un conjunto fijo de estados en los que Threadpueda estar, y para simplificar las cosas, estos diferentes estados se representan como enumeraciones . Esto significa que muchas comprobaciones basadas en estado son simples ifs e interruptores que en la práctica funcionan con valores enteros, sin que el programador tenga que preocuparse por cuáles son realmente los valores.
  • Banderas de bits que representan opciones no excluyentes entre sí: (según .Net System.Text.RegularExpressions.RegexOptions) Las banderas de bits son un uso muy común de las enumeraciones. De hecho, es tan común que en .Net todas las enumeraciones tienen un HasFlag(Enum flag)método incorporado. También admiten los operadores bit a bit y hay FlagsAttributeque marcar una enumeración como destinada a ser utilizada como un conjunto de banderas de bits. Al usar una enumeración como un conjunto de indicadores, puede representar un grupo de valores booleanos en un solo valor, así como tener los indicadores claramente nombrados por conveniencia. Esto sería extremadamente beneficioso para representar los indicadores de un registro de estado en un emulador o para representar los permisos de un archivo (leer, escribir, ejecutar), o casi cualquier situación en la que un conjunto de opciones relacionadas no sean mutuamente excluyentes.

Malos usos de las enumeraciones:

  • Clases / tipos de personajes en un juego: a menos que el juego sea una demostración de un solo disparo que es poco probable que uses de nuevo, las enumeraciones no se deben usar para las clases de personajes porque es probable que quieras agregar muchas más clases. Es mejor tener una sola clase que represente al personaje y tener un 'tipo' de personaje en el juego representado de otra manera. Una forma de manejar esto es el patrón TypeObject , otras soluciones incluyen el registro de tipos de caracteres con un diccionario / registro de tipos, o una combinación de ambos.
  • Colores extensibles: si está usando una enumeración para colores que luego se pueden agregar, no es una buena idea representar esto en forma de enumeración, de lo contrario, se quedará agregando colores para siempre. Esto es similar al problema anterior, por lo tanto, se debe utilizar una solución similar (es decir, una variación de TypeObject).
  • Estado extensible: si tiene una máquina de estados que puede terminar introduciendo muchos más estados, no es una buena idea representar estos estados mediante enumeraciones. El método preferido es definir una interfaz para el estado de la máquina, proporcionar una implementación o clase que envuelva la interfaz y delegue sus llamadas al método (similar al patrón de Estrategia ), y luego altera su estado cambiando qué implementación siempre está activa.
  • Banderas de bits que representan opciones mutuamente excluyentes: si está utilizando enumeraciones para representar banderas y dos de esas banderas nunca deberían aparecer juntas, entonces se ha disparado en el pie. Cualquier cosa que esté programada para reaccionar a una de las banderas reaccionará repentinamente a la bandera a la que esté programada para responder primero, o peor aún, podría responder a ambas. Este tipo de situación solo está buscando problemas. El mejor enfoque es tratar la ausencia de una bandera como la condición alternativa si es posible (es decir, la ausencia de la Truebandera implica False). Este comportamiento se puede respaldar aún más mediante el uso de funciones especializadas (es decir, IsTrue(flags)y IsFalse(flags)).
Pharap
fuente
Agregaré ejemplos de bitflag si alguien puede encontrar un ejemplo funcional o bien conocido de enumeraciones que se usan como bitflags. Soy consciente de que existen, pero desafortunadamente no puedo recordar ninguno en este momento.
Pharap
1
.NET's RegexOptions msdn.microsoft.com/en-us/library/…
BLSully
@BLSully Excelente ejemplo. No puedo decir que los haya usado alguna vez, pero existen por alguna razón.
Pharap
4

Las enumeraciones son una gran mejora sobre los números de identificación mágicos para conjuntos cerrados de valores que no tienen mucha funcionalidad asociada a ellos. Por lo general, no le importa qué número está realmente asociado con la enumeración; en este caso, es fácil de extender agregando nuevas entradas al final, no debería producirse fragilidad.

El problema es cuando tiene una funcionalidad significativa asociada con la enumeración. Es decir, tienes este tipo de código por ahí:

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

Uno o dos de estos están bien, pero tan pronto como tenga este tipo de enumeración significativa, estas switchdeclaraciones tienden a multiplicarse como tribbles. Ahora, cada vez que extienda la enumeración, debe buscar todos los asociadosswitch es para asegurarse de haber cubierto todo. Esto es frágil Fuentes como Clean Code le sugerirán que debe tener una switchpor enumeración, como máximo.

En este caso, lo que debe hacer es utilizar los principios OO y crear un interfaz para este tipo. Aún puede mantener la enumeración para comunicar este tipo, pero tan pronto como necesite hacer algo con ella, creará un objeto asociado con la enumeración, posiblemente utilizando una Fábrica. Esto es mucho más fácil de mantener, ya que solo necesita buscar un lugar para actualizar: su Fábrica y agregar una nueva clase.

congusbongus
fuente
1

Si tienes cuidado en cómo los usas, no consideraría las enumeraciones dañinas. Pero hay algunas cosas a considerar si desea usarlas en algún código de biblioteca, en lugar de una sola aplicación.

  1. Nunca elimine ni reordene valores. Si en algún momento tiene un valor de enumeración en su lista, ese valor debe asociarse con ese nombre por toda la eternidad. Si lo desea, en algún momento puede cambiar el nombre de los valores deprecated_orco lo que sea, pero al no eliminarlos, puede mantener más fácilmente la compatibilidad con el código antiguo compilado contra el conjunto de enumeraciones anterior. Si algún código nuevo no puede ocuparse de las constantes enum antiguas, genere un error adecuado allí o asegúrese de que dicho valor no llegue a ese fragmento de código.

  2. No hagas aritmética en ellos. En particular, no hagas comparaciones de pedidos. ¿Por qué? Porque entonces puede encontrar una situación en la que no puede mantener los valores existentes y preservar un orden sensato al mismo tiempo. Tome, por ejemplo, una enumeración para las direcciones de la brújula: N = 0, NE = 1, E = 2, SE = 3, ... Ahora, si después de alguna actualización incluye NNE y así sucesivamente entre las direcciones existentes, puede agregarlas al final de la lista y así romper el orden, o intercalarlos con las claves existentes y así romper las asignaciones establecidas utilizadas en el código heredado. O bien, desprecia todas las claves antiguas y tiene un conjunto de claves completamente nuevo, junto con un código de compatibilidad que se traduce entre claves antiguas y nuevas por el bien del código heredado.

  3. Elige un tamaño suficiente. Por defecto, el compilador usará el entero más pequeño que puede contener todos los valores de enumeración. Lo que significa que si, en alguna actualización, su conjunto de enumeraciones posibles se expande de 254 a 259, de repente necesitará 2 bytes para cada valor de enumeración en lugar de uno. Esto podría romper la estructura y los diseños de clase en todo el lugar, así que trate de evitar esto utilizando un tamaño suficiente en el primer diseño. C ++ 11 le da mucho control aquí, pero especificar otra entrada también LAST_SPECIES_VALUE=65535debería ayudar.

  4. Tener un registro central. Dado que desea corregir la asignación entre nombres y valores, es malo si permite que los usuarios externos de su código agreguen nuevas constantes. No se debe permitir que ningún proyecto que use su código altere ese encabezado para agregar nuevas asignaciones. En cambio, deberían molestarte para agregarlos. Esto significa que, para el ejemplo humano y enano que mencionó, las enumeraciones son de hecho un mal ajuste. Allí sería mejor tener algún tipo de registro en tiempo de ejecución, donde los usuarios de su código puedan insertar una cadena y recuperar un número único, bien envuelto en algún tipo opaco. El "número" podría incluso ser un puntero a la cadena en cuestión, no importa.

A pesar del hecho de que mi último punto anterior hace que las enumeraciones no sean adecuadas para su situación hipotética, su situación real en la que algunas especificaciones podrían cambiar y tendría que actualizar algún código parece encajar bastante bien con un registro central para su biblioteca. Entonces las enumeraciones deberían ser apropiadas allí si tomas en serio mis otras sugerencias.

MvG
fuente