ってことで、例外のお話です。
どうも僕の知っている限りでは、チェック例外という言語機能をもっているのはJavaだけみたいです。
僕個人としては、すばらしい機能なんですが。
Javaで最初にチェック例外を学んだ時、「そうそう、これこれ、これが欲しかったのよ!」
って思ったあの頃の想いは今も色あせていません。
「ずっと好きだったんだぜ~♪」って斉藤和義の曲を口ずさむくらいに、
今もチェック例外を愛しています。
しかし、このチェック例外、他の言語からdisられまくって、最近の言語には取り込まれないという悲しいお話になっています。
過去に様々な議論があった中で、結論として
チェック例外は役に立たないってことになったらしいです。
経緯は知らんがね、そんなん。僕には役立ってるんだから。
ってことで、僕の立場から見た、チェック例外についての考察と扱いを書きます。
反論したい方、
自重をお願いします。
だって、チェック例外はもう既に、
C#だ~って、Rubyだーって~ Scalaだ~って~ みんなみんなチェック無いんだ 友達なんだ~♪
っていうくらいの、
実質的な敗北宣言になってんだから、これ以上の反論は
「
死者にムチを打つ」
って言葉がふさわしいと思いますね、僕は。
悲しいけど、これ現実なのよね。。。orz
なんで、今後新しい技術を取り入れるには、チェック例外とバイバイしないといけないので、自分の気持ちに整理をつける意味で、このブログを書いてます。。。
ってことで、気を取り直して、例外の話に入る前にまずはエラー処理について。
プログラミングの初期から、エラーをどのように扱うかという命題があり、様々なエラー処理方法が生み出されています。
例えば、一般的なエラーを返す仕組みとしては、
・当たり障りのない値を返す
・エラーコードを返す
などがあります。
当たり障りのない値を返す というのは、例えば、
テーブル走査をした結果、検索レコードが存在した場合はそのエンティティオブジェクト、存在しなかった場合はnullを返すとか、
文字列検索をした結果、検索文字列が存在した場合はその位置インデックス。存在しなかった場合は-1を返す
とかです。
この方式の欠点は、
正常な値とエラー値がバッティングする場合にあります。
整数の計算値を返す関数があったとして、なんらかのエラー値に-1を使うとした場合、-1がエラー値なのか計算した結果なのかが分からないんですね。
また、具体的にどんなエラーが発生したかという詳細情報を返せない欠点もあります。
対照的に、エラーコードを返すってのは、戻り値に処理結果を返す方法ですが、戻り値に計算結果を返せなくなるので、関数としては使えず、プロシージャのみの使用になる欠点があります。
エラー返却以外にも、エラー処理として
・システム全体を停止するのか?
・処理を継続するのか?
・
処理の一部のみを停止するのか?
エラーが発生した場合にどうするかの考慮も必要であり、エラーを適切に扱おうとすると、プログラムコードの7~8割はエラー制御になるって話があるくらいエラー処理は複雑で面倒な話です。
ここらへんの話は、「
コードコンプリート 第2版 上」のp.237 にある「8.3 エラー処理テクニック」に上手くまとめられていますので、もっと詳しく知りたい方はこちらをどうぞ。
お勧め名著ですよ!
- Code Complete第2版〈上〉―完全なプログラミングを目指して/スティーブ マコネル
- ¥6,405
- Amazon.co.jp
このようなエラー処理をうまく扱おうとして生み出されたのが例外という機構です。
例外は、戻り値とは別に、呼び出し元に詳細なエラー情報を返すことができます。
また、例外にはプログラム動作を制御する機構もあり、呼び出し元が例外を処理しなければ、そのまた呼び出し元に例外を返すといったことも自動で行ってくれます。
ま、例外の細かい話は、
この記事を読もうって思った人は知っているはずなんで、これくらいで。
で、本題のJavaの例外について。
Javaの例外は下のクラス構造になっています。
全ての例外とエラーはTrowableを継承しています。
・ErrorとRuntimeException、およびこれを継承するクラスは非チェック例外
・Exception、およびこれを継承するクラスはチェック例外
です。
Errorは「通常のアプリケーションであればキャッチすべきではない重大な問題」が発生したことをあらわします。
例えば、あるはずのメソッドがない(実行時リンケージエラー)とか、JVMのバグとか、アサーションエラーとかね。
Exceptionは通常の処理で発生する可能性のあるエラーを表します。
入出力例外とか、データベースエラーとかね。
っで、RuntimeExceptionは、そもそも発生すること事態がおかしいんじゃねーの?って状態を表します。NullPointer例外(通称、ヌルポ)とかIllegalArgumentException(いわゆる、パラメータエラー)とかです。
Javaでアプリケーションアーキテクトを構築する際は、このような例外をどのように扱うかを定義する必要があります。
僕がアプリケーションを設計する場合、簡単に表現すると、以下の図のようなパターンで考えます。
上図のフレームワーク層はホットスポットで、StrutsやSpringフレームワークといったフローズンスポットではありません。各アプリケーション特性に応じて、プロジェクト毎に作成します。
ライブラリ群は、Java APIやOSSライブラリなどにない処理や、各アプリケーション特性に応じた処理をプロジェクト毎に作成します。まー、ライブラリは複数プロジェクトで使いまわすことも多いです。
っで、例外の扱いはフレームワーク層とライブラリで異なります。
ライブラリでの例外の扱いは単純で、
呼び出し元に、「これ処理してね」ってお願いするエラーはチェック例外
「おいおい、この呼び出しはおかしいやろ」って、呼び出し元をdisるにやさしく教えてあげる場合は、非チェック例外
って判断で、例外を呼び出し元にスローするよう規定しています。
ここで気をつけないといけないのは、例外はインタフェースの一部ってところです。
例えば、アプリケーション環境設定の情報を扱うライブラリがあったとして、その情報を外部ストレージから読み込むloadという処理があったとします。コードで書くと、こんな感じ。
---------------------
public class AppConfig {
public void load() throws IOException {
:
:
}
}
---------------------
このload()メソッドの例外の扱いは不正解です。
なんでかというと、IOExceptionを返す時点で、アプリケーション環境設定が外部ディスクに保存されている前提になっているからです。途中で設定をデータベースに持とうって話になった時に、
public void load() throws SQLException
ってインタフェース変更しなければいけなくなります。
ですので、正しくは
public void load() throws AppConfigException
といった、そのライブラリの抽象度にあわせて、
Javaの例外をそのまま呼び出し元にスローするか、違う別の例外に変換して呼び出し元にスローするかを判断する必要があると考えています。
一方、フレームワーク層では、アプリケーション層がフレームワーク層に投げていい例外を規定します。
僕の場合は、アプリケーション例外というチェック例外のみをフレームワーク層に返していい仕様にすることが多いです。
アプリケーション例外には、アプリケーションのメッセージコードやメッセージ、またアプリケーション例外の原因になった例外を情報として持たせます。
また、フレームワーク層で扱う例外は、
その例外が何を表すかだけでなく、その例外が発生した場合にどのように処理するかまでを規定します。
僕の場合、フレームワーク層とライブラリはオブジェクト指向で設計し、アプリケーション層はトランザクション単位をあらわすサービスというクラスを作って、Transaction Scriptでガリガリのパターンが多いんですが(今、僕をdisった奴は、現場を知らない奴だと思う。色々あるんだよ、現場は。。。)、サービスがアプリケーション例外をフレームワーク層に返却すると、トランザクションのロールバックとエラーログ出力およびエラー通知を行うなどをフレームワークで行います。
サービスのインタフェースをコードで表すとこんな感じ
---------------------
public interface AppService {
AppResponse execute(AppRequest req) throws AppException;
:
}
---------------------
一方、フレームワークのコードイメージはこんな感じ
---------------------
AppResponse resp = null;
try {
resp = appService.execute(req);
} catch (AppException ex) {
// アプリケーションエラー(起こりえる)
} catch (RuntimeException ex) {
// システムエラー(いわゆるバグ)
} catch (Error err) {
// システム障害(致命的)
}
---------------------
ここで方式設計者としてうれしいのは、
フレームワーク層には、アプリケーション例外か非チェック例外およびエラーしか投げられないってことが言語として保障されている点です。
JavaAPIなどで規定された低レベルな例外がフレームワーク層にスローされることはないんですね。
全て適切にアプリケーション層で処理され、ユーザおよびシステム運用者に通知できるレベルにエラー変換されているんです。
ですので、フレームワーク層にエラーが渡ってきた時点で、そのエラーが致命的なシステム障害なのか、システム不具合(いわゆるバグ)なのか、発生しうるエラー(アプリケーション例外)なのかが即座に判断可能です。
これは、システムを保守する上でとても重要なことです。エラーメールが飛んできた時点で、致命的エラーかバグか、はたまた起こりえるエラーかを即座に判断できる。また、起こりえるエラー(アプリケーション例外)の場合、メッセージコードと詳細なアプリケーションレベルのメッセージを確認し、あらかじめメッセージコード毎に纏められているシステム運用手順を行えば、すばやく対応できるんですね。
このようなことは、24時間365日稼動システムなど、稼働率を強く求められるシステムでは大切なことです。
方式設計者は、
ライブラリ層でアプリケーション層に例外対処をうながし、
フレームワーク層でアプリケーションがスローできる例外を限定する
んです。
チェック例外がないってことは、これをドキュメントレベルでしか規定できず、方式設計者が使える武器が一つ減るんですね。
こういう考えなんで、
Twitterは、バックエンドにScalaを採用しているみたいですが、たまにでる
「おや、なにかがおかしいですよ」
ってエラー。本当に何が起こったか分からんのちゃうかww って思ってしまいます。
以上が僕の例外に対する考察と扱いです。
あと、巷に流れている例外の扱いについて、僕なりの考察を書いていきます。
■ 『チェック例外=回復可能な例外、非チェック例外=回復不可能な例外って考え』
僕の考えは上記のとおり、
チェック例外(Exception)=プログラムで発生を防げない。外部要因などで発生する可能性のある例外
非チェック例外(RuntimeException)=プログラムで発生を防げる。投げられること事態、おかしな可能性のある例外
エラー(Error)=プログラムで発生を防げない。そもそも発生すること事態が想定外な例外。
です。
なんで、データベースエラーなんかは外部要因で発生するのでチェック例外です。
データベースエラーなんかはエンタープライズシステムでは一般的に回復不可能やし、キャッチしてもしゃーないってことで、SpringなんかのF/Wはわざわざ非チェック例外に変換しなおしているみたいですがね。
でも、本来外部要因で発生するデータベースエラーや通信エラーなんかはアプリケーション層で処理しなければいけないものであり、非チェック例外として扱うのはおかしいと僕は考えています。
そういった意味で、ファイルオープン時に、ファイルが存在しないってのもチェック例外です。
誰かがオープン直前にファイルを消すかもしれないし、外部要因で発生するからプログラムで防げないエラーでかつ、発生する可能性はあるからです。
また、データベースアクセスにおいて、1つのエンティティを返す主キー検索を行うメソッド(findByPrimaryKey() みたいなの)についても、レコードが存在しなければnullを返すんではなく、NoDataFoundException なんかの例外をスローしてもいいと考えています。
今まで、
Entity entity = EntityDao.findByPrimaryKey(1);
if (entity.getId() == 1) {
:
みたいな処理でNullPointer例外が発生するのをなんども見てきたので。
OracleのPL/SQLは、NO_DATA_FOUND例外を返す方針ですね。
■ 『例外は例外的な場合で使うべき』
有名な著書「
プログラミング作法」のp.161にも以下の文章があります。
『
ファイルのオープンに失敗するのはあまり例外的な事態とは言えず、上記のように例外を生成するのは技巧に走りすぎだと思う。例外は、ファイルシステムが使い尽くされた場合や浮動小数点エラーのような本当に予期できないイベントにのみ使うのがベストだ。』 ~プログラミング作法 p.161 ~
まー分からんでもないですが、じゃー、例外的な事態の基準はなに? って思います。
ファイルオープン時にファイルがなかったり、IOエラーはよくあるから例外的じゃない。でも、ファイルシステムが使い尽くされた場合は例外的って、そんな基準は曖昧だし、人によって解釈が異なりすぎると思うんですね。
それに、例外的なエラーと例外的でないエラーの対処をソース内で分けて書かなあかんから、余計にソースが複雑になると思います。
僕は、例外はコントロールブレイク性のある、エラー処理の仕組みと割り切っています。
つまり、関数やメソッドで発生したエラーとエラーの詳細を返す仕組みです。
エラーに例外的も例外的じゃないもありません。期待できる正常な値が返せない場合は、みんな例外で処理してOKだと思います。
また、「
プログラミング作法」ではJavaのファイルオープンについて、
『
ファイルがオープンできなかったときには、CやC++の場合のように入力ストリームをnullにセットするのではなく、このコードは例外を上げている。』 ~プログラミング作法 p.161 ~
と批判的見解がありますが、この場合、ファイルが無かった場合にnullが返却されることをプログラマが把握しないといけないし、ファイルが無かった場合
のエラー処理はif文によるnullチェックで、入出力エラーの場合は例外キャッチで、っと処理を分けるほうがコードの可読性を落とすような気がします。
また、nullを返却するエラーパターンがファイルがない以外もでてきた場合、どんなエラーだったのかを呼び出し元が知ることのできる仕組みが別途必要になります。
そういう仕組みを一々構築しないといけないのは面倒な話です。
そういった意味で、言語として用意されている例外は、呼び出し元にエラーを返すのに優れた仕組みです。
まー、「
プログラミング作法」が憎いわけではないです。
この本、
新人に必ず読ませたい本 第一位 ですよ、僕の中では。
- プログラミング作法/ブライアン カーニハン
- ¥2,940
- Amazon.co.jp
■ 『チェック例外は生産性を落とす割りに効果に乏しい』
チェック例外のそもそもの目的は信頼性をあげることで、生産性を挙げることではないんですね。
トレードオフの問題だと思います。
チェック例外を使って効果に乏しいってのは、信頼性をあまり求められていないシステムではそうかもしれませんし、そもそもアプリケーション基盤の例外の扱いに問題があるかもしれません。
ただ、ちょっとしたスクリプト言語を組む際は確かに面倒くさい機能です。チェック例外って。
なんで、僕も思うところはあります。
コンパイラオプションでチェック例外をキャッチしていなくてもエラーにしないとか、警告レベルにするとか、またはこの例外を継承しているクラスだけはエラーにしないとかの機能がJavaコンパイラにあってもいいんじゃないかとは思います。
GroovyやScalaやっている人は知っていると思いますが、別にJVMレベルでチェック例外ってサポートしている訳ではないんですね。単に、Javaコンパイラが勝手に構文チェックしているだけ。
なんで、やろうと思えばJavaでもできますし、そういう選択肢も欲しいと思います。
っていうか、
Scalaに欲しかったな、その機能。
また、そもそもJavaって言語自体に紋切り型なコードを多く作成しないといけないって欠点もあるんですね。
Scalaみたいに、色々な型推論やcaseクラスみたいな感じでショートコーディングできたり、try-catch ではない書き方を生み出すことで、生産性も上がるん違うかな?って思ったりします。
■ 『チェック例外はExceptionをスローするメソッドや例外の握りつぶしなどの弊害を出すことが多い』
checkstyle使えば?
ってことで、まーグダグダ言っても、今後チェック例外が復活する兆しは今のところなく、
まー、割り切ってやっていきます。
ではこの辺で。さよなライオン。ぽぽぽぽ~ん