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.
fuente
Respuestas:
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:
value
es un valor de color válido o no. El compilador ya lo ha hecho.enum class
función en C ++ moderno incluso le permite obligar a las personas a escribir siempre el primero en lugar del segundo.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".
fuente
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 ladrawSolid()
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: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 unaswitch
declaración a la que le falta unacase
clá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 lasdraw()
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.fuente
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 otraprogrammers SE
pregunta.Pyramid
saber realmentedraw()
una pirámide. En el mejor de los casos, podría derivarSolid
y tener unGetTriangles()
método y podría pasarlo a unSolidDrawer
servicio. Pensé que nos estábamos alejando de los ejemplos de objetos físicos como ejemplos de objetos en OOP.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:
Buenos usos de las enumeraciones:
System.DayOfWeek
) A menos que esté lidiando con un calendario increíblemente oscuro, solo habrá 7 días de la semana.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.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 queThread
pueda 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.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 unHasFlag(Enum flag)
método incorporado. También admiten los operadores bit a bit y hayFlagsAttribute
que 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:
True
bandera implicaFalse
). Este comportamiento se puede respaldar aún más mediante el uso de funciones especializadas (es decir,IsTrue(flags)
yIsFalse(flags)
).fuente
RegexOptions
msdn.microsoft.com/en-us/library/…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í:
Uno o dos de estos están bien, pero tan pronto como tenga este tipo de enumeración significativa, estas
switch
declaraciones 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 unaswitch
por 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.
fuente
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.
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_orc
o 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.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.
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=65535
debería ayudar.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.
fuente