web-dev-qa-db-ja.com

例外:なぜ早く投げるのですか?なぜ遅れるの?

単独での例外処理については、よく知られている多くのベストプラクティスがあります。私は「すべきこととすべきでないこと」を十分に理解していますが、大規模な環境でのベストプラクティスやパターンに関しては状況が複雑になります。 「早く投げて、遅く追いかけて」-何度も聞いたことがありますが、それでも混乱します。

低レベルのレイヤーでnullポインター例外がスローされる場合、なぜ早くスローし、遅くキャッチする必要があるのですか?なぜ上位レイヤーでキャッチする必要があるのですか?ビジネスレイヤーなど、上位レベルで下位レベルの例外をキャッチしても意味がありません。各層の懸念に違反しているようです。

次の状況を想像してみてください。

数値計算サービスを行っています。図を計算するために、サービスはリポジトリにアクセスして生データを取得し、他のいくつかのサービスは計算を準備します。データ取得レイヤーで問題が発生した場合、なぜDataRetrievalExceptionをより高いレベルにスローする必要があるのですか?対照的に、例外を意味のある例外、たとえば、CalculationServiceExceptionにラップすることを好みます。

なぜ早く投げ、なぜ遅れてキャッチするのですか?

165
shylynx

私の経験では、エラーが発生した時点で例外をスローするのが最善です。これは、例外がトリガーされた理由について最もよく知っているポイントだからです。

例外がレイヤーを巻き戻すので、キャッチして再スローすることは、例外にコンテキストを追加する良い方法です。これは別のタイプの例外をスローすることを意味する場合がありますが、これを行う場合は元の例外を含めます。

最終的に例外は、コードフローを決定できる層に到達します(たとえば、ユーザーにアクションを促す)。これは、最終的に例外を処理して通常の実行を継続する必要があるポイントです。

コードベースの実践と経験により、エラーに追加のコンテキストをいつ追加するか、実際に最も適切な場所で最終的にエラーを処理するかどうかを判断するのは非常に簡単になります。

キャッチ→再スロー

これを行うと、開発者が問題を理解するためにすべての層を介して作業する必要がなくなるため、より多くの情報を追加できます。

キャッチ→ハンドル

これは、ソフトウェアを介して何が適切であるが異なる実行フローであるかについて最終決定を下せるところで行います。

キャッチ→エラーリターン

これが適切な場合もありますが、Catch→Rethrow実装にリファクタリングする場合は、例外をキャッチしてエラー値を呼び出し元に返すことを検討する必要があります。

121
Michael Shaw

原因を見つけやすくするため、できるだけ早く例外をスローする必要があります。たとえば、特定の引数で失敗する可能性のあるメソッドを考えます。引数を検証し、メソッドの最初で失敗した場合、エラーが呼び出しコードにあることがすぐにわかります。失敗する前に引数が必要になるまで待機する場合は、実行を追跡して、呼び出しコードにバグがあるか(引数が正しくないか)、メソッドにバグがあるかを把握する必要があります。例外をスローするのが早いほど、根本的な原因に近くなり、どこで問題が発生したのかを簡単に把握できます。

例外がより高いレベルで処理される理由は、より低いレベルがエラーを処理するための適切な一連のアクションを知らないためです。実際、呼び出しコードが何であるかに応じて、同じエラーを処理する適切な方法が複数存在する可能性があります。たとえば、ファイルを開くとします。構成ファイルを開こうとしていて、そこにない場合は、例外を無視してデフォルトの構成を続行するのが適切な応答です。プログラムの実行に不可欠なプライベートファイルを開いていて、それがなんらかの理由で見つからない場合、おそらくプログラムを閉じることが唯一の選択肢です。

例外を正しいタイプでラップすることは、完全に直交する問題です。

58
Doval

他の人はかなり早くなぜ早く投げるのかを要約しています。代わりに、なぜ遅れるのかの部分に集中してみましょう。私の好みについては満足のいく説明がありません。

なぜ例外なのか?

そもそもなぜ例外が存在するのかについてはかなり混乱しているようです。ここで大きな秘密を共有しましょう。例外の理由と例外処理は... [〜#〜] abstraction [〜#〜]です。

次のようなコードを見たことがありますか?

_static int divide(int dividend, int divisor) throws DivideByZeroException {
    if (divisor == 0)
        throw new DivideByZeroException(); // that's a checked exception indeed

    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    try {
        int res = divide(a, b);
        System.out.println(res);
    } catch (DivideByZeroException e) {
        // checked exception... I'm forced to handle it!
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}
_

これは、例外の使用方法ではありません。上記のようなコードは実際に存在しますが、それらは異常であり、本当に例外です(しゃれ)。たとえばdivisionの定義は、純粋な数学でも、条件付きです。入力ドメインを制限するためにゼロの例外的なケースを処理する必要があるのは、常に「呼び出し元コード」です。それは醜いです。それは発信者にとって常に苦痛です。それでも、このような状況では、check-then-doパターンが自然な方法です。

_static int divide(int dividend, int divisor) {
    // throws unchecked ArithmeticException for 0 divisor
    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt();
    if (b != 0) {
        int res = divide(a, b);
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}
_

または、OOPこのようなスタイルで完全なコマンドを実行できます:

_static class Division {
    final int dividend;
    final int divisor;

    private Division(int dividend, int divisor) {
        this.dividend = dividend;
        this.divisor = divisor;
    }

    public boolean check() {
        return divisor != 0;
    }

    public int eval() {
        return dividend / divisor;
    }

    public static Division with(int dividend, int divisor) {
        return new Division(dividend, divisor);
    }
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    Division d = Division.with(a, b);
    if (d.check()) {
        int res = d.eval();
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}
_

ご覧のとおり、呼び出し元のコードには事前チェックの負担がありますが、その後の例外処理は行いません。 ArithmeticExceptiondivideまたはevalの呼び出しからのものである場合、それは[〜#〜] you [〜#〜]誰がcheck()を忘れたため、例外処理を実行してコードを修正してください。同様の理由で、NullPointerExceptionをキャッチすることはほとんどの場合間違った行為です。

現在、メソッド/関数のシグネチャの例外的なケースを見たい、つまり出力 domain を明示的に拡張したいという人がいます。彼らは チェックされた例外 を支持する人です。もちろん、出力ドメインを変更すると、直接の呼び出し元コードが強制的に適応されます。これは、チェックされた例外で実際に達成されます。 しかし、例外は必要ありません!これが_Nullable<T>_ 汎用クラスケースクラス代数的データ型 、および 共用体型Some OO people かもしれない 戻るのが望ましいnullこのような単純なエラーの場合:

_static Integer divide(int dividend, int divisor) {
    if (divisor == 0) return null;
    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    Integer res = divide(a, b);
    if (res != null) {
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}
_

技術的な例外は上記のような目的で使用できますが、ここにポイントがあります:例外はそのような使用法には存在しません。例外はプロによる抽象化です。例外は間接についてです。例外により、directクライアントコントラクトを壊すことなく「結果」ドメインを拡張し、エラー処理を「別の場所」に延期することができます。コードが、同じコードの直接の呼び出し元で処理される例外をスローし、間に抽象化のレイヤーがない場合は、それを行っていますW。 R. O. N. G。

遅れをとるには?

だからここにいる。上記のシナリオで例外を使用することは、例外の使用方法ではないことを示すために、自分の方法について議論しました。ただし、例外処理によって提供される抽象化と間接化が不可欠である、本物のユースケースが存在します。そのような使用法を理解することは、catch late推奨事項を理解するのにも役立ちます。

その使用例は次のとおりです。Resource Abstractionsに対するプログラミング ...

ええ、ビジネスロジックは 抽象化に対してプログラムされた である必要があり、具体的な実装ではありません。トップレベル [〜#〜] ioc [〜#〜] 「配線」コードは、リソース抽象化の具体的な実装をインスタンス化し、ビジネスロジックに渡します。ここには何も新しいものはありません。しかし、これらのリソース抽象化の具体的な実装は、独自の実装固有の例外をスローする可能性がありますね。

では、これらの実装固有の例外を処理できるのは誰ですか?その場合、ビジネスロジックでリソース固有の例外をまったく処理することは可能ですか?いいえ、そうではありません。ビジネスロジックは抽象化に対してプログラムされているため、これらの実装固有の例外の詳細に関する知識は除外されません。

「あはっ!」と言うかもしれません:「だからこそ、例外をサブクラス化して例外階層を作成できるのです」( Mr. Spring !をチェックしてください)。それは間違いです。まず、OOPに関するあらゆる合理的な本は、具体的な継承は悪いと述べていますが、何らかの理由で、JVMのこのコアコンポーネントである例外処理は、具体的な継承と密接に結びついています。皮肉なことに、Joshua Blochは彼の- 効果的なJava本 動作するJVMを体験する前に、そうすることができますか?これは、次世代のための「教訓」の本です。さらに重要なことに、高レベルの例外をキャッチした場合、それをどのように処理しますか?PatientNeedsImmediateAttentionException:私たちは彼女に錠剤を与えるか、彼女の足を切断する必要があります!?可能な限りすべての切り替えステートメントについてはどうですか?サブクラスですか?ポリモーフィズム、抽象化があります。

では、リソース固有の例外を処理できるのは誰ですか?それは具体化を知っているものでなければなりません!リソースをインスタンス化した人!もちろん「配線」コード!これをチェックしてください:

抽象化に対してコード化されたビジネスロジック...コンクリートリソースのエラー処理なし!

_static interface InputResource {
    String fetchData();
}

static interface OutputResource {
    void writeData(String data);
}

static void doMyBusiness(InputResource in, OutputResource out, int times) {
    for (int i = 0; i < times; i++) {
        System.out.println("fetching data");
        String data = in.fetchData();
        System.out.println("outputting data");
        out.writeData(data);
    }
}
_

一方、他のどこかで具体的な実装...

_static class ConstantInputResource implements InputResource {
    @Override
    public String fetchData() {
        return "Hello World!";
    }
}

static class FailingInputResourceException extends RuntimeException {
    public FailingInputResourceException(String message) {
        super(message);
    }
}

static class FailingInputResource implements InputResource {
    @Override
    public String fetchData() {
        throw new FailingInputResourceException("I am a complete failure!");
    }
}

static class StandardOutputResource implements OutputResource {
    @Override
    public void writeData(String data) {
        System.out.println("DATA: " + data);
    }
}
_

そして最後に配線コード...具体的なリソース例外を処理するのは誰ですか?それらについて知っている人!

_static void start() {
    InputResource in1 = new FailingInputResource();
    InputResource in2 = new ConstantInputResource();
    OutputResource out = new StandardOutputResource();

    try {
        ReusableBusinessLogicClass.doMyBusiness(in1, out, 3);
    }
    catch (FailingInputResourceException e)
    {
        System.out.println(e.getMessage());
        System.out.println("retrying...");
        ReusableBusinessLogicClass.doMyBusiness(in2, out, 3);
    }
}
_

今私と一緒に耐えます。上記のコードは簡単です。 IOCコンテナが管理するリソースの複数のスコープを持つエンタープライズアプリケーション/ Webコンテナがあり、セッションまたはリクエストスコープリソースなどの自動再試行と再初期化が必要な場合があります。より低いレベルのスコープには、リソースを作成するための抽象的なファクトリが与えられる可能性があるため、正確な実装を認識しません。より低いレベルのスコープだけが、それらのより低いレベルのリソースがスローできる例外を実際に認識します。

残念ながら、例外は呼び出しスタックを介した間接化のみを許可し、カーディナリティが異なるさまざまなスコープは通常、複数の異なるスレッドで実行されます。例外を除いて、それを介して通信する方法はありません。ここにはもっと強力なものが必要です。回答: 非同期メッセージ受け渡し 。下位レベルのスコープのルートですべての例外をキャッチします。何も無視しないでください、何もすり抜けさせないでください。これにより、現在のスコープの呼び出しスタックに作成されたすべてのリソースが閉じられ、破棄されます。次に、エラー処理ルーチンのメッセージキュー/チャネルを使用して、エラーメッセージを、具体的な情報がわかるレベルに到達するまで、上位のスコープに伝播します。それはその扱い方を知っている人です。

SUMMA SUMMARUM

したがって、私の解釈によれば、catch lateは、最も便利な場所で例外をキャッチすることを意味しますこれ以上、抽象を壊していないところ。早すぎないでください!リソース抽象化のインスタンスをスローする具体的な例外を作成するレイヤー、つまり抽象化の具体化を知っているレイヤーで例外をキャッチします。 「配線」レイヤー。

HTH。幸せなコーディング!

24
Daniel Dinnyes

この質問に正しく答えるために、一歩下がって、さらに根本的な質問をしてみましょう。

そもそもなぜ例外があるのですか?

例外をスローして、メソッドの呼び出し元に、要求された処理を実行できなかったことを通知します。例外のタイプが説明するwhyやりたいことができなかった。

いくつかのコードを見てみましょう:

double MethodA()
{
    return PropertyA - PropertyB.NestedProperty;
}

PropertyBがnullの場合、このコードは明らかにnull参照例外をスローする可能性があります。この場合、この状況を「修正」するために実行できることが2つあります。我々は出来た:

  • PropertyBがない場合は、自動的に作成します。または
  • 呼び出し側のメソッドまで例外をバブルさせます。

ここでPropertyBを作成することは非常に危険です。このメソッドがPropertyBを作成する理由は何ですか?確かに、これは単一責任の原則に違反します。おそらく、ここにPropertyBが存在しない場合は、何かが間違っていることを示しています。部分的に構築されたオブジェクトでメソッドが呼び出されているか、PropertyBが誤ってnullに設定されていました。ここでPropertyBを作成することで、データ破損の原因となるバグなど、後で発生する可能性のあるはるかに大きなバグを隠すことができます。

代わりに、null参照をバブルさせた場合、このメソッドを呼び出した開発者に、問題が発生したことをできるだけ早く知らせます。このメソッドを呼び出すという重要な前提条件が満たされていません。

つまり、懸念をより適切に分離するため、事実上、私たちは早期にスローしています。障害が発生したらすぐに、上流の開発者に通知します。

なぜ「遅れて捕まえる」のかは別の話です。問題を適切に処理する方法を知っているので、私たちは本当に遅れてキャッチするのではなく、できるだけ早くキャッ​​チしたいのです。時々、これは抽象化の15層になることもあれば、作成の時点になることもあります。

ポイントは、例外を適切に処理するために必要なすべての情報が揃った時点で例外を処理できるようにする抽象化のレイヤーで例外をキャッチしたいということです。

10
Stephen

オブジェクトを無効な状態にしないために、投げる価値のある何かを見つけたらすぐに投げます。つまり、nullポインターが渡された場合、それを早期にチェックしてNPE beforeをスローすることは、低レベルにトリクルダウンする可能性があることを意味します。

エラーを修正するために何をすべきかがわかったらすぐにキャッチします(これは通常、スローする場所ではなく、if-elseを使用できます)。無効なパラメーターが渡された場合、パラメーターを提供するレイヤーが結果に対処する必要があります。 。

6
ratchet freak

有効なビジネスルールは、「下位レベルのソフトウェアが値の計算に失敗した場合、...」です。

これは上位レベルでのみ表現できます。そうでない場合、下位レベルのソフトウェアは独自の正確さに基づいて動作を変更しようとしますが、結び目で終わるだけです。

4
soru

まず第一に、例外は例外的な状況のためのものです。あなたの例では、ロードできなかったために生データが存在しない場合、図を計算することはできません。

私の経験から、スタックをたどりながら例外を抽象化することは良い習慣です。通常、これを実行するポイントは、例外が2つのレイヤーの境界を越えるときです。

データレイヤーで生データを収集するときにエラーが発生した場合は、例外をスローして、データをリクエストした人に通知します。ここでこの問題を回避しようとしないでください。処理コードは非常に複雑になる可能性があります。また、データ層はデータを要求することのみを担当し、これを実行中に発生したエラーの処理は担当しません。これが"throwアーリー"の意味するところです。

あなたの例では、キャッチ層はサービス層です。サービス自体は、データアクセス層の上に位置する新しい層です。そこで、そこで例外をキャッチしたいとします。おそらく、サービスにフェイルオーバーインフラストラクチャがあり、別のリポジトリからデータをリクエストしようとしています。これも失敗する場合は、サービスの呼び出し元が理解できるものの内側に例外をラップします(それがWebサービスの場合、これはSOAP障害の可能性があります)。元の例外を内部例外として設定して、後でレイヤーは、問題の原因を正確に記録できます。

サービス障害は、サービスを呼び出すレイヤー(たとえば、UI)によってキャッチされる場合があります。そして、これは"catch late"が意味するものです。下位層で例外を処理できない場合は、再スローしてください。最上位のレイヤーが例外を処理できない場合は、それを処理してください!これには、ログの記録や表示が含まれる場合があります。

例外を再スローする必要がある理由(上記で説明したように、例外をより一般的な例外でラップするなど)は、たとえば、ポインターが無効なメモリを指しているためにエラーが発生したことをユーザーが理解できない可能性が高いためです。そして、彼は気にしません。彼はサービスによって数値が計算されなかったことを気にしているだけであり、これは彼に表示されるべき情報です。

さらに、理想的な世界では、UIからtry/catchコードを完全に除外できます。代わりに、下位層によってスローされる可能性のある例外を理解し、それらをいくつかのログに書き込んで、エラーの意味のある(おそらくローカライズされた)情報を含むエラーオブジェクトにラップするグローバル例外ハンドラーを使用します。これらのオブジェクトは、任意の形式(メッセージボックス、通知、メッセージトーストなど)でユーザーに簡単に提示できます。

2
Aschratt

壊れたコントラクトが必要以上にコードを通過しないようにするため、一般的には例外をスローすることをお勧めします。たとえば、特定の関数パラメーターが正の整数であることが予想される場合は、その変数がコードスタックの他の場所で使用されるまで待機するのではなく、関数呼び出しの時点でその制約を適用する必要があります。

遅れてキャッチすることはできません。自分のルールがあり、プロジェクトごとに異なるので、コメントすることはできません。私がやろうとしていることの1つは、例外を2つのグループに分けることです。 1つは内部使用のみで、もう1つは外部使用のみです。内部例外は私のコードによってキャッチおよび処理され、外部例外はコードが私を呼び出しているものによって処理されることを意図しています。これは基本的に後で物事をキャッチする形式ですが、内部コードで必要なときにルールから逸脱する柔軟性を提供するため、これは完全ではありません。

1
davidk01