Utilice tipos constantes para un código más seguro y limpio

En este tutorial se ampliará la idea de las constantes enumeradas como se explica en Eric Armstrong, "Crear constantes enumeradas en Java". Recomiendo encarecidamente leer ese artículo antes de sumergirse en este, ya que asumiré que está familiarizado con los conceptos relacionados con las constantes enumeradas, y ampliaré algunos de los ejemplos de código que presentó Eric.

El concepto de constantes

Al tratar con las constantes enumeradas, voy a discutir la parte enumerada del concepto al final del artículo. Por ahora, solo nos centraremos en el aspecto constante . Las constantes son básicamente variables cuyo valor no puede cambiar. En C / C ++, la palabra clave constse usa para declarar estas variables constantes. En Java, usa la palabra clave final. Sin embargo, la herramienta presentada aquí no es simplemente una variable primitiva; es una instancia de objeto real. Las instancias de objeto son inmutables e inmutables; su estado interno no puede modificarse. Esto es similar al patrón singleton, donde una clase solo puede tener una única instancia; en este caso, sin embargo, una clase solo puede tener un conjunto limitado y predefinido de instancias.

Las principales razones para utilizar constantes son la claridad y la seguridad. Por ejemplo, el siguiente fragmento de código no se explica por sí mismo:

public void setColor (int x) {...} public void someMethod () {setColor (5); }

A partir de este código, podemos determinar que se está estableciendo un color. Pero, ¿qué color representa el 5? Si este código fue escrito por uno de esos pocos programadores que comentan sobre su trabajo, podríamos encontrar la respuesta en la parte superior del archivo. Pero lo más probable es que tengamos que buscar algunos documentos de diseño antiguos (si es que existen) para obtener una explicación.

Una solución más clara es asignar un valor de 5 a una variable con un nombre significativo. Por ejemplo:

public static final int RED = 5; public void someMethod () {setColor (RED); }

Ahora podemos saber inmediatamente qué está pasando con el código. El color se establece en rojo. Esto es mucho más limpio, pero ¿es más seguro? ¿Qué pasa si otro codificador se confunde y declara diferentes valores así:

public static final int RED = 3; public static final int VERDE = 5;

Ahora tenemos dos problemas. En primer lugar, REDya no se establece en el valor correcto. En segundo lugar, el valor del rojo está representado por la variable nombrada GREEN. Quizás la parte más aterradora es que este código se compilará bien y es posible que el error no se detecte hasta que se haya enviado el producto.

Podemos solucionar este problema creando una clase de color definitiva:

public class Color {public static final int RED = 5; public static final int VERDE = 7; }

Luego, a través de la revisión de documentación y código, alentamos a los programadores a usarlo así:

public void someMethod () {setColor (Color.RED); }

Digo alentar porque el diseño en esa lista de códigos no nos permite obligar al codificador a cumplir; el código aún se compilará incluso si todo no está en orden. Por lo tanto, si bien esto es un poco más seguro, no es completamente seguro. Aunque los programadores deben usar la Colorclase, no es obligatorio. Los programadores podrían escribir y compilar muy fácilmente el siguiente código:

 setColor (3498910); 

¿El setColormétodo reconoce que este gran número es un color? Probablemente no. Entonces, ¿cómo podemos protegernos de estos programadores deshonestos? Ahí es donde los tipos de constantes vienen al rescate.

Comenzamos redefiniendo la firma del método:

 public void setColor (Color x) {...} 

Ahora los programadores no pueden pasar un valor entero arbitrario. Se ven obligados a proporcionar un Colorobjeto válido . Una implementación de ejemplo de esto podría verse así:

public void someMethod () {setColor (nuevo Color ("Rojo")); }

Seguimos trabajando con un código limpio y legible, y estamos mucho más cerca de lograr una seguridad absoluta. Pero aún no hemos llegado a ese punto. El programador todavía tiene espacio para causar estragos y puede crear arbitrariamente nuevos colores como este:

public void someMethod () {setColor (new Color ("Hola, mi nombre es Ted.")); }

Prevenimos esta situación haciendo que la Colorclase sea inmutable y ocultando la instanciación al programador. Hacemos que cada tipo diferente de color (rojo, verde, azul) sea un singleton. Esto se logra haciendo que el constructor sea privado y luego exponiendo los identificadores públicos a una lista de instancias restringida y bien definida:

public class Color {private Color () {} public static final Color ROJO = nuevo Color (); public static final Color VERDE = nuevo Color (); público estático final Color AZUL = nuevo Color (); }

En este código finalmente hemos logrado una seguridad absoluta. El programador no puede fabricar colores falsos. Solo se pueden utilizar los colores definidos; de lo contrario, el programa no se compilará. Así es como se ve nuestra implementación ahora:

public void someMethod () {setColor (Color.RED); }

Persistencia

Bien, ahora tenemos una forma limpia y segura de lidiar con tipos constantes. Podemos crear un objeto con un atributo de color y estar seguros de que el valor del color siempre será válido. Pero, ¿qué pasa si queremos almacenar este objeto en una base de datos o escribirlo en un archivo? ¿Cómo guardamos el valor del color? Tenemos que asignar estos tipos a valores.

En el artículo de JavaWorld mencionado anteriormente, Eric Armstrong usó valores de cadena. El uso de cadenas proporciona la ventaja adicional de brindarle algo significativo para devolver en el toString()método, lo que hace que la depuración sea muy clara.

Sin embargo, las cadenas pueden ser caras de almacenar. Un entero requiere 32 bits para almacenar su valor, mientras que una cadena requiere 16 bits por carácter (debido al soporte Unicode). Por ejemplo, el número 49858712 se puede almacenar en 32 bits, pero la cadena TURQUOISErequeriría 144 bits. Si está almacenando miles de objetos con atributos de color, esta diferencia relativamente pequeña en bits (entre 32 y 144 en este caso) puede acumularse rápidamente. Entonces, usemos valores enteros en su lugar. ¿Cuál es la solución a este problema? Conservaremos los valores de cadena, porque son importantes para la presentación, pero no los almacenaremos.

Las versiones de Java desde la 1.1 en adelante pueden serializar objetos automáticamente, siempre que implementen la Serializableinterfaz. Para evitar que Java almacene datos extraños, debe declarar tales variables con la transientpalabra clave. Entonces, para almacenar los valores enteros sin almacenar la representación de la cadena, declaramos que el atributo de la cadena es transitorio. Aquí está la nueva clase, junto con los descriptores de acceso a los atributos de entero y cadena:

La clase pública Color implementa java.io.Serializable {valor int privado; nombre de cadena transitorio privado; public static final Color ROJO = nuevo Color (0, "Rojo"); public static final Color AZUL = nuevo Color (1, "Azul"); public static final Color VERDE = nuevo Color (2, "Verde"); color privado (valor int, nombre de cadena) {this.value = valor; this.name = nombre; } public int getValue () {valor de retorno; } public String toString () {nombre de retorno; }}

Ahora podemos almacenar de manera eficiente instancias del tipo constante Color. Pero, ¿qué hay de restaurarlos? Eso va a ser un poco complicado. Antes de continuar, ampliemos esto en un marco que manejará todos los escollos antes mencionados por nosotros, permitiéndonos enfocarnos en la simple cuestión de definir tipos.

El marco de tipo constante

With our firm understanding of constant types, I can now jump into this month's tool. The tool is called Type and it is a simple abstract class. All you have to do is create a very simple subclass and you've got a full-featured constant type library. Here's what our Color class will look like now:

public class Color extends Type { protected Color( int value, String desc ) { super( value, desc ); } public static final Color RED = new Color( 0, "Red" ); public static final Color BLUE = new Color( 1, "Blue" ); public static final Color GREEN = new Color( 2, "Green" ); } 

The Color class consists of nothing but a constructor and a few publicly accessible instances. All of the logic discussed to this point will be defined and implemented in the superclass Type; we'll be adding more as we go along. Here's what Type looks like so far:

public class Type implements java.io.Serializable { private int value; private transient String name; protected Type( int value, String name ) { this.value = value; this.name = name; } public int getValue() { return value; } public String toString() { return name; } } 

Back to persistence

With our new framework in hand, we can continue where we left off in the discussion of persistence. Remember, we can save our types by storing their integer values, but now we want to restore them. This is going to require a lookup -- a reverse calculation to locate the object instance based on its value. In order to perform a lookup, we need a way to enumerate all of the possible types.

In Eric's article, he implemented his own enumeration by implementing the constants as nodes in a linked list. I'm going to forego this complexity and use a simple hashtable instead. The key for the hash will be the integer values of the type (wrapped in an Integer object), and the value of the hash will be a reference to the type instance. For example, the GREEN instance of Color would be stored like so:

 hashtable.put( new Integer( GREEN.getValue() ), GREEN ); 

Of course, we don't want to type this out for each possible type. There could be hundreds of different values, thus creating a typing nightmare and opening the doors to some nasty problems -- you might forget to put one of the values in the hashtable and then not be able to look it up later, for instance. So we'll declare a global hashtable within Type and modify the constructor to store the mapping upon creation:

 private static final Hashtable types = new Hashtable(); protected Type( int value, String desc ) { this.value = value; this.desc = desc; types.put( new Integer( value ), this ); } 

But this creates a problem. If we have a subclass called Color, which has a type (that is, Green) with a value of 5, and then we create another subclass called Shade, which also has a type (that is Dark) with a value of 5, only one of them will be stored in the hashtable -- the last one to be instantiated.

In order to avoid this, we have to store a handle to the type based on not only its value, but also its class. Let's create a new method to store the type references. We'll use a hashtable of hashtables. The inner hashtable will be a mapping of values to types for each specific subclass (Color, Shade, and so on). The outer hashtable will be a mapping of subclasses to inner tables.

This routine will first attempt to acquire the inner table from the outer table. If it receives a null, the inner table doesn't exist yet. So, we create a new inner table and put it into the outer table. Next, we add the value/type mapping to the inner table and we're done. Here's the code:

 private void storeType( Type type ) { String className = type.getClass().getName(); Hashtable values; synchronized( types ) // avoid race condition for creating inner table { values = (Hashtable) types.get( className ); if( values == null ) { values = new Hashtable(); types.put( className, values ); } } values.put( new Integer( type.getValue() ), type ); } 

And here's the new version of the constructor:

 protected Type( int value, String desc ) { this.value = value; this.desc = desc; storeType( this ); } 

Now that we are storing a road map of types and values, we can perform lookups and thus restore an instance based on a value. The lookup requires two things: the target subclass identity and the integer value. Using this information, we can extract the inner table and find the handle to the matching type instance. Here's the code:

 public static Type getByValue( Class classRef, int value ) { Type type = null; String className = classRef.getName(); Hashtable values = (Hashtable) types.get( className ); if( values != null ) { type = (Type) values.get( new Integer( value ) ); } return( type ); } 

Thus, restoring a value is as simple as this (note that the return value must be casted):

 int value = // read from file, database, etc. Color background = (ColorType) Type.findByValue( ColorType.class, value ); 

Enumerating the types

Gracias a nuestra organización hashtable-of-hashtables, es increíblemente sencillo exponer la funcionalidad de enumeración que ofrece la implementación de Eric. La única advertencia es que la clasificación, que ofrece el diseño de Eric, no está garantizada. Si está utilizando Java 2, puede sustituir el mapa ordenado por las tablas hash internas. Pero, como dije al principio de esta columna, en este momento solo me preocupa la versión 1.1 del JDK.

La única lógica necesaria para enumerar los tipos es recuperar la tabla interna y devolver su lista de elementos. Si la tabla interna no existe, simplemente devolvemos nulo. Aquí está el método completo: