読者です 読者をやめる 読者になる 読者になる

かとじゅんの技術日誌

技術の話をするところ

非検査例外に萌えるわけ

検査例外はアジャイルやオブジェクト指向の考えに反するという事実 - じゅんいち☆かとうの技術日誌

タイトルは釣り度が強すぎかなー、、、まぁ、個人のブログなんで気にしないでくださいw
ブログはカジュアルに書けばいいんですよ。タイトル付け失敗してもサーセンです。
オブジェクト指向の考え」ではなく「オープンクローズド原則」に反するとしたほうがいいですね。
しかし、貶める気もない人に 貶めるという定義付けは いただけないなー。

で、そんなこんなで重量級をゲットしてもた。。
非チェック例外多用作戦のトレードオフ認識 - 都元ダイスケ IT-PRESS
オブジェクト指向と型システムの狭間で例外を考える - プログラマーの脳みそ

エントリをいただいたので、さらにまた考えてみました。

チェック例外の正当性を再検証する

最近、多くの人の尊敬を集めているBruce EckelやRod Johnsonといった何人かのエキスパートが次のように公式に述べています。つまり、自分たちも当初はチェック例外に関する正統的な立場を完全に支持したのですが、結論として分かったのは、チェック例外のみを使うのは当初思われていた程には良い考えではないこと、またチェック例外が、多くの大プロジェクトで重大な問題の元になる、ということなのです。

まずこのあたりの問題提起も把握した上で議論したいですね。
前回のエントリでは、他にも問題あるのですが検査例外の一つの問題であるこの点をあげています。

不安定なメソッド・シグニチャ

不安定なメソッド・シグニチャーの問題は先の問題に関連しています。単純にメソッドを通して例外を渡していると、メソッドの実装を変える度にそのメソッド・シグニチャーを変える必要があるだけでなく、そのメソッドを呼ぶ全てのコードも変える必要があります。一旦クラスが実稼働状態に展開されてしまうと、繊細なメソッド・シグニチャーを管理するのは高くつくものになります。ところがこの問題は基本的に、Blochの43項のヒントに従わないことから起きる別の症状なのです。メソッドは失敗があった時には例外を投げるべきですが、その例外が反映すべきなのはそのメソッドが何をするかであって、どのようにするか、ではないのです。

実装変更によるメソッド・シグニチャーへの例外の追加削除にプログラマーが疲れて、対象のレイヤーが投げる例外タイプの定義に抽象化を使わず、単純に全てのメソッドがExceptionを投げるように宣言してしまうことが時々あります。別の言い方をすれば、例外はあまりにも面倒すぎると結論し、例外という電源スイッチを切ってしまうのです。当然ですがこの手法は一般的に、どうでも良いようなコード以外では良いエラー処理とは言えません。

上記のソースコードをみると、setterが削除されてコンストラクタが増えることによって、クライアントのコード(コード1)を確実に破壊することがわかるだろう。これも避けようがない。

この手の煩わしさを避けたければ、動的型付け言語しかない。これを避けられるのが動的言語の一つのメリットだ。では静的言語のメリットとは一体何なのか。何かとんでもない事があったときにビルドが壊れてくれるのがメリットではなかろうか。メソッドのシグネチャが変わるというのはとんでもない事*2である。

このビルドが壊れてくれない動的言語は、気づかないままリリースしてその後に「そんなメソッドねえよ」的なエラーを起こす可能性がある。対する動的言語では、コンパイルエラーが発生するため、そのままリリースしてしまう危険性はまずないだろう。

ただ、上記のようなメソッドの名前、戻り値、引数の仕様が変化することと、throwsによる仕様が変化することでは壊れる範囲が違うと認識している。
メソッドの名前、戻り値、引数というのは、呼び出し元(クライアント)と呼び出し先(サーバ)のメソッド間でピアツーピアで契約が成り立つもので、一方、throwsの場合は、例外を受信するcatch側(クライアント)と送信するtry側(サーバ)の契約で、呼び出し経路上のメソッドが複数存在する場合がある。
前者は、ピアツーピアなので間に巻き込まれる第三者のメソッドがいない。契約が変更になったのだから、契約書の甲と乙で契約を結び直す行為だね。
後者は、送受信の当事者間の間に経路上のメソッドが存在するが、壊れてほしいのは送信側と受信側だけだが、無用にも経路上のメソッドも一緒に壊れてしまう。論理的には経路上のメソッドを呼び出すメソッドに対して同様の契約を追わせないといけないので、一緒に壊れるのは仕方ないといえるが、壊れる範囲が相対的に大きくなりやすく実用的ではない。このデイジーチェーンな契約スタイルはいかがなものかと。経路上のメソッドは何か例外に対する責務を負う必要性があるのかという疑問をいつも持っている。責務を透過的にできるオプションもあってよいのではないかと。経路上のメソッドにthrowsを書かないでよいオプションが必要だと思う。問題にしているのこの契約の範囲であって、静的型付け言語自体ではない。だから、動的型付け言語を選択するということも想定にない。つまるところ検査例外という契約スタイルが使いにくいという話だね。

チェック例外によるシグネチャの変更にも全く同じことが言える。ビルドが壊れないように、という理由だけで何でもかんでも非チェック例外で処理しようとすると、気づかないままリリースしてその後にRTEが飛ぶ危険性が高まるのだ。*3

翻って、非検査例外では、契約の当事者間で契約が変更されたことをコンパイラレベルで検知できない。指摘のとおり、想定外のRTEがスローされてしまうのに、気づかないままリリースされることが問題だ。たとえば、想定外の例外がスローされないことを保証するテストケースを用意するのはどうだろうか。それでも、テストケースでもcatch漏れが発生するリスクはある。だがしかしだ。打ち出の小槌はなく、トレードオフ。テスト工程も含めた開発プロセス全体で考えるところだと思うのだ。最初から完璧を求めるのは現実的ではないのだ。

RTEを多用する開発と、動的言語による開発は、以下の点で共通項があると思う。この逆が、Javaでしっかりチェック例外を使う開発だ。

* APIのマズい設計がまかり通りやすい(ビルドが壊れないから、簡単にいつでも修正できると思いがち)
* ただ、深く緻密に考えなくても済む分、開発は早い
* が、その結果上記のようにバグを見逃してリリースする可能性がある

他にもいろいろ特徴はあると思う。そして結論は一長一短ですよ。(よくある、動的言語vs静的言語の宗教戦争の意図はありません。)

「変更に強く」し「開発速度も早く」を得るために、あえて非検査例外を選択し、実装工程における最後の砦となるテストケースに細心の注意を払い、さらに実装工程だけではなくテスト工程を含めた開発プロセス全体での品質保証を考えていけばよいのだと個人的に考えている。

実際には、どのような開発をするか、その開発プロジェクトのポリシーによって例外をどのように扱っていくか決まるだろう。旧来通り検査例外も利用するならそれに従うべきだろう。がしかし、技術者個人としては、デフォルトでどの立場をとるのかというのは当然ある。それは述べた通りだ。ほぼこのスタンスと考えてよい。

また、「まぁどちらかと言えば後者なんだけど、そこまでガチに後者を突き詰めたい訳じゃない。少々堅牢さを失うけどスピード感も得られるから、ちょっと前者側に寄った方がバランス良いと思うんだ。」というのも一つの解だ。

なぜ技術者個人としては、そのような「非検査例外」を軸とするスタンスをとるのか。理由は以下だ。
事前に緻密に設計し「堅牢性」を手に入れたいところだが、そうもいかない事情がある。昨今のプロジェクトでは走りながら考えることを要求されるからだ。プロジェクトにアサインされた時点ですでに遅延しているケースも少なくない。一般論としては昨今のプロジェクトでは、十分に考える時間は与えられないことが多いのだ。さらに、現況から考察すると、デフレが加速し外注費を含めた人件費が削減される傾向にあるので、短期間で効率よくといった「アジャイル」の視点を持った開発が至上命題になっていくと推測している。最初から検査例外で「手間は掛かっても堅牢性」を指向する設計はベストな方法論だ。だが実用的ではない。「いや、それだったらLLでやるべきじゃん」という短絡的な発想で必ずしも片付けることができないのが現実問題だ。ゆえに、コードとしての「堅牢性」は低下するが、「変更に強く」、「開発速度も早い」という現実路線を選択する。そして開発プロセス全体でシステムの品質を洗練させていき、最終的にシステムとしての「堅牢性」を担保するというベターな戦略がよいと思っている。
ともあれ、小さな開発会社だが、技術と信用でエンタープライズから小規模案件まで対応する技術部門の長としての考え方だ。それ以外の立場では考え方がかわって当然。がしかし、ビジネスの現場での有益さが伴わない技術論は、どこまでいっても不毛だと感じるのである。そして、「どういう方法論を選ぶかは顧客や開発プロジェクトの方針だ」と簡単に片付けたくない。自分の答えを持っていないのと同じだ。SIer(技術者)が顧客と共生を目指すなら「この場合だったらこの答え、ちがうならこの答え」と、ただ”判断”するのではなく、顧客像をイメージしながら技術としてのはっきりとした”決断”されたスタンスを持っておきたい。その上で顧客が選択すればいいのだ。

それと最後に
Javaの理論と実践: 例外をめぐる議論
これも読んでから考察してみるとよい。Bruce EckelやRod Johnsonの検査例外に対する問題提起をちゃんと把握したほうがいい。

さて、あなたはどう考えますか?
.NETな方も言及きぼんですw

あわせて読みたい
http://d.hatena.ne.jp/kkamegawa/20090809/p1


追記:
この話も同様のことをいってるが、、、実際の稼働しているデプロイ環境で想像してみるといいよ。検査例外によって縛られた依存関係ができてしまうといってよい。怖すぎるよねー。throwsを変更すると互換性に問題がでるので既存の検査例外を使って隠蔽してしまう可能性だって出てくる。まぁ、メソッドの型付けと違ってあまりに実用的でないという話だ。
使いにくいので検査例外を使わずに、非検査例外だけを使うことが、「とても善良なJavaプログラマがやることでない」といわれそうですが、他言語からみた場合なんの罪もないことだ。非検査例外のほうがデファクトなのだ。検査例外のないC#が悪なのか、そんなことはないでしょう。
さらにいうならば、Javaの検査例外という概念が、Java 10年の歴史をもってしても、今なお 他言語や他のプラットフォームに普及していない。それは、評価されていないという事実だと思うのだ。よいものは他の言語、プラットフォームにも普及するのですよ。キャズムを超えるというのはそういうことだ。

まず、検査例外は発生したその場、もしくは直接の呼出し元で処理しない限り、throws に記述せざるを得ない。

そうしない場合、より上位層の throws を追加する必要が出てくる。このような追加、もしくは変更は、中間のクラスの再リリースという手間も必要となる。

これは、明らかに開放閉鎖原則に違反する。

コメント欄だが、このあたりも核心をえぐっている気がするよ。私も同じ考えだ。であったが、「検査例外」自体が悪いわけではないということがわかった。

全ての例外をクラスの設計時に想定できるかと言うと、否です。
なので、検査例外を使うと判断したときから、開放閉鎖原則に則ったクラス設計と言うのは破綻するのではないか、というのが自分の考えです。
検査例外という存在は、非検査例外に変換すると言う方法以外ではどうやっても開放閉鎖原則を壊してしまう存在だ、という考えですね。

追記:

目的と手段で分離してみた場合、「開放閉鎖原則」を「検査例外」を使って破っているだけであって「検査例外」自体の存在が「開放閉鎖原則」を破っているわけでない。「開放閉鎖原則」を破るのは「非検査例外」でもできるわけで、直接の因果関係は成立しないということですね。

id:Nagiseさんからご指摘受けて、新たな知見を得られました。ありがとうございました。