メソッドが複数のタイプのチェック済み例外をスローしないのはなぜですか?


47

SonarQubeを使用してJavaコードを分析しますが、このルール(クリティカルに設定)があります。

パブリックメソッドは最大で1つのチェック済み例外をスローする必要があります

チェック例外を使用すると、メソッド呼び出し元はエラーを伝播または処理することでエラーを処理します。これにより、これらの例外はメソッドのAPIの一部になります。

呼び出し元の複雑さを合理的に保つために、メソッドは複数の種類のチェック済み例外をスローするべきではありません。」

ソナーの別のビットにはこれがあります

パブリックメソッドは最大で1つのチェック済み例外をスローする必要があります

チェック例外を使用すると、メソッド呼び出し元はエラーを伝播または処理することでエラーを処理します。これにより、これらの例外はメソッドのAPIの一部になります。

呼び出し元の複雑さを合理的に保つために、メソッドは複数の種類のチェック済み例外をスローするべきではありません。

次のコード:

public void delete() throws IOException, SQLException {      // Non-Compliant
  /* ... */
}

以下にリファクタリングする必要があります。

public void delete() throws SomeApplicationLevelException {  // Compliant
    /* ... */
}

オーバーライドするメソッドはこのルールによってチェックされず、いくつかのチェックされた例外をスローできます。

例外処理に関する私の読書でこの規則/推奨事項に出くわしたことは一度もなく、このトピックに関する標準、議論などを見つけようとしました。私が見つけた唯一のことはCodeRachからのこれです:メソッドは最大でいくつの例外をスローする必要がありますか?

これは広く受け入れられている標準ですか?


7
どう思いますか?SonarQubeから引用した説明は理にかなっているようです。それを疑う理由はありますか?
ロバートハーヴェイ14年

3
複数の例外をスローするコードを多数記述し、複数の例外もスローする多くのライブラリを使用しました。また、例外処理に関する書籍/記事では、スローされる例外の数を制限するトピックは通常取り上げられません。しかし、例の多くは、練習に暗黙の承認を与える複数のスロー/キャッチを示しています。そのため、このルールは驚くべきものであると判断し、例外処理のベストプラクティス/哲学について、基本的なハウツー例だけでなく、さらに調査したいと考えました。
sdoca

回答:


32

次のコードが提供されている状況を考えてみましょう。

public void delete() throws IOException, SQLException {      // Non-Compliant
  /* ... */
}

ここでの危険は、呼び出すために記述するコードが次のようにdelete()なることです。

try {
  foo.delete()
} catch (Exception e) {
  /* ... */
}

これも悪いです。そして、ベースの例外クラスをキャッチするフラグを立てる別のルールでキャッチされます。

重要なのは、他の場所に悪いコードを書きたくなるようなコードを書かないことです。

あなたが遭遇しているルールはかなり一般的なものです。 Checkstyleの設計ルールには次のようなものがあります。

ThrowsCount

throwsステートメントを指定されたカウント(デフォルトでは1)に制限します。

根拠:例外はメソッドのインターフェースの一部を形成します。あまりにも多くの異なるルートの例外をスローするようにメソッドを宣言すると、例外処理が面倒になり、catch(Exception ex)のようなコードを記述するなどのプログラミングプラクティスが貧弱になります。このチェックにより、開発者は例外を階層に入れ、最も単純なケースでは、呼び出し元が1種類の例外のみをチェックする必要がありますが、必要に応じてサブクラスを具体的にキャッチできます。

これは問題を正確に記述し、問題は何であり、なぜそれをしてはいけないかです。多くの静的分析ツールが識別してフラグを付けることは、広く受け入れられている標準です。

そして、あなた言語設計に従ってそれをするかもしれません、そしてそれが正しいことである時があるかもしれませんが、それはあなたが見て、すぐに行くべきものです。誰もが決して絶対catch (Exception e) {}に規律を守らない内部コードにとっては受け入れられるかもしれませんが、特に内部の状況では、多くの場合、私は人々がコーナーを切るのを見てきました。

クラスを使用している人に悪いコードを書きたがらせないでください。


Java SE 7以降では、1つのcatchステートメントで複数の例外をキャッチできるため、この重要性が低下していることを指摘しておく必要があります(複数の例外タイプをキャッチし、Oracleの改善されたタイプチェックで例外を再スロー)。

Java 6以前では、次のようなコードになりました。

public void delete() throws IOException, SQLException {
  /* ... */
}

そして

try {
  foo.delete()
} catch (IOException ex) {
     logger.log(ex);
     throw ex;
} catch (SQLException ex) {
     logger.log(ex);
     throw ex;
}

または

try {
    foo.delete()
} catch (Exception ex) {
    logger.log(ex);
    throw ex;
}

Java 6のこれらのオプションはどちらも理想的ではありません。最初のアプローチはDRYに違反しています。複数のブロックが同じことを何度も何度も繰り返します-例外ごとに1回。例外をログに記録して再スローしたいですか?OK。各例外に対して同じコード行。

2番目のオプションは、いくつかの理由により悪いです。まず、すべての例外をキャッチしていることを意味します。Nullポインターがそこにキャッチされます(そうすべきではありません)。さらに、あなたはコードを使用している人々が強制されているため、スタックをさらに混乱させるExceptionメソッドシグネチャが再スローされることを意味deleteSomething() throws Exceptionします。catch(Exception e)

Javaの7では、これがない、あなたが代わりに行うことができるので重要:

catch (IOException|SQLException ex) {
    logger.log(ex);
    throw ex;
}

1の場合はさらに、タイプはチェックしないスローされた例外の種類をキャッチ:

public void rethrowException(String exceptionName)
throws IOException, SQLException {
    try {
        foo.delete();
    } catch (Exception e) {
        throw e;
    }
}

タイプチェッカーは、それeがタイプまたはのみであることを認識します。私はまだこのスタイルの使用についてあまり熱心ではありませんが、Java 6の場合ほど悪いコードを引き起こしているわけではありません(例外が拡張するスーパークラスであるメソッドシグネチャを強制する場合)。IOExceptionSQLException

これらのすべての変更にもかかわらず、多くの静的解析ツール(Sonar、PMD、Checkstyle)がJava 6スタイルガイドを施行しています。それは悪いことではありません。私はこれらがまだ実施されるという警告に同意する傾向がありますが、あなたのチームがそれらに優先順位を付ける方法に従って、それらの優先度をメジャーまたはマイナーに変更するかもしれません。

例外は、オンまたはオフする必要がある場合は...それは問題であるG R 電子トンの 議論 1は、簡単に、引数の各側面を取って無数のブログ記事を見つけることができるということを。ただし、チェックされた例外を使用している場合は、少なくともJava 6で複数の型をスローすることは避けてください。


それがよく受け入れられている標準であるという私の質問に答えるので、これを答えとして受け入れます。しかし、私の標準がどうなるかを決定するために、彼の答えで提供されたリンクPanzercrisisから始まるさまざまな賛否両論の議論をまだ読んでいます。
sdoca 14年

「型チェッカーは、eがIOExceptionまたはSQLException型のみであることを認識します。」:これはどういう意味ですか?別のタイプの例外がスローされるとfoo.delete()どうなりますか?まだ捕まって再スローされていますか?
ジョルジオ

@Giorgio deleteその例でIOExceptionまたはSQLException以外のチェック済み例外をスローすると、コンパイル時エラーになります。私がやろうとしていた重要な点は、rethrowExceptionを呼び出すメソッドがJava 7の例外の型を取得することです。Java6では、それらすべてExceptionが静的分析や他のコーダーを悲しませている共通の型にマージされます。

そうですか。私には少し複雑に思えます。私はそれを禁止しcatch (Exception e)、強制的にどちらかcatch (IOException e)またはcatch (SQLException e)代わりにすることがより直感的だと思うでしょう。
ジョルジオ

@Giorgio優れたコードを簡単に記述できるようにすることは、Java 6からの一歩前進です。残念ながら、不良コードを作成するオプションは長い間使用されます。catch(IOException|SQLException ex)代わりにJava 7でできることを忘れないでください。ただし、例外を再スローするだけの場合は、型チェッカーが例外の実際の型を伝播できるようにして、コードを単純化することは悪いことではありません。

22

理想的には、1つのタイプの例外のみをスローしたい理由は、そうしないと、単一責任および依存関係反転の原則に違反する可能性が高いためです。例を使用して説明します。

パーシステンスからデータを取得するメソッドがあり、パーシステンスがファイルのセットであるとしましょう。ファイルを扱っているので、次のものがありますFileNotFoundException

public String getData(int id) throws FileNotFoundException

現在、要件が変更されており、データはデータベースから取得されています。FileNotFoundException(ファイルを処理していないため)の代わりに、次をスローしSQLExceptionます。

public String getData(int id) throws SQLException

メソッドを使用するすべてのコードを調べて、チェックする必要がある例外を変更する必要があります。変更しないと、コードはコンパイルされません。メソッドが広範囲に呼び出された場合、それを変更したり、他の人に変更させたりすることができます。それには多くの時間がかかり、人々は幸せになるつもりはありません。

依存関係の反転では、これらの例外はカプセル化に取り組んでいる内部実装の詳細を公開するため、実際にはこれらの例外のいずれもスローすべきではないと述べています。コードを呼び出すには、レコードを取得できるかどうかを本当に心配する必要があるときに、使用している永続性のタイプを知る必要があります。代わりに、APIを通じて公開しているのと同じ抽象化レベルでエラーを伝える例外をスローする必要があります。

public String getData(int id) throws InvalidRecordException

ここで、内部実装を変更した場合、その例外を単純にラップしてInvalidRecordException渡すことができます(またはラップせずに、単に新しいをスローしますInvalidRecordException)。外部コードは、どのタイプの永続性が使用されているかを知りません。すべてカプセル化されています。


単一責任に関しては、複数の無関係な例外をスローするコードについて考える必要があります。次のメソッドがあるとしましょう:

public Record parseFile(String filename) throws IOException, ParseException

この方法について何が言えますか?署名から、ファイル開いて解析することがわかります。メソッドの説明で「and」や「or」などの接続詞を見ると、複数のことを行っていることがわかります。それには複数の責任があります。複数の責任を持つメソッドは、責任が変更されると変更される可能性があるため、管理が困難です。代わりに、メソッドを分割して、単一の責任を持たせる必要があります。

public String readFile(String filename) throws IOException
public Record parse(String data) throws ParseException

データを解析する責任からファイルを読み取る責任を抽出しました。この副作用の1つは、任意の文字列データを任意のソース(メモリ内、ファイル、ネットワークなど)から解析データに渡すことができることparseです。ディスク上のファイルが不要なため、テストがより簡単になりました。に対してテストを実行します。


メソッドから実際にスローできる例外が2つ(またはそれ以上)ある場合もありますが、SRPとDIPに固執すると、この状況に遭遇する回数は少なくなります。


あなたの例のように低レベルの例外をラップすることに完全に同意します。これを定期的に行い、MyAppExceptionの分散をスローします。複数の例外をスローする例の1つは、データベース内のレコードを更新しようとしたときです。このメソッドはRecordNotFoundExceptionをスローします。ただし、レコードは特定の状態にある場合にのみ更新できるため、メソッドはInvalidRecordStateExceptionもスローします。これは有効であり、発信者に貴重な情報を提供すると思います。
sdoca 14

@sdoca updateメソッドが可能な限りアトミックであり、例外が適切な抽象化レベルにある場合、2つの例外的なケースがあるため、2つの異なるタイプの例外をスローする必要があるようです。これは、これらの(場合によっては任意の)リンタールールではなく、スローできる例外の数の測定値である必要があります。
cbojar 14

2
しかし、ストリームからデータを読み取り、処理しながらデータを解析するメソッドがある場合、ストリーム全体をバッファーに読み込まずにこれらの2つの関数を分割することはできません。さらに、例外を適切に処理する方法を決定するコードは、読み取りおよび解析を行うコードとは別にすることができます。読み取りコードと解析コードを書いているとき、コードを呼び出すコードがどちらのタイプの例外を処理するのかわからないので、両方を許可する必要があります。
user3294068

+1:特にモデルの観点から問題に取り組むため、私はこの回答がとても気に入っています。catch (IOException | SQLException ex)実際の問題はプログラムのモデル/設計にあるため、多くの場合、別のイディオム(など)を使用する必要はありません。
ジョルジオ

3

しばらく前にJavaで遊んでいたときに、これを少しいじったことを覚えていますが、質問を読むまでは、チェック済みと未チェックの違いを意識していませんでした。Googleでこの記事を見つけたのはかなり早かったのですが、明らかな論争がいくつかありました。

http://tutorials.jenkov.com/java-exception-handling/checked-or-unchecked-exceptions.html

そうは言っても、この男がチェック例外で言及した問題の1つは、throwsメソッド宣言の句に多数のチェック例外を追加し続けると、Javaで最初から個人的にこれに遭遇したことですより高いレベルのメソッドに移行するときに、それをサポートするためにもっと多くの定型的なコードを入れる必要がありますが、より多くの例外タイプをより低いレベルのメソッドに導入しようとすると、混乱が大きくなり、互換性が失われます。チェックされた例外タイプを下位レベルのメソッドに追加する場合、コードを再度実行し、他のいくつかのメソッド宣言も調整する必要があります。

記事で言及された軽減の1つのポイント-作成者は個人的にはこれが好きではありませんでした-基本クラス例外を作成し、throwsそれを使用するように句を制限し、そのサブクラスを内部で発生させるだけでした。これにより、すべてのコードを実行し直すことなく、新しいチェック済み例外タイプを作成できます。

この記事の著者はあまり好きではなかったかもしれませんが、私の個人的な経験では完全に理にかなっています(特に、すべてのサブクラスが何であるかを調べることができれば)、そしてそれがあなたに与えられているアドバイスがすべてを1つのチェック済み例外タイプにキャップします。さらに、あなたが言及したアドバイスは、非パブリックメソッドで複数のチェック例外タイプを実際に許可していることです。とにかく、それがプライベートメソッドまたは同様の何かである場合、1つの小さなことを変更するときにコードベースの半分を実行することはありません。

これは一般に受け入れられている標準かどうかを主に尋ねましたが、あなたが言及した研究、この合理的に考え抜かれた記事、そして個人的なプログラミング経験から言えば、決して目立ったものではないようです。


2
Throwable独自の階層をすべて作成するのではなく、単にthrowsを宣言し、それを実行しないのはなぜですか?
デデュプリケーター14年

@Deduplicatorこれは、著者がそのアイデアを好まなかった理由の一種です。彼は、あなたがそうするつもりなら、すべてのチェックされていないものを使用するかもしれないと考えました。ただし、API(おそらく同僚)を使用している人が、基本クラスから派生するすべてのサブクラス化された例外のリストを持っている場合、少なくとも予想されるすべての例外が存在することを知らせることで、少しのメリットが得られますサブクラスの特定のセット内。そして、それらのうちの1つが他のものよりも「処理可能」であると感じた場合、その特定のハンドラーを放棄する傾向はありません。
パンツァークライシス14年

一般に例外をチェックした理由は悪いカルマである理由は単純です:それらはすべてのユーザーに感染するバイラルです。意図的に最も広い可能性のある仕様を指定することは、すべての例外がチェックされていないことを示す方法であり、混乱を回避します。はい、ドキュメントのために、あなたが処理したいものを文書化することは良いアイデアです:関数からどの例外が発生する可能性があるかを知ることは、厳密に制限された値です。
デデュプリケーター14年

1
@Deduplicator私はこのアイデアを支持しているわけではありませんし、必ずしもそれを支持するわけでもありません。私は、OPから与えられたアドバイスがどこから来たのかについて話しているだけです。
パンツァークライシス14年

1
リンクをありがとう。これは、このトピックを読むのに最適な出発点でした。
sdoca 14年

-1

複数のチェック済み例外をスローすることは、実行すべき複数の妥当なことがある場合に意味があります。

たとえば、メソッドがあるとしましょう

public void doSomething(Credentials cred, Work work) 
    throws CredentialsRequiredException, TryAgainLaterException{...}

これは、pne例外ルールに違反しますが、意味があります。

残念ながら、通常起こるのは次のような方法です

void doSomething() 
    throws IOException, JAXBException,SQLException,MyException {...}

ここでは、呼び出し元が例外の種類に基づいて特定のことを行う可能性はほとんどありません。したがって、これらのメソッドが失敗する可能性があることを認識させたい場合は、SomethingMightGoWrongExceptionをスローするだけで十分です。

したがって、最大で1つのチェック例外をルールします。

ただし、プロジェクトで複数の意味のあるチェックされた例外があるデザインを使用している場合、このルールは適用されません。

補足:実際のところ、どこでも何かがうまくいかないことがあるので、?RuntimeExceptionを拡張しますが、「私たちはすべて間違いを犯します」と「これは外部システムと通信し、時にはダウンして対処します」との違いがあります。


1
「複数の合理的な物事を行うには、」ほとんどに準拠していないSRP -このポイントはにレイアウトされた前の答え
ブヨ

します。1つの機能を処理する(1つの側面を処理する)1つの問題と、別の問題(1つの側面を処理する)を検出できます。また、呼び出し側は、1つの層でtry catch(1つの関数)で1つの問題を処理し、別の問題を処理して、その問題を単独で処理することもできます。
user470365
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.