web-dev-qa-db-ja.com

例外をキャッチして再スローするためのベストプラクティスは何ですか?

キャッチされた例外を直接再スローする必要がありますか、それとも新しい例外にラップする必要がありますか?

つまり、これを行う必要があります:

try {
  $connect = new CONNECT($db, $user, $password, $driver, $Host);
} catch (Exception $e) {
  throw $e;
}

またはこれ:

try {
  $connect = new CONNECT($db, $user, $password, $driver, $Host);
} catch (Exception $e) {
  throw new Exception("Exception Message", 1, $e);
}

あなたの答えが直接投げるである場合、例外連鎖の使用を提案してください。例外連鎖を使用する現実のシナリオを理解することはできません。

140
Rahul Prasad

例外をキャッチするべきではありません何か意味のあることをしようとしない限り

「意味のあるもの」は次のいずれかです。

例外処理

最も明白な意味のあるアクションは、例外を処理することです。エラーメッセージを表示し、操作を中止します。

try {
    $connect = new CONNECT($db, $user, $password, $driver, $Host);
}
catch (Exception $e) {
    echo "Error while connecting to database!";
    die;
}

ロギングまたは部分的なクリーンアップ

特定のコンテキスト内で例外を適切に処理する方法がわからない場合があります。 「全体像」についての情報が不足している可能性がありますが、障害が発生したポイントにできるだけ近いところでログに記録する必要があります。この場合、キャッチ、ログ、および再スローすることができます。

try {
    $connect = new CONNECT($db, $user, $password, $driver, $Host);
}
catch (Exception $e) {
    logException($e); // does something
    throw $e;
}

関連するシナリオは、失敗した操作のクリーンアップを実行するのに適切な場所にいますが、トップレベルでの障害の処理方法を決定することではありません。以前のPHPバージョンでは、これは次のように実装されていました。

$connect = new CONNECT($db, $user, $password, $driver, $Host);
try {
    $connect->insertSomeRecord();
}
catch (Exception $e) {
    $connect->disconnect(); // we don't want to keep the connection open anymore
    throw $e; // but we also don't know how to respond to the failure
}

PHP 5.5ではfinallyキーワードが導入されたため、クリーンアップシナリオでは、これにアプローチする別の方法があります。クリーンアップコードを何が起こったとしても実行する必要がある場合(つまり、エラー時と成功時の両方)、スローされた例外を透過的に伝播しながら、これを実行できるようになりました。

$connect = new CONNECT($db, $user, $password, $driver, $Host);
try {
    $connect->insertSomeRecord();
}
finally {
    $connect->disconnect(); // no matter what
}

エラーの抽象化(例外チェーン付き)

3番目のケースは、大きな傘の下で多くの可能性のある障害を論理的にグループ化する場合です。論理的なグループ化の例:

class ComponentInitException extends Exception {
    // public constructors etc as in Exception
}

class Component {
    public function __construct() {
        try {
            $connect = new CONNECT($db, $user, $password, $driver, $Host);
        }
        catch (Exception $e) {
            throw new ComponentInitException($e->getMessage(), $e->getCode(), $e);
        }
    }
}

この場合、Componentのユーザーに、データベース接続を使用して実装されていることを知らせたくありません(オプションを開いたままにして、将来ファイルベースのストレージを使用したい場合があります)。したがって、Componentの仕様では、「初期化に失敗した場合、ComponentInitExceptionがスローされます」と書かれています。これにより、Componentのコンシューマーは、予期されるタイプの例外をキャッチできますデバッグコードがすべての(実装依存の)詳細にアクセスできるようにする

より豊かなコンテキストの提供(例外チェーンを使用)

最後に、例外に対してより多くのコンテキストを提供したい場合があります。この場合、エラーが発生したときに何をしようとしていたかについての詳細情報を保持する別の例外に例外をラップすることは理にかなっています。例えば:

class FileOperation {
    public static function copyFiles() {
        try {
            $copier = new FileCopier(); // the constructor may throw

            // this may throw if the files do no not exist
            $copier->ensureSourceFilesExist();

            // this may throw if the directory cannot be created
            $copier->createTargetDirectory();

            // this may throw if copying a file fails
            $copier->performCopy();
        }
        catch (Exception $e) {
            throw new Exception("Could not perform copy operation.", 0, $e);
        }
    }
}

このケースは上記と似ていますが(例はおそらく最良のものではないかもしれません)、より多くのコンテキストを提供するポイントを示しています:例外がスローされた場合、ファイルコピーが失敗したことを通知します。しかし、なぜ失敗しましたか?この情報は、ラップされた例外で提供されます(例がより複雑な場合、複数のレベルが存在する可能性があります)。

これを行うことの価値は、たとえばUserProfileオブジェクトを作成すると、ユーザープロファイルがファイルに保存され、トランザクションセマンティクスがサポートされるため、ファイルがコピーされます。変更がコミットされるまでプロファイルのコピーでのみ実行されるため、変更を「元に戻す」ことができます。

この場合、あなたがした場合

try {
    $profile = UserProfile::getInstance();
}

その結果、「ターゲットディレクトリを作成できませんでした」という例外エラーをキャッチしたため、混乱する権利があります。コンテキストを提供する他の例外のレイヤーにこの「コア」例外をラップすると、エラーの処理がはるかに簡単になります(「プロファイルコピーの作成に失敗しました」->「ファイルコピー操作に失敗しました」->「ターゲットディレクトリを作成できませんでした」)。

265
Jon

まあ、それはすべて抽象化を維持することです。そのため、例外の連鎖を使用して直接スローすることをお勧めします。理由については、 leaky abstractions の概念を説明しましょう

モデルを構築しているとしましょう。モデルは、アプリケーションの残りの部分からすべてのデータの永続性と検証を抽象化することになっています。それでは、データベースエラーが発生するとどうなりますか? DatabaseQueryExceptionを再スローすると、抽象化が漏れています。理由を理解するために、少しの間抽象化を考えてください。モデルがデータを保存する方法howは気にしません。同様に、モデルの基礎となるシステムで何がうまくいかなかったのかは気にしません。何かがうまくいかなかったことと、おおよそ何がうまくいかなかったかを知っているだけです。

そのため、DatabaseQueryExceptionを再スローすることにより、抽象化をリークし、モデルの下で行われていることのセマンティクスを理解するために呼び出しコードを要求しています。代わりに、汎用のModelStorageExceptionを作成し、その中にキャッチされたDatabaseQueryExceptionをラップします。このようにして、呼び出し元のコードはエラーを意味論的に処理しようとすることができますが、その抽象化レイヤーからエラーを公開するだけなので、モデルの基礎となるテクノロジーは重要ではありません。さらに良いことに、例外をラップしたので、バブルがいっぱいになってログに記録する必要がある場合は、スローされたルート例外(チェーンをたどる)をトレースして、必要なデバッグ情報をすべて取得できます!

後処理を行う必要がない限り、単純に同じ例外をキャッチして再スローしないでください。しかし、} catch (Exception $e) { throw $e; }のようなブロックは無意味です。ただし、いくつかの重要な抽象化のために例外を再ラップできます。

34
ircmaxell

私見、例外をキャッチして再スローするだけです役に立たない。この場合、それをキャッチせずに、以前に呼び出されたメソッドがそれを処理できるようにします(コールスタックの「上位」であるメソッド)

それを再スローする場合、キャッチした例外に含まれる情報を保持するため、キャッチした例外をスローする新しい例外にチェーンすることは間違いなく良い実践です。ただし、キャッチする例外に何らかの情報を追加するか何かを処理するの場合にのみ、再スローするのが便利です。何らかのコンテキスト、値、ロギング、リソースの解放などです。

いくつかの情報を追加する方法は、Exceptionクラスを拡張して、NullParameterExceptionDatabaseExceptionなどの例外を作成することです。さらに、これにより、開発者は自分ができる例外扱う。たとえば、データベースへの再接続のように、DatabaseExceptionのみをキャッチし、Exceptionの原因を解決することができます。

9

PHP 5.3の例外ベストプラクティス を確認する必要があります。

PHPでの例外処理は、ストレッチによる新機能ではありません。次のリンクには、例外に基づいたPHP 5.3の2つの新機能があります。 1つ目はネストされた例外で、2つ目はSPL拡張機能(PHPランタイムのコア拡張機能)によって提供される新しい例外タイプのセットです。これらの新機能はどちらもベストプラクティスの本に組み込まれているため、詳細に検討する必要があります。

http://ralphschindler.com/2010/09/15/exception-best-practices-in-php-5-

2
HMagdy

通常、このように考えます。

クラスは、一致しない多くのタイプの例外をスローする場合があります。そのため、そのクラスまたはクラスのタイプに対して例外クラスを作成し、それをスローします。

したがって、クラスを使用するコードは、1種類の例外をキャッチするだけです。

1
Ólafur Waage