Използвайте константи за по-безопасен и по-чист код

В този урок ще се разшири идеята за изброените константи, както е описано в Ерик Армстронг, "Създаване на изброени константи в Java." Силно препоръчвам да прочетете тази статия, преди да се потопите в нея, тъй като ще предположа, че сте запознати с понятията, свързани с изброените константи, и ще разгледам някои от примерните кодове, които Ерик представи.

Понятието константи

При работата с изброените константи ще обсъдя изброената част от концепцията в края на статията. Засега ще се съсредоточим само върху постоянния аспект. Константите са основно променливи, чиято стойност не може да се променя. В C / C ++ ключовата дума constсе използва за деклариране на тези константни променливи. В Java използвате ключовата дума final. Въведеният тук инструмент обаче не е просто примитивна променлива; това е действителен екземпляр на обект. Екземплярите на обекта са неизменни и неизменни - тяхното вътрешно състояние не може да бъде променено. Това е подобно на единичния модел, където клас може да има само един единствен екземпляр; в този случай обаче клас може да има само ограничен и предварително определен набор от екземпляри.

Основните причини да се използват константи са яснотата и безопасността. Например следната част от кода не се обяснява сама по себе си:

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

От този код можем да установим, че се задава цвят. Но какъв цвят представлява 5? Ако този код е написан от някой от онези редки програмисти, които коментират работата му, може да намерим отговора в горната част на файла. Но по-вероятно ще трябва да разровим някои стари дизайнерски документи (ако изобщо съществуват) за обяснение.

По-ясно решение е да се присвои стойност 5 на променлива със значимо име. Например:

публичен статичен финал int ЧЕРВЕН = 5; публична невалидна someMethod () {setColor (ЧЕРВЕНО); }

Сега можем веднага да кажем какво се случва с кода. Цветът се настройва на червен. Това е много по-чисто, но по-безопасно ли е? Ами ако друг програмист се обърка и обяви различни стойности по следния начин:

публичен статичен финал int ЧЕРВЕН = 3; публичен статичен финал int GREEN = 5;

Сега имаме два проблема. На първо място, REDвече не е зададена на правилната стойност. Второ, стойността за червено се представя от имената на променливата GREEN. Може би най-страшната част е, че този код ще се компилира добре и грешката може да не бъде открита, докато продуктът не бъде изпратен.

Можем да разрешим този проблем, като създадем окончателен цветен клас:

публичен клас Цвят {публичен статичен финал int RED = 5; публичен статичен финал int GREEN = 7; }

След това, чрез преглед на документация и код, насърчаваме програмистите да го използват така:

публична невалидна someMethod () {setColor (Color.RED); }

Казвам насърчаване, защото дизайнът в този списък с кодове не ни позволява да принудим кодера да спазва; кодът пак ще се компилира, дори ако всичко не е съвсем в ред. По този начин, макар това да е малко по-безопасно, то не е напълно безопасно. Въпреки че програмистите трябва да използват Colorкласа, от тях не се изисква. Програмистите могат много лесно да напишат и компилират следния код:

 setColor (3498910); 

setColorРазпознава ли методът това голямо число като цвят? Вероятно не. И така, как можем да се предпазим от тези измамници програмисти? Тук на помощ идват константите.

Започваме с предефиниране на подписа на метода:

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

Сега програмистите не могат да предадат произволно цяло число. Те са принудени да предоставят валиден Colorобект. Примерно изпълнение на това може да изглежда така:

public void someMethod () {setColor (нов цвят ("червен")); }

Все още работим с чист, четим код и сме много по-близо до постигането на абсолютна безопасност. Но все още не сме съвсем там. Програмистът все още има малко място за разрушаване и може произволно да създава нови цветове по следния начин:

public void someMethod () {setColor (new Color ("Здравейте, името ми е Тед.")); }

Предотвратяваме тази ситуация, като правим Colorкласа неизменим и скриваме инстанцията от програмиста. Ние правим всеки различен тип цвят (червен, зелен, син) единичен. Това се постига чрез превръщане на конструктора в частен и след това излагане на публични манипулатори на ограничен и добре дефиниран списък с екземпляри:

публичен клас Color {private Color () {} публичен статичен краен цвят ЧЕРВЕН = нов цвят (); публичен статичен краен цвят ЗЕЛЕН = нов цвят (); публичен статичен краен цвят СИН = нов цвят (); }

В този код най-накрая постигнахме абсолютна безопасност. Програмистът не може да създава фалшиви цветове. Могат да се използват само определените цветове; в противен случай програмата няма да се компилира. Ето как изглежда нашето внедряване сега:

публична невалидна someMethod () {setColor (Color.RED); }

Постоянство

Добре, сега имаме чист и безопасен начин да се справим с постоянни типове. Можем да създадем обект с атрибут цвят и да сме сигурни, че стойността на цвета винаги ще е валидна. Но какво, ако искаме да съхраним този обект в база данни или да го запишем във файл? Как да запазим стойността на цвета? Трябва да съпоставим тези типове със стойности.

В споменатата по-горе статия за JavaWorld Ерик Армстронг използва низови стойности. Използването на низове осигурява допълнителен бонус, като ви дава нещо смислено, което да върнете в toString()метода, което прави изхода за отстраняване на грешки много ясен.

Струните обаче могат да бъдат скъпи за съхранение. Цяло число изисква 32 бита, за да съхранява стойността си, докато низ изисква 16 бита на символ (поради поддръжката на Unicode). Например числото 49858712 може да се съхранява в 32 бита, но низът TURQUOISEще изисква 144 бита. Ако съхранявате хиляди обекти с цветни атрибути, тази относително малка разлика в битовете (между 32 и 144 в този случай) може да се добави бързо. Така че нека вместо това използваме целочислени стойности. Какво е решението на този проблем? Ще запазим низовите стойности, защото те са важни за представяне, но няма да ги съхраняваме.

Версиите на Java от 1.1 нататък могат автоматично да сериализират обекти, стига да изпълняват Serializableинтерфейса. За да попречите на Java да съхранява чужди данни, трябва да декларирате такива променливи с transientключовата дума. Така че, за да съхраним целочислените стойности, без да съхраняваме низовото представяне, ние декларираме атрибута низ за преходен. Ето новия клас, заедно с достъп до атрибутите на цяло число и низ:

публичен клас Color реализира java.io.Serializable {private int стойност; частно преходно име на низ; публичен статичен краен цвят ЧЕРВЕН = нов цвят (0, "Червен"); публичен статичен краен цвят СИН = нов цвят (1, "син"); публичен статичен краен цвят ЗЕЛЕН = нов цвят (2, "Зелен"); private Color (int стойност, име на низ) {this.value = value; this.name = име; } public int getValue () {върната стойност; } публичен String toString () {върнато име; }}

Сега можем ефективно да съхраняваме екземпляри от константния тип Color. Но какво да кажем за възстановяването им? Това ще бъде малко сложно. Преди да продължим по-нататък, нека разширим това в рамка, която ще се справи с всички гореспоменати клопки за нас, позволявайки ни да се съсредоточим върху простия въпрос за определяне на типовете.

Постоянният тип рамка

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

Благодарение на нашата организация на hashtable-of-hashtables е невероятно лесно да се изложи функционалността за изброяване, предлагана от изпълнението на Ерик. Единственото предупреждение е, че сортирането, което предлага дизайнът на Ерик, не е гарантирано. Ако използвате Java 2, можете да замените сортираната карта за вътрешните хаштаби. Но, както казах в началото на тази колона, в момента се занимавам само с версията 1.1 на JDK.

Единствената логика, необходима за изброяване на типовете, е да извлече вътрешната таблица и да върне нейния списък с елементи. Ако вътрешната таблица не съществува, ние просто връщаме null. Ето целия метод: