Utilisez des types constants pour un code plus sûr et plus propre

Dans ce didacticiel, nous développerons l'idée des constantes énumérées comme décrit dans Eric Armstrong, «Créer des constantes énumérées en Java». Je vous recommande fortement de lire cet article avant de vous plonger dans celui-ci, car je suppose que vous êtes familier avec les concepts liés aux constantes énumérées, et je développerai certains des exemples de code présentés par Eric.

Le concept de constantes

En traitant des constantes énumérées, je vais discuter de la partie énumérée du concept à la fin de l'article. Pour l'instant, nous allons nous concentrer uniquement sur l' aspect constant . Les constantes sont essentiellement des variables dont la valeur ne peut pas changer. En C / C ++, le mot clé constest utilisé pour déclarer ces variables constantes. En Java, vous utilisez le mot-clé final. Cependant, l'outil présenté ici n'est pas simplement une variable primitive; c'est une instance d'objet réelle. Les instances d'objet sont immuables et inchangeables - leur état interne ne peut pas être modifié. Ceci est similaire au modèle singleton, où une classe ne peut avoir qu'une seule instance; dans ce cas, cependant, une classe ne peut avoir qu'un ensemble limité et prédéfini d'instances.

Les principales raisons d'utiliser des constantes sont la clarté et la sécurité. Par exemple, le morceau de code suivant n'est pas explicite:

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

À partir de ce code, nous pouvons vérifier qu'une couleur est définie. Mais quelle couleur représente 5? Si ce code a été écrit par l'un de ces rares programmeurs qui commente son travail, nous pourrions trouver la réponse en haut du fichier. Mais plus probablement, nous devrons fouiller pour trouver de vieux documents de conception (s'ils existent même) pour une explication.

Une solution plus claire consiste à attribuer une valeur de 5 à une variable avec un nom significatif. Par exemple:

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

Nous pouvons maintenant dire immédiatement ce qui se passe avec le code. La couleur est réglée sur le rouge. C'est beaucoup plus propre, mais est-ce plus sûr? Que faire si un autre codeur est confus et déclare des valeurs différentes comme ceci:

public static final int RED = 3; public statique final int VERT = 5;

Maintenant, nous avons deux problèmes. Tout d'abord, REDn'est plus réglé sur la valeur correcte. Deuxièmement, la valeur du rouge est représentée par la variable nommée GREEN. La partie la plus effrayante est peut-être que ce code se compilera très bien et que le bogue ne sera peut-être pas détecté tant que le produit n'aura pas été expédié.

Nous pouvons résoudre ce problème en créant une classe de couleur définitive:

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

Ensuite, via la documentation et la révision du code, nous encourageons les programmeurs à l'utiliser comme ceci:

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

Je dis encourager parce que la conception de cette liste de codes ne nous permet pas de forcer le codeur à se conformer; le code sera toujours compilé même si tout n'est pas tout à fait en ordre. Ainsi, bien que ce soit un peu plus sûr, ce n'est pas complètement sûr. Bien que les programmeurs devraient utiliser la Colorclasse, ils ne sont pas obligés de le faire. Les programmeurs pourraient très facilement écrire et compiler le code suivant:

 setColor (3498910); 

La setColorméthode reconnaît-elle ce grand nombre comme étant une couleur? Probablement pas. Alors, comment pouvons-nous nous protéger de ces programmeurs voyous? C'est là que les types de constantes viennent à la rescousse.

Nous commençons par redéfinir la signature de la méthode:

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

Désormais, les programmeurs ne peuvent pas transmettre une valeur entière arbitraire. Ils sont obligés de fournir un Colorobjet valide . Un exemple d'implémentation de ceci pourrait ressembler à ceci:

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

Nous travaillons toujours avec un code clair et lisible, et nous sommes bien plus près d'atteindre une sécurité absolue. Mais nous n'en sommes pas encore tout à fait là. Le programmeur a encore de la place pour faire des ravages et peut créer arbitrairement de nouvelles couleurs comme ceci:

public void someMethod () {setColor (new Color ("Salut, je m'appelle Ted.")); }

Nous évitons cette situation en rendant la Colorclasse immuable et en cachant l'instanciation au programmeur. Nous faisons de chaque type de couleur (rouge, vert, bleu) un singleton. Ceci est accompli en rendant le constructeur privé, puis en exposant des handles publics à une liste restreinte et bien définie d'instances:

public class Color {private Color () {} public static final Color RED = new Color (); public static final Color GREEN = new Color (); public static final Color BLUE = new Color (); }

Dans ce code, nous avons enfin atteint une sécurité absolue. Le programmeur ne peut pas fabriquer de fausses couleurs. Seules les couleurs définies peuvent être utilisées; sinon, le programme ne compilera pas. Voici à quoi ressemble notre implémentation maintenant:

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

Persistance

D'accord, nous avons maintenant un moyen propre et sûr de traiter les types constants. Nous pouvons créer un objet avec un attribut de couleur et être certain que la valeur de couleur sera toujours valide. Mais que faire si nous voulons stocker cet objet dans une base de données ou l'écrire dans un fichier? Comment enregistrer la valeur de la couleur? Nous devons mapper ces types sur des valeurs.

Dans l' article JavaWorld mentionné ci-dessus, Eric Armstrong a utilisé des valeurs de chaîne. L'utilisation de chaînes offre l'avantage supplémentaire de vous donner quelque chose de significatif à renvoyer dans la toString()méthode, ce qui rend la sortie de débogage très claire.

Les cordes, cependant, peuvent être coûteuses à stocker. Un entier nécessite 32 bits pour stocker sa valeur tandis qu'une chaîne nécessite 16 bits par caractère (en raison du support Unicode). Par exemple, le nombre 49858712 peut être stocké sur 32 bits, mais la chaîne TURQUOISEnécessiterait 144 bits. Si vous stockez des milliers d'objets avec des attributs de couleur, cette différence relativement faible en bits (entre 32 et 144 dans ce cas) peut s'additionner rapidement. Alors utilisons plutôt des valeurs entières. Quelle est la solution à ce problème? Nous conserverons les valeurs de chaîne, car elles sont importantes pour la présentation, mais nous n'allons pas les stocker.

Les versions de Java à partir de 1.1 sont capables de sérialiser automatiquement les objets, à condition qu'elles implémentent l' Serializableinterface. Afin d'empêcher Java de stocker des données superflues, vous devez déclarer ces variables avec le transientmot - clé. Ainsi, afin de stocker les valeurs entières sans stocker la représentation sous forme de chaîne, nous déclarons que l'attribut string est transitoire. Voici la nouvelle classe, ainsi que les accesseurs aux attributs integer et string:

classe publique Color implémente java.io.Serializable {valeur int privé; nom de chaîne transitoire privé; 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"); couleur privée (valeur int, nom de la chaîne) {this.value = value; this.name = nom; } public int getValue () {valeur de retour; } public String toString () {nom de retour; }}

Maintenant, nous pouvons stocker efficacement les instances de type constant Color. Mais qu'en est-il de les restaurer? Cela va être un peu délicat. Avant d'aller plus loin, développons cela dans un cadre qui gèrera tous les pièges susmentionnés pour nous, nous permettant de nous concentrer sur la simple question de la définition des types.

Le cadre de type constant

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

Grâce à notre organisation hashtable-of-hashtables, il est incroyablement simple d'exposer la fonctionnalité d'énumération offerte par l'implémentation d'Eric. La seule mise en garde est que le tri, que propose la conception d'Eric, n'est pas garanti. Si vous utilisez Java 2, vous pouvez remplacer la carte triée par les tables de hachage internes. Mais, comme je l'ai dit au début de cette chronique, je ne suis concerné que par la version 1.1 du JDK pour le moment.

La seule logique requise pour énumérer les types est de récupérer la table interne et de renvoyer sa liste d'éléments. Si la table interne n'existe pas, nous retournons simplement null. Voici la méthode complète: