非チェック例外多用作戦のトレードオフ認識
まず、以下に持論を展開するにあたって、自分の立ち位置を明確にしよう。自分は「Webアプリケーション開発者」としてではなく「JavaによるWebアプリではない(デスクトップアプリ,コマンドラインアプリ,ライブラリ)アプリの開発者」として語る。まぁ、自分に一番馴染みの深いプロダクトとしてJiemamyが挙げられるわけだが、こいつはWebアプリじゃない。Eclipse上で動くアプリケーションであり、そしてMaven2によって呼ばれるCLIアプリでもあり、また、クラスライブラリである。
この視点からJavaにおけるチェック例外と非チェック例外の話を再び。
Javaにおけるthrows句は、メソッドシグネチャの一部であり、インターフェイスにも現れる情報である。今まで「Javadocは仕様だ」と言い続けて来たが、正確にはインターフェイス(シグネチャ+Javadoc)が仕様だ。*1
検査例外が使いにくいと感じるのは、マズい設計のAPIを修正する時だ。ただ、修正時にこの種の煩わしさを感じるのは、id:j5ik2oさんの言う「throwsのエントリ追加時」だけではない。以下の例を見てみよう。
「このライブラリ、使いにくいよな」 〜 マズいAPIの例
SomeAPI api = new SomeAPI(); api.doSomething(); // ←ここでよくわからないNPEが発生する
こりゃ使いにくいですわ。Javadocが無かったらまず最悪。どうすりゃいいのかは、中のコードを見るまで分かるまい。Javadocがあったとして「doSomething処理を使う前には、SomeAPIに対して、setFoo(...)とsetBar(...)を必ず実行すること。そうでない場合の動作は保証しない。」と書いてあったりする。
そうなんですか…とユーザは渋々書き換える。…それならコンストラクタで受け取れっつの、と突っ込みながら。
【コード1】
SomeAPI api = new SomeAPI();
api.setFoo(foo);
api.setBar(bar);
api.doSomething();
その時のユーザにとっての理想的なコードはこんな感じだ。
【コード2】
SomeAPI api = new SomeAPI(foo, bar);
api.doSomething();
API提供側は、使いにくさを自覚した際、コードを以下のversion1→2のように書き換える。まぁversion2のようにせず、version1のように設計する正当な理由は幾つか考えることができるが、ここでは「そんな理由は全くない」という前提で話を進める。正当な理由がないならば、version2の方が親切で使い易いAPIであると言える。
【SomeAPI version1】
public class SomeAPI { private Foo foo; private Bar bar; public void setFoo(Foo foo) { this.foo = foo; } public void setBar(Bar bar) { this.bar = bar; } public void doSomething() { ... } }
【SomeAPI version2】
public class SomeAPI { private Foo foo; private Bar bar; public SomeAPI(Foo foo, Bar bar) { // foo,barのnullチェックをココに書くべきだが、まぁ省略 this.foo = foo; this.bar = bar; } public void doSomething() { ... } }
一度リリースした「マズいAPI」を変更する
上記のソースコードをみると、メソッドシグニチャのthrowsが変更されることによって、クライアントのコード(具体的には呼び出し経路のコード)を確実に破壊されることがわかるだろう。これは避けようがない。
「検査例外はアジャイルやオブジェクト指向の考えに反するという事実」について一部誤解あり - じゅんいち☆かとうの技術日誌
そうですね。明らかですね。
____ ,: 三ニ三ミミ;、-、 xX'' `YY"゙ミ、 だがちょっと待ってくれ。 彡" ..______. ミ. ::::: ::; ,=ミ______=三ミ ji,=三ミi i 、'ーーー|,-・ー |=|,ー・- | ,-v-、 i; ':: ::: ーー" ゙i ,ーー'j / _ノ_ノ:^) ーi:: ::i: /`^ー゙`、_ ..i / _ノ_ノ_ノ /) |:::. ゙"i _,,.-==-、;゙゙i / ノ ノノ// 〉::.:.. 丶 " ゙̄ .'.ノ ____ / ______ ノ / i, `ー-、.,____,___ノ\____(" `ー" 、 ノ ー'/ 'i. ヽ、 ,二ニ/ \ ``ー-、 ゙ ノ / 'i、 /\ / > ( `ー''"ー'" \ 'i," (__) / / \ /ノ
上記のソースコードをみると、setterが削除されてコンストラクタが増えることによって、クライアントのコード(コード1)を確実に破壊することがわかるだろう。これも避けようがない。
この手の煩わしさを避けたければ、動的型付け言語しかない。これを避けられるのが動的言語の一つのメリットだ。では静的言語のメリットとは一体何なのか。何かとんでもない事があったときにビルドが壊れてくれるのがメリットではなかろうか。メソッドのシグネチャが変わるというのはとんでもない事*2である。
このビルドが壊れてくれない動的言語は、気づかないままリリースしてその後に「そんなメソッドねえよ」的なエラーを起こす可能性がある。対する動的言語では、コンパイルエラーが発生するため、そのままリリースしてしまう危険性はまずないだろう。
チェック例外によるシグネチャの変更にも全く同じことが言える。ビルドが壊れないように、という理由だけで何でもかんでも非チェック例外で処理しようとすると、気づかないままリリースしてその後にRTEが飛ぶ危険性が高まるのだ。*3
RTEを多用する開発と、動的言語による開発は、以下の点で共通項があると思う。この逆が、Javaでしっかりチェック例外を使う開発だ。
- APIのマズい設計がまかり通りやすい(ビルドが壊れないから、簡単にいつでも修正できると思いがち)
- ただ、深く緻密に考えなくても済む分、開発は早い
- が、その結果上記のようにバグを見逃してリリースする可能性がある
他にもいろいろ特徴はあると思う。そして結論は一長一短ですよ。(よくある、動的言語vs静的言語の宗教戦争の意図はありません。)
どんな開発を目指すのか?
今やろうとしている開発はどちらなのか、よく考えるべきだ。Javaを選んだからには、それなりに後者の意図があるはずだ。しかしその上で、RTEを多用して前者側に寄ろうとする意図は何なのか?
意図がはっきりしていれば良いと思う。例えばSeasarでは、チェック例外をほぼ全てRTE化して処理している。恐らくこの流れは、開発当時、相当 Rails を意識したからなんだと解釈している。このように、「Javaでも軽量言語的な開発ができるんだよ、というアピールがしたい」というのは一つの解。
また、「まぁどちらかと言えば後者なんだけど、そこまでガチに後者を突き詰めたい訳じゃない。少々堅牢さを失うけどスピード感も得られるから、ちょっと前者側に寄った方がバランス良いと思うんだ。」というのも一つの解だ。
RTE多用を選択しようとするあなたに、このような意図はあるだろうか?