#注意
以下に書かれていることは基本的にアンチパターンだ。ただ、アンチパターンの中には稀なケースで効果抜群に使用できるものも紛れこんでいる。以下、私自身も稀にしか使わない、けれどもそのケースでは効果抜群だった、とあるアンチパターンについて飼い慣らし方を説明する。
#突然だが、はじめに例外に対する指針を
- 例外は例外的状態にだけ使用するべきだ (Effective Java 項目39)
- チェック例外を不必要にスローしない (同項目41)
- できる限り標準例外を使う (同項目42)
- スローする全ての例外を文書化する(同項目44)
もとより名著「Effective Java」に楯突く気などないが、上記指針は私も全面的に賛成だ。例外はVMの中で稀にしか発生しない事象であり、最適化の対象としては優先順位が低い。それ故、なるべく例外ハンドリングを行なわないようにコードを記述した方がパフォーマンスが高い。例外はAPI設計を混乱させやすく、APIが例外を投げるよりはむしろ「事前に」例外的状態かどうかを検査できる手段を用意した方が使用者側のコードの柔軟性をもたらす。そういう訳で、API使用者に必ず try catch の使用を強制するチェック例外は不必要にスローせず、できる限り使用者が事前に知識を持っていると思われる標準例外を使うのが望ましい。さらに例え標準例外をスローするのだとしても、必ずAPIのドキュメントにどのような例外が発生する可能性があるか詳述するべきだ。
だが、しかし。私はこのベストプラクティスをあえて踏みはずしてコーディングすることもある、と告白する。何も下記のようなコードを書こうという訳じゃない。
try {
int i = 0;
while (true) array[i++].doSomething();
} catch (ArrayIndexOutOfBoundsException e) {}
このような例は制御フローを混乱させるあきらかなアンチパターンだ。しかし、Javaの例外には深いメソッド呼び出しから一度の操作で復帰できる、という特筆すべき特徴があり、これは他の方法では決して実現できない。丁度 C の setjmp longjmp のようなものだ。(というか歴史的な順序は当然逆だが) これがどのように役立つかというと、例えば何層にも重なった State パターンから一度で一番上位の State を遷移させることができるようになる。何を言っているのかわからないと思うので以下、具体例を出しつつもう少し詳しく述べる。
株式市場の注文マッチング
弊社は金融系SIを営む会社なので、株式市場のマッチングエンジンのようなものをコーディングすることがある。株式取引をしたことがない読者のために以下、簡単に売買注文がどのようにマッチングされるのか説明する。(以下の説明がわかりにくければ原典にあたって欲しい)
まず朝、モーニングセッション前に投資家からの売買注文を集めはじめる。(8:00 - 9:00) それを適宜、ブックとして表示していき、9:00になった瞬間に「板寄せ」という操作を行なって売買を成立させる。板寄せから先はざら場と呼ばれる状態になり売買成立方法は continuous auction とよばれる方法に移行する。ざら場では基本的に売買は連続的に行なわれていくが、売買成立価格が大きく変動しすぎてしまうケースでは、売買成立を停止させて特別気配(もしくはspecial quote)という状態になる。特別気配中には板寄せという先程と同様の操作で売買が成立するのを待ちつつ、一定の時間間隔で特別気配という特殊な値段を表示させつつブックの更新を行う。市場の閉まる時間が来ると最後にもう一度板寄せという操作を行なって売買を成立させて終了だ。
図にすると以下のようだ。
これを実装するには先述した2層に重なったStateパターンを使うのが良さそうだと理解してもらえるであろうか。我々もそのように理解しコーディングを行なった。しかし2層のStateパターンで実際にコーディングを進めていくうちに煩雑なコードになってしまうことに気がついた。一番の要因は注文のマッチングを行う層で発生する種々の事象により上位の市場モード自体の遷移が発生してしまうことによる。そこで我々は以下のようなコードで下位のStateから例外を投げることにより上位のStateの遷移が可能なようにした。
// このメソッドでまず最初に Event を受信、その後、MarketMode のインスタンス -> TradingMode のインスタンスと順に渡して行く
// ここで Event とは時間経過、もしくは売買注文が入ったことを示すオブジェクト
public void processEvent(Event event) {
while (true) {
try {
marketMode = marketMode.processEvent(event); // State パターン
return; // イベントを処理したら終了
} catch (MarketModeChangedException e) {
marketMode = e.nextMarketMode;
// イベント処理中に市場モードが遷移してしまったのでreturn しない
// 同じイベントを再度、新しい市場モードで処理
}
}
}
private static class MarketModeChangedException extends Exception {
// このアンチパターン内では例外のシングルトンを作っておくと良い(理由は後述)
public static final MarketModeChangedException singleton = new MarketModeChangedException();
public MarketMode nextMarketMode; // 次の市場モードを表現するオブジェクトをここに載せる
// 詳細は省略
}
public static class MarketMode { // Stateパターンにより市場のモード(BeforeOpening Zaraba SpecialQuote Closing) を表現する
public TradingMode tradingMode;
public MarketMode processEvent(Event event) throws MarketModeChangedException {
tradingMode = tradingMode.processEvent(event); // Stateパターン。ここでは下位のStateからの例外を意図的にスルー
}
// 詳細は省略
}
public static abstract class TradingMode { // Stateパターンにより注文取り扱いのモード (JustBooking Itayose ContinuousAuction SpecialQuoteAndBooking) を表現する
public abstract TradingMode processEvent(Event event) throws MarketModeChangedException;
// 詳細は省略
}
例えば MarketMode: Zaraba、TradeingMode: ContinuousAuction の状態で ContinuousAuction 内の深いメソッド呼び出し階層内で価格変動が大きすぎることを検知したら、その場で MarketModeChangedException に適切な次の状態(MarketMode: SpecialQuote、TradingMode: SpecialQuoteAndBooking)をセットしてスローする。すると未処理のイベントは次の(遷移後の)Stateが受信して処理が続行される、という訳だ。
さほど例外的でもない例外をつかう
さて以上で、ときには「さほど例外的でもない例外」を使用する価値がある場合もある、という部分の説明を終えた。(あまりうまく説明できた自信はないが…) ここでようやく本題の「アンチパターンの飼い慣らしかた」に話を進められる。私は、このような例外の使い方をする場合には基本的に冒頭で述べたベストプラクティスの逆をやるのが良い、と考えている。
まず、「項目39:例外は例外的状態にだけ使用するべきだ」は、本アンチパターンにおいてはそもそも逆の行動をしている。そして「項目41:チェック例外を不必要にスローしない」「項目42:できる限り標準例外を使う」に関して言えば、アンチパターン内では逆に非標準のチェック例外をスローするように書くべきである。なぜなら見慣れない非標準の例外がスローされていることがアンチパターンを用いてコーディングされている特殊なコードであることを示す適切なマークとなりうるからである。そのため、非チェック例外にしてマークをはずせてしまえるようにすべきではない。また、標準例外を使ってしまうとコードの他の部分から発生する標準例外と混じってしまい、フローを変えたいためなのか真の例外なのか判断をつけづらく意図せぬバグをつくりこむことになりやすい。
最後の項目である「項目44:スローする全ての例外を文書化する」、これに関しては特に逆をやる必要はない。当たり前だがアンチパターンにてコーディングされているものは適切に文書化しておく必要がある。また、このようなコードは密結合されたモジュール内の奥深くでひっそりと存在するのが望ましく、当たり前だがAPIなどを通じて「さほど例外的でもない例外」をスローしてしまうような設計は許されない。
最後にコードの性能に関して述べておくが、ある工夫をすると満足できる速度で動いてくれることが多い。それは「例外のシングルトンを作成しておく」というものだ。例外のハンドリングは概して性能が悪いが、それは主にスタックトレース生成にコストがかかることが理由である。そのためあらかじめ例外をシングルトンとして生成しておいてそれをスローする、というコーディングを行なうことにより性能の悪化を大部分防ぐことができる。
以上でこの「アンチパターンの飼い慣らしかた」についての話を終える。が、この手の少し危険な領域に身を守りながら入っていくための手法は世の中にそれほど情報として広まっていない。私の知る限りコーディングの指南書でもブログでもこのような手法に関して述べているものがない。であるから、これはあくまで「ぼくのかんがえるさいきょうの」飼い慣らしかたにすぎない。このようなアンチパターンを使用する際には at your own risk でお願いしたいし、もっと良い別の手法、あるいはアンチパターンの飼い慣らし方があればご教示いただきたいと思っている。