エラー処理の指針

エラーの定義

エラーとは、プログラムが予期せぬおかしな状態に陥り、 復旧不可能な事態のことです。ここでいう「復旧」の範囲は、 メソッドレベルとします。また、復旧が不完全な状態は 「復旧している」とみなします。

この例では、メソッドの目的は指定した DSN に接続することです。 ここでできることは PEAR DB に処理を依頼することだけなので、 もし DB からエラーを返された場合は何もせず例外を発生させます。

この例では、例外を捕捉してそこから復旧させています。低レベルの connectDB() メソッドは、 データベースへの接続が失敗した際にエラーをスローすることしかできません。 しかし、その上位に位置する connect() では、設定済みデータベースのいずれかひとつに接続できればよいことを知っています。 エラーからは復旧可能なので、このレベルでの例外は無視され、上位にはスローされません。

この復旧には副作用があるので、完全ではありません。 しかし、プログラムの実行はそのまま続けられます。 例外は処理されたとみなされるので、再度スローしてはいけません。 先の例と同様、例外を黙らせた際にはログ出力や警告を行うべきでしょう。

PHP 5 用 PEAR パッケージにおけるエラー通知

PHP 5 用に書かれた PEAR パッケージのエラーは、 例外を使用して通知しなければなりません。リターンコードで示したり PEAR_Error オブジェクトを返したりといった方法は推奨されません。 もちろん PHP 4 との互換性を提供するパッケージについてはこの限りではありません。 その場合は、PHP 4 用の PEAR コーディング規約に従ってエラー処理を行います。

例外は、先の節で述べた定義によるエラーが発生した場合に常にスローしなければなりません。 スローする例外の中には、エラーのデバッグを行うための情報と エラーの原因をはっきりさせるための情報を十分に含める必要があります。 実際の運用時には、例外が一般使用者に見えることがないことに注意しましょう。 つまり、例外のエラーメッセージに、 技術的に複雑な内容を含めるのを躊躇する必要はないということです。

基本となる PEAR_Exception には、 エラーを表すテキストを含めることができます。これにより、 例外をスローする原因となったプログラムの状態を説明します。 また、オプションで、下位レベルの例外をラップすることもできます。 これにより、下位レベルで発生したエラーの原因をより詳細に伝えられます。

例外に含める情報は、エラーの内容によって異なります。 例外をスローする側の立場で考えると、 エラーには三種類あります。

  1. 事前の条件チェックによって見つかったエラー

  2. 下位レベルのライブラリから、エラーコードやオブジェクトで返されたエラー

  3. 復旧不可能な、下位のライブラリの例外

事前の条件チェックによるエラーの場合は、 失敗したチェックの内容を説明に含めましょう。 可能な限り、チェックに失敗した実際の値も含めておくようにしましょう。 通常は、この場合は他の例外をラップすることはありません。 このエラーの原因は下位レベルではないからです。 この形式のエラーは、たとえば以下のようになります。

下位レベルのライブラリからエラーコードが返された場合は、 もしそれが復旧不可能なものなら例外に変換しましょう。 エラーの説明には、もとのエラーに含まれる情報をすべて含めるようにします。 たとえば、先に示した connect メソッドなどを参考にしましょう。

下位のライブラリの例外を復旧できない場合は、 再度スローするか、そのままにしておきます。 再度スローする場合は、スローする例外の中で元の例外をラップしておく必要があります。 元の例外をそのまま放置すると、例外は処理されず、 コールスタックをさかのぼって別のハンドラを探します。

例外を新たにスローするか元の例外を放置しておくかは、ソフトウェアの設計の問題となります。 以下のふたつの例外を除き、基本的に例外は放置しておくべきです。

  1. 元の例外が別のパッケージで発生したものである場合。 これをそのまま放置しておくと、内部実装の詳細が丸見えになってしまいます。 これは各レイヤの抽象化に反するまずい設計です。

  2. 現在のメソッド内で、 受け取ったエラーに有用なデバッグ情報を追加して再スローできる場合。

例外および通常のプログラムの流れ

例外は、決して通常のプログラムの流れで使用してはいけません。 すべての例外処理ロジック (try-catch 文) をプログラムから取り除くと、 残った部分が "One True Path"、 つまりエラーがない場合に通る処理の流れになっているべきです。

言い換えると、例外はエラーが発生した場合にのみ使用し、 通常の処理中には使ってはいけないということです。

以下は例外の間違った使用例で、 再起処理の結果を例外の「たらいまわし」機能で返そうとしています。

この例では、ResultException を、 深い再帰レベルから一気に "終了!" させるだけのために使用しています。 エラーを報告する際にはこの機能は便利ですが、 この例のような使い方は、単に開発者の手抜きでしかありません。

例外クラスの階層

PEAR パッケージの例外は、すべて PEAR_Exception を継承していなければなりません。 PEAR_Exception は通常の PHP の例外クラスとは違って例外のラッピング機能を持っています。これは、 先の節で説明した要件を満たすために必要となります。

さらに、各 PEAR パッケージでは、 <Package_Name>_Exception という名前の例外を提供しなければなりません。そして、 パッケージ内ではその例外を継承した例外しかスローしないようにしておくことを推奨します。

例外についてのドキュメント

PHP では、Java とは異なり、 そのメソッドがどんな例外をスローするのかを シグネチャで明示的に宣言することができません。 そのため、発生する例外についてはメソッドのヘッダでしっかり説明しておく必要があります。

多くの場合は、アプリケーションの中間層で下位レベルの例外を再構成し、 よりわかりやすいアプリケーション固有の例外に変換します。 これについてもきちんと説明しておく必要があります。

あるいは、あなたの書いたメソッドは、 下位レベルで発生した例外を何も処理せず放置することもあるかもしれません。 そんなことをする場合にもやはり、どの例外を 捕捉しない のかを記述しておく必要があります。

API の一部としての例外

例外は、あなたのライブラリの API において重要な役割を演じます。 開発者があなたのライブラリを使用する際には、 パッケージのどこでどんな場合に例外が発生するのかの説明が必要です。 ドキュメントが重要になります。 また、スローされるメッセージの型についての説明も、 過去との互換性を確保するために重要となります。

例外はパッケージの API の中でも最重要なところなので、 例外を変更することで過去との互換性 (BC) をなくしてしまわないことが大切です。

過去との互換性をなくしてしまう変更には、次のようなものがあります。

過去との互換性をなくさない変更には、次のようなものがあります。