Użyj stałych typów, aby zapewnić bezpieczniejszy i czystszy kod

W tym samouczku rozwiniemy ideę wyliczeniowych stałych, opisaną w artykule Erica Armstronga „Tworzenie wyliczeniowych stałych w Javie”. Zdecydowanie polecam przeczytanie tego artykułu, zanim zagłębisz się w ten, ponieważ założę, że znasz pojęcia związane z wyliczanymi stałymi, a ja rozwinę część przykładowego kodu, który przedstawił Eric.

Pojęcie stałych

Zajmując się wyliczonymi stałymi, omówię wyliczoną część koncepcji na końcu artykułu. Na razie skupimy się tylko na stałym aspekcie. Stałe to w zasadzie zmienne, których wartość nie może się zmienić. W C / C ++ słowo kluczowe constsłuży do deklarowania tych stałych zmiennych. W Javie używasz słowa kluczowego final. Jednak narzędzie tutaj wprowadzone nie jest zwykłą zmienną pierwotną; jest to rzeczywista instancja obiektu. Instancje obiektów są niezmienne i niezmienne - ich stan wewnętrzny nie może być modyfikowany. Jest to podobne do wzorca singleton, w którym klasa może mieć tylko jedną instancję; w tym przypadku jednak klasa może mieć tylko ograniczony i wstępnie zdefiniowany zestaw instancji.

Głównymi powodami używania stałych są przejrzystość i bezpieczeństwo. Na przykład poniższy fragment kodu nie jest oczywisty:

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

Na podstawie tego kodu możemy stwierdzić, że ustawiany jest kolor. Ale jaki kolor reprezentuje 5? Gdyby ten kod został napisany przez jednego z tych nielicznych programistów, który komentuje jego pracę, odpowiedź mogłaby znaleźć się u góry pliku. Ale bardziej prawdopodobne jest, że będziemy musieli poszukać starych dokumentów projektowych (jeśli w ogóle istnieją), aby uzyskać wyjaśnienie.

Bardziej przejrzystym rozwiązaniem jest przypisanie wartości 5 zmiennej o znaczącej nazwie. Na przykład:

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

Teraz możemy od razu powiedzieć, co się dzieje z kodem. Kolor jest ustawiany na czerwony. To jest znacznie czystsze, ale czy jest to bezpieczniejsze? Co się stanie, jeśli inny koder się pomyli i zadeklaruje różne wartości w ten sposób:

public static final int RED = 3; public static final int GREEN = 5;

Teraz mamy dwa problemy. Przede wszystkim REDnie ma już ustawionej prawidłowej wartości. Po drugie, wartość koloru czerwonego jest reprezentowana przez zmienną o nazwie GREEN. Być może najbardziej przerażające jest to, że ten kod skompiluje się dobrze, a błąd może nie zostać wykryty, dopóki produkt nie zostanie wysłany.

Możemy rozwiązać ten problem, tworząc ostateczną klasę kolorów:

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

Następnie poprzez przegląd dokumentacji i kodu zachęcamy programistów do korzystania z niego w następujący sposób:

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

Mówię zachęcam, ponieważ projekt w tym wykazie kodu nie pozwala nam zmusić kodera do przestrzegania; kod będzie się nadal kompilował, nawet jeśli nie wszystko jest w porządku. Tak więc, chociaż jest to trochę bezpieczniejsze, nie jest całkowicie bezpieczne. Chociaż programiści powinni używać tej Colorklasy, nie jest to wymagane. Programiści mogli bardzo łatwo napisać i skompilować następujący kod:

 setColor (3498910); 

Czy setColormetoda rozpoznaje, że ta duża liczba jest kolorem? Prawdopodobnie nie. Jak więc możemy się chronić przed tymi nieuczciwymi programistami? Tutaj na ratunek przychodzą typy stałych.

Zaczynamy od przedefiniowania podpisu metody:

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

Teraz programiści nie mogą przekazać dowolnej wartości całkowitej. Są zmuszeni dostarczyć ważny Colorprzedmiot. Przykładowa implementacja może wyglądać następująco:

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

Nadal pracujemy z czystym, czytelnym kodem i jesteśmy znacznie bliżej osiągnięcia absolutnego bezpieczeństwa. Ale to jeszcze nie wszystko. Programista nadal ma trochę miejsca na siejenie spustoszenia i może dowolnie tworzyć nowe kolory, takie jak:

public void someMethod () {setColor (new Color ("Cześć, mam na imię Ted.")); }

Zapobiegamy tej sytuacji, czyniąc Colorklasę niezmienną i ukrywając instancję przed programistą. Każdy rodzaj koloru (czerwony, zielony, niebieski) jest dla nas singletonem. Osiąga się to, ustawiając konstruktor jako prywatny, a następnie udostępniając uchwyty publiczne ograniczonej i dobrze zdefiniowanej liście instancji:

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

W tym kodzie w końcu osiągnęliśmy absolutne bezpieczeństwo. Programista nie może tworzyć fałszywych kolorów. Można używać tylko zdefiniowanych kolorów; w przeciwnym razie program się nie skompiluje. Tak wygląda teraz nasza realizacja:

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

Trwałość

OK, teraz mamy czysty i bezpieczny sposób radzenia sobie ze stałymi typami. Możemy stworzyć obiekt z atrybutem koloru i mieć pewność, że wartość koloru będzie zawsze aktualna. Ale co, jeśli chcemy przechowywać ten obiekt w bazie danych lub zapisać go do pliku? Jak zachowujemy wartość koloru? Musimy zmapować te typy na wartości.

We wspomnianym powyżej artykule JavaWorld Eric Armstrong użył wartości ciągów. Używanie łańcuchów zapewnia dodatkową korzyść polegającą na tym, że daje ci coś sensownego do zwrócenia w toString()metodzie, co sprawia, że ​​wyniki debugowania są bardzo jasne.

Jednak przechowywanie sznurków może być kosztowne. Liczba całkowita wymaga 32 bitów do przechowywania swojej wartości, podczas gdy ciąg znaków wymaga 16 bitów na znak (ze względu na obsługę Unicode). Na przykład liczba 49858712 może być przechowywana w 32 bitach, ale ciąg TURQUOISEwymagałby 144 bitów. Jeśli przechowujesz tysiące obiektów z atrybutami kolorów, ta stosunkowo niewielka różnica w bitach (w tym przypadku między 32 a 144) może szybko się sumować. Zamiast tego użyjmy wartości całkowitych. Jakie jest rozwiązanie tego problemu? Zachowamy wartości ciągów, ponieważ są one ważne dla prezentacji, ale nie będziemy ich przechowywać.

Wersje Java od 1.1 mogą automatycznie serializować obiekty, o ile implementują Serializableinterfejs. Aby uniemożliwić Javie przechowywanie zbędnych danych, należy zadeklarować takie zmienne za pomocą transientsłowa kluczowego. Tak więc, aby przechowywać wartości całkowite bez przechowywania reprezentacji ciągu, deklarujemy, że atrybut ciągu jest przejściowy. Oto nowa klasa wraz z metodami dostępu do atrybutów typu integer i string:

public class Color implementuje java.io.Serializable {prywatna wartość int; prywatna przejściowa nazwa String; public static final Color RED = nowy kolor (0, „czerwony”); public statyczny końcowy kolor NIEBIESKI = nowy kolor (1, „niebieski”); public statyczny końcowy kolor ZIELONY = nowy kolor (2, „zielony”); prywatny Kolor (wartość int, nazwa ciągu) {this.value = wartość; this.name = name; } public int getValue () {wartość zwracana; } public String toString () {nazwa powrotu; }}

Teraz możemy efektywnie przechowywać instancje typu stałego Color. Ale co z ich przywróceniem? To będzie trochę trudne. Zanim przejdziemy dalej, rozwińmy to do struktury, która poradzi sobie ze wszystkimi wyżej wymienionymi pułapkami, pozwalając nam skupić się na prostej kwestii definiowania typów.

Struktura typu stałego

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

Dzięki naszej organizacji z tablicami haszującymi, niezwykle łatwo jest wyeksponować funkcjonalność wyliczania oferowaną przez implementację Erica. Jedynym zastrzeżeniem jest to, że sortowanie, które oferuje projekt Erica, nie jest gwarantowane. Jeśli używasz języka Java 2, możesz zastąpić posortowaną mapę wewnętrznymi tabelami mieszania. Ale, jak powiedziałem na początku tej kolumny, teraz interesuje mnie tylko wersja 1.1 JDK.

Jedyną logiką wymaganą do wyliczenia typów jest pobranie tabeli wewnętrznej i zwrócenie jej listy elementów. Jeśli tabela wewnętrzna nie istnieje, po prostu zwracamy wartość null. Oto cała metoda: