Genericsを使ったJavaBean
JavaBeanを作る時にGetter,Setterを書くのメンドウですよね。Rubyみたいに
attr :foo
とかでアクセサを作ってくれれば良いけどあいにくJavaにそんな機能はありません。(Java SE 7でできるらしいけど。)楽する手段としてMapを使うという手がありますが、これはこれで以下の問題があってほとんど使われていない(と思う)。
- 取り得る値の型チェックが効かない。
- 指定できるキーがどれかわからない。関係ないモデルのキーが指定できてしまう。
でも、Java5から使えるようになったGeneicsを使えば、上の問題をクリアしつつMapをBeanに仕立て上げることができるんじゃね、と思って作ってみました。
戦略
ネタ的には タイプセーフなPropertiesを作る。に近いです。
- API
- Map風なAPIとしてアクセサを提供する。
- 「get(<キー>)」で属性返す。
- 「set(<キー>、<値>)」で属性を設定。
- Map風なAPIとしてアクセサを提供する。
- プロパティを取得するためのキー
- 値の型はプロパティごとに決まるので、プロパティの型パラメータで値の型を指定できるようにする。
- 別の型パラメータで「このプロパティを持つモデルの型」を指定できるようにする。
- モデル(Bean)のAPI
- setでは、プロパティの型パラメータにあった値しか指定できないように制限する。
- getで返す値も、プロパティの型パラメータにあったものを返すようにする。
- また、モデルクラス自体を型パラメータに持つPropertyしかキーとして受け付けないようにし、関係ないモデルでプロパティが使われることを防ぐ。
実装
まずはプロパティ。列挙型には型パラメータが渡せないので独自に作ります。
/** * プロパティのキー * * @param <M> プロパティを持つモデルの型 * @param <T> プロパティが取り得るデータの型 * * @version $Revision:$ * @author $Author:$ */ public class Property<M, T> { /**キー*/ private final String key; /** * コンストラクタ * @param key キー */ protected Property( String key ) { this.key = key; } /* 継承元のクラスのJavaDocを参照 */ public boolean equals ( Object obj ) { if ( obj == null ) { return false; } if ( obj instanceof Property) { Property that = (Property) obj; return that.key == null ? key == null : that.key.equals( key ); } return false; } /* 継承元のクラスのJavaDocを参照 */ public int hashCode () { return key.hashCode(); } }
Beanは抽象クラスの「AbstractBean」を継承して作ります。「AbstractBean」はMapと、プロパティを受け取って値を返す汎用的なアクセサを持ちます。
import java.util.HashMap; import java.util.Map; /** * Beanの抽象基底クラス。 * * @version $Revision:$ * @author $Author:$ * * @param <M> Beanの型 */ public abstract class AbstractBean<M> { /*** * 値の格納先 */ private Map<Property<M, ?>, Object> props = new HashMap<Property<M, ?>, Object>(); /** * 値を取得する。 * @param <V> 値の型 * @param key プロパティキー * @return 値 */ public <V> V get( Property<M, V> key ) { return (V) props.get( key ); } /** * 値を設定する。 * @param <V> 値の型 * @param key プロパティキー * @param value 値 */ public <V> void set( Property<M, V> key, V value ) { props.put( key, value ); } }
最後はBeanの実体です。定数でプロパティ定義を持ちます。
/** * Kitten. * * @version $Revision:$ * @author $Author:$ */ public final class Kitten extends AbstractBean<Kitten> { /** * Kittenのプロパティ * @param <V> プロパティが取り得る値の型 */ private static final class KittenProperty<V> extends Property<Kitten, V> { /** * コンストラクタ * @param key キー */ protected KittenProperty ( String key ) { super( key ); } } /**プロパティ:名前*/ public static final KittenProperty<String> NAME = new KittenProperty<String>( "NAME" ); /**プロパティ:年齢*/ public static final KittenProperty<Integer> AGE = new KittenProperty<Integer>( "AGE" ); }
テスト用にもう1つ。
/** * Penguin. */ public final class Penguin extends AbstractBean<Penguin> { /** * Penguinのプロパティ * @param <V> プロパティが取り得る値の型 */ private static final class PenguinProperty<V> extends Property<Penguin, V> { /** * コンストラクタ * @param key キー */ protected PenguinProperty ( String key ) { super( key ); } } /**プロパティ:名前*/ public static final PenguinProperty<String> NAME = new PenguinProperty<String>( "NAME" ); /**プロパティ:年齢*/ public static final PenguinProperty<Integer> AGE = new PenguinProperty<Integer>( "AGE" ); }
これだけ。以下のような感じで使えます。
Kitten mii = new Kitten(); // get, setでの値の型チェックが効く。 mii.set( Kitten.NAME, "mii" ); mii.set( Kitten.AGE, 1 ); //mii.set( Kitten.NAME, 1 ); // コンパイルエラー //mii.set( Kitten.AGE, "" ); // コンパイルエラー String name = mii.get( Kitten.NAME ); int age = mii.get( Kitten.AGE ); //int age2 = mii.get( Kitten.NAME ); // コンパイルエラー //String name2 = mii.get( Kitten.AGE ); // コンパイルエラー // 他のモデルクラス用のキーも指定できない。 //mii.set( Penguin.NAME, "penny" ); // コンパイルエラー //mii.set( Penguin.AGE, 1 ); // コンパイルエラー
メリット
Map likeなAPIですが、指定可能なキーの型チェックも、値の型チェックも効きます。プロパティの追加は定数を追加するだけでOK。フィールドを足してGetメソッド書いて・・とかしなくて済みます。あと、コードも短くなる!
既知の問題
ただし、以下のような問題はあったりします。
// 次のようなプロパティが作られた場合はエラーにならない・・・。 // 有効なKittenのプロパティとして認識される。 class FooProperty<V> extends Property<Kitten, V> { protected FooProperty ( String key ) { super( key ); } } FooProperty<String> foo = new FooProperty<String>("foo"); String str = mii.get( foo ); mii.set( foo, "hoge" );
まぁ、こういう使い方はするな、ということで運用で回避すべし。