Usa tipi costanti per un codice più sicuro e pulito

In questo tutorial verrà ampliata l'idea delle costanti enumerate come trattato in "Crea costanti enumerate in Java" di Eric Armstrong. Consiglio vivamente di leggere quell'articolo prima di immergerti in questo, poiché presumo che tu abbia familiarità con i concetti relativi alle costanti enumerate, e amplierò alcuni dei codici di esempio presentati da Eric.

Il concetto di costanti

Trattando delle costanti enumerate, discuterò la parte enumerata del concetto alla fine dell'articolo. Per ora, ci concentreremo solo sull'aspetto costante . Le costanti sono fondamentalmente variabili il cui valore non può cambiare. In C / C ++, la parola chiave constviene utilizzata per dichiarare queste variabili costanti. In Java, usi la parola chiave final. Tuttavia, lo strumento qui introdotto non è semplicemente una variabile primitiva; è un'istanza di oggetto reale. Le istanze dell'oggetto sono immutabili e immutabili - il loro stato interno non può essere modificato. Questo è simile al pattern singleton, in cui una classe può avere solo una singola istanza; in questo caso, tuttavia, una classe può avere solo un insieme limitato e predefinito di istanze.

I motivi principali per utilizzare le costanti sono la chiarezza e la sicurezza. Ad esempio, il seguente pezzo di codice non è autoesplicativo:

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

Da questo codice possiamo accertare che si sta impostando un colore. Ma che colore rappresenta 5? Se questo codice fosse stato scritto da uno di quei rari programmatori che commenta il suo lavoro, potremmo trovare la risposta all'inizio del file. Ma più probabilmente dovremo cercare alcuni vecchi documenti di progettazione (se esistono) per una spiegazione.

Una soluzione più chiara è assegnare un valore 5 a una variabile con un nome significativo. Per esempio:

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

Ora possiamo dire immediatamente cosa sta succedendo con il codice. Il colore viene impostato sul rosso. Questo è molto più pulito, ma è più sicuro? E se un altro programmatore si confonde e dichiara valori diversi in questo modo:

public static final int RED = 3; pubblico statico finale int VERDE = 5;

Adesso abbiamo due problemi. Innanzitutto REDnon è più impostato sul valore corretto. In secondo luogo, il valore per il rosso è rappresentato dalla variabile denominata GREEN. Forse la parte più spaventosa è che questo codice verrà compilato correttamente e il bug potrebbe non essere rilevato fino a quando il prodotto non verrà spedito.

Possiamo risolvere questo problema creando una classe di colori definitiva:

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

Quindi, tramite la documentazione e la revisione del codice, incoraggiamo i programmatori a utilizzarlo in questo modo:

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

Dico incoraggia perché il design in quell'elenco di codici non ci consente di obbligare il programmatore a conformarsi; il codice verrà comunque compilato anche se non è tutto in ordine. Quindi, anche se questo è un po 'più sicuro, non è completamente sicuro. Sebbene i programmatori dovrebbero usare la Colorclasse, non sono obbligati a farlo. I programmatori potrebbero scrivere e compilare molto facilmente il seguente codice:

 setColor (3498910); 

Il setColormetodo riconosce questo numero elevato come un colore? Probabilmente no. Allora come possiamo proteggerci da questi programmatori canaglia? È qui che i tipi di costanti vengono in soccorso.

Iniziamo ridefinendo la firma del metodo:

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

Ora i programmatori non possono passare un valore intero arbitrario. Sono obbligati a fornire un Coloroggetto valido . Un esempio di implementazione di questo potrebbe essere simile a questo:

public void someMethod () {setColor (new Color ("Red")); }

Stiamo ancora lavorando con un codice pulito e leggibile e siamo molto più vicini al raggiungimento della sicurezza assoluta. Ma non siamo ancora arrivati. Il programmatore ha ancora un po 'di spazio per creare scompiglio e può creare arbitrariamente nuovi colori in questo modo:

public void someMethod () {setColor (new Color ("Ciao, mi chiamo Ted.")); }

Preveniamo questa situazione rendendo la Colorclasse immutabile e nascondendo l'istanza al programmatore. Rendiamo singleton ogni diverso tipo di colore (rosso, verde, blu). Ciò si ottiene rendendo privato il costruttore e quindi esponendo gli handle pubblici a un elenco di istanze limitato e ben definito:

public class Color {private Color () {} public static final Color RED = new Color (); pubblico statico finale Colore VERDE = nuovo Colore (); pubblico statico finale Colore BLU = nuovo Colore (); }

In questo codice abbiamo finalmente raggiunto la sicurezza assoluta. Il programmatore non può fabbricare colori fasulli. Possono essere utilizzati solo i colori definiti; altrimenti, il programma non verrà compilato. Ecco come appare ora la nostra implementazione:

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

Persistenza

Ok, ora abbiamo un modo pulito e sicuro per gestire i tipi costanti. Possiamo creare un oggetto con un attributo color ed essere certi che il valore del colore sarà sempre valido. Ma cosa succede se vogliamo memorizzare questo oggetto in un database o scriverlo su un file? Come salviamo il valore del colore? Dobbiamo mappare questi tipi ai valori.

Nell'articolo JavaWorld citato sopra, Eric Armstrong ha utilizzato valori stringa. L'uso delle stringhe fornisce il vantaggio aggiuntivo di darti qualcosa di significativo da restituire nel toString()metodo, il che rende molto chiaro l'output di debug.

Le corde, tuttavia, possono essere costose da memorizzare. Un numero intero richiede 32 bit per memorizzare il suo valore mentre una stringa richiede 16 bit per carattere (a causa del supporto Unicode). Ad esempio, il numero 49858712 può essere memorizzato a 32 bit, ma la stringa TURQUOISErichiederebbe 144 bit. Se si memorizzano migliaia di oggetti con attributi di colore, questa differenza relativamente piccola di bit (tra 32 e 144 in questo caso) può sommarsi rapidamente. Quindi usiamo invece valori interi. Qual è la soluzione a questo problema? Manterremo i valori di stringa, perché sono importanti per la presentazione, ma non li memorizzeremo.

Le versioni di Java dalla 1.1 in poi sono in grado di serializzare gli oggetti automaticamente, purché implementino l' Serializableinterfaccia. Per evitare che Java memorizzi dati estranei, è necessario dichiarare tali variabili con la transientparola chiave. Quindi, per memorizzare i valori interi senza memorizzare la rappresentazione della stringa, dichiariamo che l'attributo della stringa è transitorio. Ecco la nuova classe, insieme alle funzioni di accesso agli attributi integer e string:

public class Color implementa java.io.Serializable {private int value; nome stringa transitoria privata; pubblico statico finale Colore ROSSO = nuovo Colore (0, "Rosso"); pubblico statico finale Colore BLU = nuovo Colore (1, "Blu"); pubblico statico finale Colore VERDE = nuovo Colore (2, "Verde"); colore privato (valore int, nome stringa) {this.value = value; this.name = nome; } public int getValue () {valore di ritorno; } public String toString () {return name; }}

Ora possiamo archiviare in modo efficiente istanze del tipo costante Color. Ma per quanto riguarda ripristinarli? Sarà un po 'complicato. Prima di andare oltre, espandiamolo in un framework che gestirà tutte le insidie ​​di cui sopra per noi, permettendoci di concentrarci sulla semplice questione della definizione dei tipi.

Il framework di tipo costante

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

Grazie alla nostra organizzazione di tabelle hash, è incredibilmente semplice esporre la funzionalità di enumerazione offerta dall'implementazione di Eric. L'unico avvertimento è che l'ordinamento, offerto dal design di Eric, non è garantito. Se stai usando Java 2, puoi sostituire la mappa ordinata con le tabelle hash interne. Ma, come ho affermato all'inizio di questa colonna, al momento mi interessa solo la versione 1.1 di JDK.

L'unica logica richiesta per enumerare i tipi è recuperare la tabella interna e restituire il suo elenco di elementi. Se la tabella interna non esiste, restituiamo semplicemente null. Ecco l'intero metodo: