Hatena::ブログ(Diary)

しげるメモ このページをアンテナに追加 RSSフィード

2009-01-19

[] enumをもうちょっと使う

enum basics - しげるメモで基本的なことをおさらいしたので、私がよく使ってるenum関係のパターンをいくつか。


ステートパターンをenum

enumを使うようになってまず一番変わったのが、

private static final int SOME_STATE = 1;

みたいな典型的なアンチパターンを書かなくなりました。これはステートパターンを非常に書きやすくなったためだと思います。

よくこんなコード書いてます。本当はもうちょっとロック処理とかを書かないとだめですが、ステート部分は大抵こんな感じです。

public class Lifecycle {
    
    // 初期状態はINITIALに固定
    private State state = State.INITIAL;
    
    // 外側に公開する状態遷移メソッド
    public void start() {
        state = state.start(this);
    }
    public void stop() {
        state = state.stop(this);
    }
    public void disopse() {
        state.stop(this);
        state = State.DISPOSED;
        // 破棄のための処理
    }
    
    // state == ACTIVATED のときのみ実行できる処理
    public void someMethod() {
        if (state != State.ACTIVATED) {
            throw new IllegalStateException();
        }
        // ...
    }
    
    // 外側に公開しない状態遷移のための処理
    void doInitialize() {
        // ...
    }
    void doActivate() {
        // ...
    }
    void doDeactivate() {
        // ...
    }
    
    // 状態オブジェクト
    private enum State {
        INITIAL {
            @Override public State start(Lifecycle outer) {
                outer.doInitialize();
                outer.doActivate();
                return ACTIVATED;
            }
            @Override public State stop(Lifecycle outer) {
                return INITIAL;
            }
        },
        ACTIVATED {
            @Override public State start(Lifecycle outer) {
                return ACTIVATED;
            }
            @Override public State stop(Lifecycle outer) {
                outer.doDeactivate();
                return DEACTIVATED;
            }
        },
        DEACTIVATED {
            @Override public State start(Lifecycle outer) {
                outer.doActivate();
                return ACTIVATED;
            }
            @Override public State stop(Lifecycle outer) {
                return DEACTIVATED;
            }
        },
        DISPOSED {
            @Override public State start(Lifecycle outer) {
                return DISPOSED;
            }
            @Override public State stop(Lifecycle outer) {
                return DISPOSED;
            }
        },
        ;
        public abstract State start(Lifecycle outer);
        public abstract State stop(Lifecycle outer);
    }
}

このおかげで、ライフサイクルの管理と状態遷移表をほとんどStateに集約させられてます。

これくらい程度の規模の状態だと、クラスのメンバにprivate enumで状態用の定数を作っちゃってますが、そこだけテストしたい人はパッケージプライベートにしとくくらいの感じで。


シングルトンパターンをenum

フィールドに"public static final Hoge INSTANCE = new Hoge();"みたいに書いちゃうタイプのシングルトンは、簡単にenumに書き換えられます。

まじめにシングルトンパターンを書くと意外と大変です。

たいてい面倒になってシリアル化をあきらめてしまいます。ただ、シングルトンオブジェクトシリアル化しない利点はあまりないため、enumでシングルトンを実現してやればこのあたりも勝手にやってくれて幸せ。

やり方は簡単で、enumにシングルトンインスタンス用の定数を一つだけ宣言してやるだけです。で、後半のクラス本体でシングルトンインスタンスの挙動を記述する感じ。

public enum MySingleton {
    
    /**
     * 唯一のインスタンス。
     */
    INSTANCE,
    ;
    
    // あとは好きに処理を書く
}

ストラテジパターンをenum

動的な引数を使わないストラテジパターンは、enumで宣言するのがオススメです。たとえば、java.util.Comparatorのいろいろな実装とか。

public enum StringCmps implements Comparator<String> {
    
    NUMERIC {
        public int compare(String o1, String o2) {
            return
                Double.valueOf(o1).compareTo(Double.valueOf(o2));
        }
    },
    REVERSED {
        public int compare(String o1, String o2) {
            String r1 = new StringBuilder(o1).reverse().toString();
            String r2 = new StringBuilder(o2).reverse().toString();
            return r1.compareTo(r2);
        }
    },
    ...
}

ここのポイントは、ストラテジの型をインターフェースとして切り出しておいて、enumに実装させるところです。最初からenumでストラテジの型を決め打ちすると、ストラテジを増やせなくなります。

特にComparatorはSerializableにしとかないと、TreeMapとかに食わせた際にそいつもシリアライズできなくて残念な気分になります。先ほどのシングルトンと同様にenumで勝手にSerializableにさせておくと幸せになれます。

こいつの問題は、1ファイルに戦略を詰め込む必要があるので、enumのファイルサイズがでかくなる点です。この辺はトレードオフかと思います。

ちなみに、どうしてもenumでまずくなった場合には、前回やったクラスへの書き換えと同じようなことをすれば、参照元のコードを変更せずにclassに戻すこともできます。あまり積極的にそうしたいという場面に出会ったことがないので、どうなるかは微妙なラインです。


コンストラクタ引数から定数を逆引きする

enumにはvalueOf(String)という、定数名から定数を逆引きするメソッドが用意されますが、それ以外にもコンストラクタに渡した文字列などから、その定数を逆引きしたい場面とか結構あります。

で、このコードを毎回作るのが面倒なので、パターン化して使ってます。

public enum Operator {
    ADD("+"),
    SUB("-"),
    MUL("*"),
    DIV("/"),
    REM("%"),
    ;
    private final String symbol;
    private Operator(String symbol) {
        this.symbol = symbol;
    }
    // Operator -> Symbol
    public String getSymbol() {
        return symbol;
    }
    // Symbol -> Operator
    public static Operator fromSymbol(String symbol) {
        return SymbolToOperator.get(symbol);
    }
    
    // 逆参照表を遅延初期化するクラス
    private static class SymbolToOperator {

        private static final Map<String, Operator> REVERSE_DICTIONARY;
        static {
            Map<String, Operator> map = new HashMap<String, Operator>();
            for (Operator elem : Operator.values()) {
                map.put(elem.getSymbol(), elem);
            }
            REVERSE_DICTIONARY = Collections.unmodifiableMap(map);
        }

        static Operator get(String key) {
            return REVERSE_DICTIONARY.get(key);
        }
    }
}

Operatorに直接クラス初期化子を書かないでOperator.SymbolToOperatorクラスに書いてますが、これはクラス初期化子内で循環参照を行っている場合に、初期化に失敗することがあるのを回避するためです。今回は文字列とOperator型くらいしか触っていないのでこんなことやらなくてもいいですが、Eclipseでテンプレ作っているので自動的にこうなってます。

private static class ${KeyName}To${enclosing_type} {
    private static final Map<${KeyType}, ${enclosing_type}> REVERSE_DICTIONARY;
    static {
        Map<${KeyType}, ${enclosing_type}> map = new HashMap<${KeyType}, ${enclosing_type}>();
        for (${enclosing_type} elem : ${enclosing_type}.values()) {
            map.put(elem.get${KeyName}(), elem);
        }
        REVERSE_DICTIONARY = Collections.unmodifiableMap(map);
    }
    static ${enclosing_type} get(${KeyType} key) {
        return REVERSE_DICTIONARY.get(key);
    }
}

上記に (${KeyName}, ${KeyType}) = (Symbol, String) をあててやれば、SymbolToOperatorクラスが勝手に作成されます。importが足らないので追加してやる必要アリ。


タグ付きオブジェクトのタグに

Eclipseとかのコードを読んでいると、よくこんなコードを見かけます。

IResource r = ...;
if (r.getType() == IResource.FILE) {
  IFile file = (IFile) r;
  ...
}

これくらいはインターフェースの階層で表現すればいいんじゃと思うことはたまにありますが、一概にそうでもないときがあります。

  • たいした機能もないのにインターフェースが増えすぎる
    • 直交する属性をインターフェースで表現してると組み合わせ爆発が発生
    • 継承は機能の拡張ができるけど、機能の制限を表現できないので直観に合わないときがある
  • r instanceof IFile, r instanceof IFolder とかは実装に一定の制約を与える
    • 対象のリソースproxy上に存在してまだどちらの型か分からず、implements IFile, IFolder とかやっていると限りなくアウトになる
    • r.getClass() も同じ理由でアウト (こっちはさらにAOPと相性が悪すぎる)
    • そもそもinstanceofがあまり好きじゃない

そうはいっても、EclipseのIResource.getType()はint型を返すのでアレなのですが、これをenumで表現してるとインターフェース継承階層とは別の世界でいろいろな属性を定義できるので便利です。

たとえば、Javaの修飾子(publicとか)を表現するノードがあったとき、

  • PublicNode
  • ProtectedNode
  • PrivateNode
  • ...

と言った具合に一つ一つの型を作るのはあまりにアフォらしいので、大抵はModifierNode + ModifierKindというタグ付きのオブジェクトを用意しています。

public enum ModifierKind {
    // public, protected, private は可視性に関する修飾子
    PUBLIC(EnumSet.of(Attribute.VISIBILITY)),
    PROTECTED(EnumSet.of(Attribute.VISIBILITY)),
    PRIVATE(EnumSet.of(Attribute.VISIBILITY)),
    // abstract, nativeが指定されるとメソッド本体がなくなる
    ABSTRACT(EnumSet.of(Attribute.NO_BODY)),
    NATIVE(EnumSet.of(Attribute.NO_BODY)),
    // staticはどうでもいい
    STATIC(Collections.<Attribute>emptySet()),
    ;
    
    // 定数の属性をさらに定数で表す
    private enum Attribute {
        VISIBILITY,
        NO_BODY,
    }
    
    private Set<Attribute> attributes;
    private ModifierKind(Set<Attribute> attributes) {
        this.attributes = attributes;
    }
    public boolean isVisibility() {
        return attributes.contains(Attribute.VISIBILITY);
    }
    public boolean hasBody() {
        return attributes.contains(Attribute.NO_BODY) == false;
    }
}

で、それぞれの修飾子が持つ特性をさらにModifierKind.Attributeであらわしておいて、enumコンストラクタに渡してやります。

ノード側はこんな感じで。

public class ModifierNode {
    // 種類を表すタグ
    private ModifierKind kind;
    // ソースコード上の開始位置
    private int startOffset;
    // ソースコード上の長さ
    private int length;
    
    public ModifierKind getKind() {
        return kind;
    }
}

ノードが持つ情報は修飾子の種類以外にもありますが、その種類と種類ごとの特性に関する情報だけはModifierKindに集約しておきます。

こう書くと、修飾子ごとの特性を宣言的に書けるので、インターフェースが無駄に増えたりクラスを行ったり来たりしないと情報が集まらないなんてのはあんまりありません(public boolean isVisibility() { return false; }みたいなのがそこらじゅうでオーバーライドされてると爆発しろとか思う)。

使い方はこんな感じで。

ModifierNode node = ...;
if (node.getKind().isVisibility()) {
    // ...
}

特性を増やしたり変更したりするときも、ModifierNodeに変更を加えずにModifierKindをちょっと触るだけでOKです。

これくらいだったら別にタグ付きオブジェクトというほどでもないですが、まっとうな言語のDOM構造を用意しようとしてるときなどは、タグ付きオブジェクトをつかってうまい具合に特性に関する情報を分離していないと発狂しそうになります。タグ付きオブジェクトはあまりほめられたパターンではないですが、ここぞという場面で使うとかなり楽になります。


switch嫌いの人へ

私は比較的switch文も使うことがありますが、世の中的にはあまりswitch文が好まれないようです。enumでswitchの書き換えパターン。

とりあえず、enumをこんな感じにします。

public enum SwitchSample {
    HOGE {
        @Override public void perform(Switch sw) {
            sw.caseOfHoge();
        }
    },
    FOO {
        @Override public void perform(Switch sw) {
            sw.caseOfFoo();
        }
    },
    BAR {
        @Override public void perform(Switch sw) {
            sw.caseOfBar();
        }
    },
    ;
    public interface Switch {
        void caseOfHoge();
        void caseOfFoo();
        void caseOfBar();
    }
    
    public abstract void perform(Switch sw);
}

ポイントは、enumの中にSwitchっていう名前のインターフェース作っておくこと。

んで、こんな感じで使えます。

public static void main(String[] args) {
    SwitchSample value = ...;

    BEFORE: switch (value) {
    case HOGE:
        System.out.println("ほげほげ");
        break;
    case FOO:
        System.out.println("ふ");
        break;
    case BAR:
        System.out.println("ばー");
        break;
    }
    
    AFTER: value.perform(new Switch() {
        public void caseOfHoge() {
            System.out.println("ほげほげ");
        }
        public void caseOfFoo() {
            System.out.println("ふ");
        }
        public void caseOfBar() {
            System.out.println("ばー");
        }
    });
}

breakが消えた。

defaultラベルも使えないこともない。

public abstract class SwitchDefault implements Switch {
    public void caseOfHoge() {
        otherwise(HOGE);
    }
    public void caseOfFoo() {
        otherwise(FOO);
    }
    public void caseOfBar() {
        otherwise(BAR);
    }
    protected abstract void otherwise(SwitchSample value);
}
...
public static void main(String[] args) {
    SwitchSample value = ...;
    ...
    DEFAULT: value.perform(new SwitchDefault() {
        @Override public void caseOfHoge() {
            System.out.println("ほげほげ");
        }
        @Override protected void otherwise(SwitchSample value) {
            System.out.println("UNKNOWN: " + value);
        }
    });
}

えーと、こういう書き方もできるよっていう感じで。

  • 利点
    • switch使わなくて済む
    • 外部のクラスに切り出しても動くので、再利用性はたぶん上がる
  • 欠点
    • コード量が増える
    • いろいろとアレ

ちなみに、 最近のVisitor - しげるメモの伏線を回収しておくと、ジェネリクスを多用することで引数とか戻り値とか例外とかも使えます(Switch<R, P, E>とか書きたくないのでやめた)。

で、

まだあると思いますけど、大体enum周りはこんなもんで。

五十歩百歩五十歩百歩 2009/02/09 23:54 enumに@Overrideなんて使えるようになったんですね。
勉強になりました。...ψ(。。)メモメモ...

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証