Gebruik constante typen voor veiligere en schonere code

In deze tutorial wordt het idee van opgesomde constanten uitgebreid zoals beschreven in Eric Armstrongs, "Maak opgesomde constanten in Java". Ik raad ten zeerste aan dat artikel te lezen voordat u zich in dit artikel verdiept, aangezien ik aanneem dat u bekend bent met de concepten die verband houden met opgesomde constanten, en ik zal een deel van de voorbeeldcode die Eric presenteerde, nader toelichten.

Het concept van constanten

Wat betreft de opgesomde constanten, ga ik het opgesomde deel van het concept aan het einde van het artikel bespreken . Voorlopig concentreren we ons alleen op het constante aspect. Constanten zijn in feite variabelen waarvan de waarde niet kan veranderen. In C / C ++ wordt het sleutelwoord constgebruikt om deze constante variabelen te declareren. In Java gebruik je het trefwoord final. De hier geïntroduceerde tool is echter niet alleen een primitieve variabele; het is een daadwerkelijke objectinstantie. De objectinstanties zijn onveranderlijk en onveranderlijk - hun interne toestand mag niet worden gewijzigd. Dit is vergelijkbaar met het singleton-patroon, waarbij een klasse slechts één enkele instantie kan hebben; in dit geval kan een klasse echter slechts een beperkte en vooraf gedefinieerde set instanties hebben.

De belangrijkste redenen om constanten te gebruiken zijn duidelijkheid en veiligheid. Het volgende stuk code is bijvoorbeeld niet vanzelfsprekend:

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

Aan de hand van deze code kunnen we vaststellen dat er een kleur wordt ingesteld. Maar welke kleur vertegenwoordigt 5? Als deze code is geschreven door een van die zeldzame programmeurs die commentaar geeft op zijn of haar werk, kunnen we het antwoord bovenaan het bestand vinden. Maar waarschijnlijker zullen we moeten graven naar enkele oude ontwerpdocumenten (als ze al bestaan) voor een verklaring.

Een duidelijkere oplossing is om een ​​waarde van 5 toe te kennen aan een variabele met een betekenisvolle naam. Bijvoorbeeld:

openbare statische finale int ROOD = 5; public void someMethod () {setColor (RED); }

Nu kunnen we meteen zien wat er aan de hand is met de code. De kleur wordt op rood gezet. Dit is veel schoner, maar is het veiliger? Wat als een andere coder in de war raakt en verschillende waarden als volgt declareert:

openbare statische finale int RED = 3; openbare statische finale int GROEN = 5;

Nu hebben we twee problemen. Allereerst REDis niet meer op de juiste waarde ingesteld. Ten tweede wordt de waarde voor rood weergegeven door de variabele met de naam GREEN. Misschien wel het engste is dat deze code prima compileert en dat de bug mogelijk pas wordt gedetecteerd als het product is verzonden.

We kunnen dit probleem oplossen door een definitieve kleurklasse te maken:

openbare klasse Kleur {openbare statische finale int RED = 5; openbare statische finale int GROEN = 7; }

Vervolgens moedigen we programmeurs via documentatie en codebeoordeling aan om het als volgt te gebruiken:

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

Ik zeg aanmoedigen omdat het ontwerp in die codelijst ons niet toestaat de codeerder te dwingen om te voldoen; de code zal nog steeds compileren, zelfs als alles niet helemaal in orde is. Dus hoewel dit een beetje veiliger is, is het niet helemaal veilig. Hoewel programmeurs de klasse zouden moeten gebruiken Color, is dat niet verplicht. Programmeurs kunnen heel gemakkelijk de volgende code schrijven en compileren:

 setColor (3498910); 

setColorHerkent de methode dit grote aantal als een kleur? Waarschijnlijk niet. Dus hoe kunnen we onszelf beschermen tegen deze malafide programmeurs? Dat is waar constanten-typen te hulp schieten.

We beginnen met het opnieuw definiëren van de handtekening van de methode:

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

Nu kunnen programmeurs geen willekeurige gehele waarde doorgeven. Ze worden gedwongen een geldig Colorobject te verstrekken . Een voorbeeldimplementatie hiervan zou er als volgt uit kunnen zien:

public void someMethod () {setColor (nieuwe kleur ("Rood")); }

We werken nog steeds met schone, leesbare code en we zijn veel dichter bij het bereiken van absolute veiligheid. Maar we zijn er nog niet helemaal. De programmeur heeft nog wat ruimte om grote schade aan te richten en kan willekeurig nieuwe kleuren creëren, zoals:

public void someMethod () {setColor (nieuwe kleur ("Hallo, mijn naam is Ted.")); }

We voorkomen deze situatie door de Colorklasse onveranderlijk te maken en de instantiatie voor de programmeur te verbergen. We maken van elk verschillend type kleur (rood, groen, blauw) een singleton. Dit wordt bereikt door de constructor privé te maken en vervolgens openbare handvatten bloot te stellen aan een beperkte en goed gedefinieerde lijst met instanties:

openbare klasse Kleur {privé Kleur () {} openbare statische definitieve kleur ROOD = nieuwe kleur (); openbare statische definitieve kleur GROEN = nieuwe kleur (); openbare statische definitieve kleur BLAUW = nieuwe kleur (); }

In deze code hebben we eindelijk absolute veiligheid bereikt. De programmeur kan geen nepkleuren fabriceren. Alleen de gedefinieerde kleuren mogen worden gebruikt; anders compileert het programma niet. Dit is hoe onze implementatie er nu uitziet:

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

Persistentie

Oké, nu hebben we een schone en veilige manier om met constante types om te gaan. We kunnen een object maken met een kleurattribuut en er zeker van zijn dat de kleurwaarde altijd geldig is. Maar wat als we dit object in een database willen opslaan of naar een bestand willen schrijven? Hoe bewaren we de kleurwaarde? We moeten deze typen toewijzen aan waarden.

In het bovengenoemde JavaWorld- artikel gebruikte Eric Armstrong string-waarden. Het gebruik van strings biedt de toegevoegde bonus dat het u iets zinvols geeft om in de toString()methode terug te keren , waardoor de uitvoer van foutopsporing zeer duidelijk wordt.

Snaren kunnen echter duur zijn om op te slaan. Een geheel getal heeft 32 bits nodig om zijn waarde op te slaan, terwijl een string 16 bits per teken nodig heeft (vanwege Unicode-ondersteuning). Het nummer 49858712 kan bijvoorbeeld in 32 bits worden opgeslagen, maar voor de string TURQUOISEzijn 144 bits nodig. Als u duizenden objecten met kleurattributen opslaat, kan dit relatief kleine verschil in bits (tussen 32 en 144 in dit geval) snel oplopen. Dus laten we in plaats daarvan gehele getallen gebruiken. Wat is de oplossing voor dit probleem? We behouden de stringwaarden, omdat ze belangrijk zijn voor de presentatie, maar we gaan ze niet opslaan.

Versies van Java vanaf 1.1 zijn in staat om objecten automatisch te serialiseren, zolang ze de Serializableinterface implementeren . Om te voorkomen dat Java externe gegevens opslaat, moet u dergelijke variabelen declareren met het transienttrefwoord. Dus om de gehele waarden op te slaan zonder de stringvoorstelling op te slaan, verklaren we dat het stringattribuut van voorbijgaande aard is. Hier is de nieuwe klasse, samen met accessors tot de integer- en stringattributen:

public class Color implementeert java.io.Serializable {private int value; private tijdelijke tekenreeksnaam; openbare statische definitieve kleur ROOD = nieuwe kleur (0, "Rood"); openbare statische definitieve kleur BLAUW = nieuwe kleur (1, "Blauw"); openbare statische definitieve kleur GROEN = nieuwe kleur (2, "Groen"); private Color (int waarde, String naam) {this.value = value; this.name = naam; } public int getValue () {retourwaarde; } public String toString () {naam retourneren; }}

Nu kunnen we instanties van het constante type efficiënt opslaan Color. Maar hoe zit het met het herstellen ervan? Dat wordt een beetje lastig. Voordat we verder gaan, laten we dit uitbreiden naar een raamwerk dat alle bovengenoemde valkuilen voor ons kan afhandelen, zodat we ons kunnen concentreren op de simpele kwestie van het definiëren van typen.

Het constante type raamwerk

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

Dankzij onze hashtable-of-hashtables-organisatie is het ongelooflijk eenvoudig om de opsommingsfunctionaliteit te tonen die wordt aangeboden door de implementatie van Eric. Het enige voorbehoud is dat sortering, die het ontwerp van Eric biedt, niet gegarandeerd is. Als u Java 2 gebruikt, kunt u de gesorteerde kaart vervangen door de innerlijke hashtabellen. Maar, zoals ik in het begin van deze column al zei, ben ik op dit moment alleen bezig met de 1.1-versie van de JDK.

De enige logica die nodig is om de typen op te sommen, is om de binnenste tabel op te halen en de elementenlijst terug te sturen. Als de binnenste tabel niet bestaat, retourneren we eenvoudig null. Hier is de hele methode: