Koristite konstantne vrste za sigurniji i čišći kod

U ovom će se vodiču proširiti ideja o nabrojanim konstantama kako je opisano u Ericu Armstrongu, "Stvaranje pobrojanih konstanti u Javi." Toplo preporučujem da pročitate taj članak prije nego što se udubite u ovaj, jer ću pretpostaviti da ste upoznati s pojmovima povezanim s nabrojanim konstantama, a ja ću proširiti nekoliko primjera koda koji je Eric predstavio.

Pojam konstanti

U radu s pobrojanim konstantama, raspravit ću o pobrojanom dijelu koncepta na kraju članka. Za sada ćemo se usredotočiti samo na konstantni aspekt. Konstante su u osnovi varijable čija se vrijednost ne može mijenjati. U C / C ++ se ključna riječ constkoristi za deklariranje tih konstantnih varijabli. U Javi koristite ključnu riječ final. Međutim, ovdje predstavljeni alat nije samo primitivna varijabla; to je stvarna instanca objekta. Primjerci objekta su nepromjenjivi i nepromjenjivi - njihovo se unutarnje stanje ne može mijenjati. Ovo je slično jednobojnom uzorku, gdje klasa može imati samo jedan pojedinačni primjerak; u ovom slučaju, međutim, klasa može imati samo ograničeni i unaprijed definirani skup primjeraka.

Glavni razlozi za upotrebu konstanti su jasnoća i sigurnost. Na primjer, sljedeći dio koda nije samorazumljiv:

javna praznina setColor (int x) {...} javna praznina someMethod () {setColor (5); }

Iz ovog koda možemo utvrditi da se postavlja boja. Ali koju boju predstavlja 5? Ako je ovaj kôd napisao netko od onih rijetkih programera koji komentira njegovo ili njezino djelo, odgovor bismo mogli pronaći na vrhu datoteke. No vjerojatnije ćemo za objašnjenje morati potražiti neke stare projektne dokumente (ako uopće postoje).

Jasnije rješenje je dodijeliti vrijednost 5 varijabli sa značajnim imenom. Na primjer:

javni statički konačni int CRVENI = 5; javna void someMethod () {setColor (CRVENA); }

Sada možemo odmah reći što se događa s kodom. Boja se postavlja na crvenu. Ovo je puno čišće, ali je li sigurnije? Što ako se drugi koder zbuni i tako objavi različite vrijednosti:

javni statički konačni int CRVENI = 3; javni statički konačni int ZELENI = 5;

Sad imamo dva problema. Prije svega, REDviše nije postavljena na ispravnu vrijednost. Drugo, vrijednost za crveno predstavlja varijabla named GREEN. Možda je najstrašnije to što će se ovaj kôd sasvim dobro kompilirati, a pogreška se možda neće otkriti dok se proizvod ne pošalje.

Ovaj problem možemo riješiti stvaranjem konačne klase boja:

javna klasa Boja {public static final int RED = 5; javni statički konačni int ZELENI = 7; }

Zatim, putem pregleda dokumentacije i koda, potičemo programere da ga koriste na sljedeći način:

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

Kažem ohrabriti, jer dizajn u tom popisu kodova ne dopušta nam prisiljavanje kodera da se uskladi; kod će se i dalje kompajlirati čak i ako sve nije sasvim u redu. Stoga, iako je ovo malo sigurnije, nije potpuno sigurno. Iako bi programeri trebali koristiti Colorklasu, od njih se to ne traži. Programeri bi vrlo lako mogli napisati i sastaviti sljedeći kod:

 setColor (3498910); 

setColorPrepoznaje li metoda ovaj veliki broj kao boju? Vjerojatno ne. Pa kako se možemo zaštititi od ovih nevaljalih programera? Tu u pomoć dolaze vrste konstanti.

Počinjemo redefiniranjem potpisa metode:

 javna praznina setColor (Boja x) {...} 

Sada programeri ne mogu unijeti proizvoljnu cijelu vrijednost. Prisiljeni su pružiti valjan Colorobjekt. Primjer implementacije ovoga mogao bi izgledati ovako:

javna void someMethod () {setColor (nova boja ("Crvena")); }

Još uvijek radimo s čistim, čitljivim kodom i puno smo bliži postizanju apsolutne sigurnosti. Ali još nismo sasvim tamo. Programer još uvijek ima prostora za pustoš i može proizvoljno stvoriti nove boje poput ove:

javna void someMethod () {setColor (nova boja ("Bok, moje ime je Ted.")); }

Sprječavamo ovu situaciju čineći Colorklasu nepromjenjivom i skrivajući instanciju od programera. Svaku različitu vrstu boje (crvenu, zelenu, plavu) izrađujemo pojedinačno. To se postiže tako da se konstruktor učini privatnim, a zatim izloži javne obrade ograničenom i dobro definiranom popisu slučajeva:

javna klasa Boja {privatna boja () {} javna statička konačna boja CRVENA = nova boja (); javna statička završna boja ZELENA = nova boja (); javna statička konačna boja PLAVA = nova boja (); }

U ovom kodeksu konačno smo postigli apsolutnu sigurnost. Programer ne može izraditi lažne boje. Mogu se koristiti samo definirane boje; u suprotnom, program se neće kompajlirati. Evo kako naša implementacija sada izgleda:

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

Upornost

U redu, sada imamo čist i siguran način rješavanja stalnih tipova. Možemo stvoriti objekt s atributom boje i biti sigurni da će vrijednost boje uvijek biti valjana. Ali što ako ovaj objekt želimo pohraniti u bazu podataka ili ga zapisati u datoteku? Kako ćemo spremiti vrijednost boje? Moramo mapirati ove vrste u vrijednosti.

U gore spomenutom članku o JavaWorldu , Eric Armstrong koristio je niz vrijednosti. Korištenje nizova pruža dodatni bonus dajući vam nešto značajno za povratak u toString()metodu, što čini izlaz za ispravljanje pogrešaka vrlo jasnim.

Žice, međutim, mogu biti skupe za pohranu. Cijeli broj zahtijeva 32 bita za pohranu svoje vrijednosti, dok niz zahtijeva 16 bita po znaku (zbog podrške za Unicode). Na primjer, broj 49858712 može se pohraniti u 32 bita, ali za niz TURQUOISEbi bilo potrebno 144 bita. Ako pohranjujete tisuće objekata s atributima boje, ta se relativno mala razlika u bitovima (u ovom slučaju između 32 i 144) može brzo zbrojiti. Pa upotrijebimo umjesto toga cjelobrojne vrijednosti. Koje je rješenje ovog problema? Zadržat ćemo vrijednosti niza jer su važne za prezentaciju, ali ih nećemo pohranjivati.

Verzije Jave od 1.1 nadalje mogu automatski serializirati objekte, sve dok implementiraju Serializablesučelje. Da biste spriječili Java da pohranjuje strane podatke, takve varijable morate prijaviti transientključnom riječi. Dakle, da bismo pohranili cjelobrojne vrijednosti bez spremanja predstavljanja niza, atribut string proglašavamo privremenim. Evo nove klase, zajedno s pristupnicima atributima cijelog broja i niza:

javna klasa Boja implementira java.io.Serializable {private int vrijednost; privatni prijelazni naziv niza; javna statička konačna boja CRVENA = nova boja (0, "Crvena"); javna statička konačna boja PLAVA = nova boja (1, "Plava"); javna statička završna boja ZELENA = nova boja (2, "Zelena"); private Color (int vrijednost, naziv niza) {this.value = value; this.name = ime; } public int getValue () {povratna vrijednost; } javni String toString () {return ime; }}

Sada možemo učinkovito pohraniti primjerke konstantnog tipa Color. Ali što je s njihovim obnavljanjem? To će biti malo zeznuto. Prije nego što nastavimo dalje, proširimo ovo u okvir koji će za nas riješiti sve gore spomenute zamke, omogućujući nam da se usredotočimo na jednostavnu stvar definiranja vrsta.

Okvir konstantnog tipa

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

Zahvaljujući našoj organizaciji hashtable-of-hashtables, nevjerojatno je jednostavno izložiti funkciju popisivanja koju nudi Ericova implementacija. Jedino je upozorenje da sortiranje, koje nudi Ericov dizajn, nije zajamčeno. Ako koristite Java 2, sortiranu kartu možete zamijeniti unutarnjim hashtablima. Ali, kao što sam rekao na početku ove kolumne, trenutno me zanima samo verzija 1.1 JDK-a.

Jedina logika potrebna za nabrajanje tipova je dohvaćanje unutarnje tablice i vraćanje popisa elemenata. Ako unutarnja tablica ne postoji, jednostavno vraćamo nulu. Evo cijele metode: