より安全でクリーンなコードのために定数型を使用する

このチュートリアルでは、EricArmstrongの「Javaで列挙型定数を作成する」で説明されている列挙型定数の概念を拡張します。列挙型定数に関連する概念に精通していることを前提としているため、この記事に没頭する前にその記事を読むことを強くお勧めします。Ericが提示したサンプルコードのいくつかを拡張します。

定数の概念

列挙型定数を扱う際に、記事の最後で概念の列挙型部分について説明します。今のところ、私たちは一定の側面に焦点を当てます。定数は基本的に、値を変更できない変数です。 C / C ++では、キーワードconstを使用してこれらの定数変数を宣言します。 Javaでは、キーワードを使用しますfinal。ただし、ここで紹介するツールは単なるプリミティブ変数ではありません。これは実際のオブジェクトインスタンスです。オブジェクトインスタンスは不変で変更できません。内部状態は変更できません。これは、クラスが1つのインスタンスしか持てないシングルトンパターンに似ています。ただし、この場合、クラスには、限定された事前定義されたインスタンスのセットしか含まれない場合があります。

定数を使用する主な理由は、明快さと安全性です。たとえば、次のコードは自明ではありません。

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

このコードから、色が設定されていることを確認できます。しかし、5は何色を表していますか?このコードが、彼または彼女の仕事についてコメントする珍しいプログラマーの1人によって書かれた場合、ファイルの先頭に答えが見つかる可能性があります。しかし、説明のために、古い設計ドキュメント(存在する場合でも)を探し回る必要がある可能性が高くなります。

より明確な解決策は、意味のある名前の変数に値5を割り当てることです。例えば:

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

これで、コードで何が起こっているのかをすぐに知ることができます。色は赤に設定されています。これははるかにクリーンですが、より安全ですか?別のコーダーが混乱し、次のように異なる値を宣言した場合はどうなりますか?

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

今、2つの問題があります。まず第一に、REDもはや正しい値に設定されていません。次に、redの値は、という名前の変数で表されますGREEN。おそらく最も恐ろしいのは、このコードが正常にコンパイルされ、製品が出荷されるまでバグが検出されない可能性があることです。

明確なカラークラスを作成することで、この問題を修正できます。

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

次に、ドキュメントとコードレビューを介して、プログラマーが次のように使用することをお勧めします。

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

そのコードリストの設計では、コーダーに強制的に準拠させることができないため、私は奨励すると言います。すべてが完全に整っていなくても、コードはコンパイルされます。したがって、これは少し安全ですが、完全に安全というわけではありません。プログラマーColorクラス使用する必要がありますが、必須ではありません。プログラマーは、次のコードを非常に簡単に記述してコンパイルできます。

 setColor(3498910); 

setColorこの方法は、この大きな数を色として認識しますか?おそらくそうではありません。では、どうすればこれらの不正なプログラマーから身を守ることができるでしょうか。そこで、定数タイプが役に立ちます。

まず、メソッドのシグネチャを再定義します。

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

現在、プログラマーは任意の整数値を渡すことができません。彼らは有効なColorオブジェクトを提供することを余儀なくされています。これの実装例は次のようになります。

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

私たちはまだクリーンで読みやすいコードを扱っており、絶対的な安全性の達成にはるかに近づいています。しかし、私たちはまだそこにいません。プログラマーにはまだ大混乱をもたらす余地があり、次のように任意に新しい色を作成できます。

public void someMethod(){setColor(new Color( "こんにちは、私の名前はテッドです。")); }

Colorクラスを不変にし、プログラマーからインスタンス化を隠すことで、この状況を防ぎます。それぞれの異なるタイプの色(赤、緑、青)をシングルトンにします。これは、コンストラクターをプライベートにしてから、パブリックハンドルを制限され明確に定義されたインスタンスのリストに公開することで実現されます。

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(); }

このコードでは、最終的に絶対的な安全性を達成しました。プログラマーは偽の色を作ることはできません。定義された色のみを使用できます。そうしないと、プログラムはコンパイルされません。これが私たちの実装が今どのように見えるかです:

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

永続性

さて、これで定数型を処理するためのクリーンで安全な方法ができました。color属性を持つオブジェクトを作成し、color値が常に有効であることを確認できます。しかし、このオブジェクトをデータベースに保存したり、ファイルに書き込んだりする場合はどうでしょうか。色の値を保存するにはどうすればよいですか?これらのタイプを値にマップする必要があります。

上記のJavaWorldの記事では、EricArmstrongが文字列値を使用していました。文字列を使用すると、toString()メソッドで返す意味のあるものが得られるという追加のボーナスが提供され、デバッグ出力が非常に明確になります。

ただし、文字列は保存に費用がかかる場合があります。整数はその値を格納するために32ビットを必要としますが、文字列は文字ごとに16ビットを必要とします(Unicodeサポートのため)。たとえば、数値49858712は32ビットで格納できますが、文字列TURQUOISEには144ビットが必要です。色属性を持つ何千ものオブジェクトを保存している場合、この比較的小さなビットの違い(この場合は32から144の間)はすぐに加算されます。代わりに整数値を使用しましょう。この問題の解決策は何ですか?文字列値は表示に重要であるため保持しますが、保存しません。

1.1以降のバージョンのJavaは、Serializableインターフェースを実装している限り、オブジェクトを自動的にシリアル化できます。 Javaが無関係なデータを格納しないようにするには、そのような変数をtransientキーワードで宣言する必要があります。したがって、文字列表現を保存せずに整数値を保存するために、文字列属性を一時的であると宣言します。新しいクラスと、整数属性および文字列属性へのアクセサーは次のとおりです。

パブリッククラスColorはjava.io.Serializable {private intvalue;を実装します。プライベート一時文字列名。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"); private Color(int value、String name){this.value = value; this.name = name; } public int getValue(){戻り値; } public 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組織のおかげで、Ericの実装によって提供される列挙機能を公開するのは非常に簡単です。唯一の注意点は、Ericの設計が提供する並べ替えが保証されていないことです。Java 2を使用している場合は、ソートされたマップを内部ハッシュテーブルに置き換えることができます。ただし、このコラムの冒頭で述べたように、現在はJDKの1.1バージョンのみに関心があります。

タイプを列挙するために必要な唯一のロジックは、内部テーブルを取得してその要素リストを返すことです。内部テーブルが存在しない場合は、単にnullを返します。全体の方法は次のとおりです。