例外をほとんどなくして、それらを通知に置き換える方法

こんにちは、Habr。



名前を翻訳するだけの記事に出くわすことがあります。 さらに興味深いのは、そのような記事がさまざまな言語の専門家に役立つかもしれないが、Javaの例が含まれている場合です。 すぐに、Javaに関する大規模な本の出版に関する最新のアイデアを皆さんと共有したいと考えていますが、現時点では、まだロシア語に翻訳されていない2014年12月のMartin Fowlerの出版に精通してください。 翻訳は少し縮小して行われます。



データを検証する場合、通常、検証が失敗したことを示すシグナルとして例外を使用しないでください。 ここでは、「通知」パターンを使用した同様のコードのリファクタリングについて説明します。



最近、JSONメッセージの最も単純な検証を実行するコードに出会いました。 次のように見えました。



public void check() { if (date == null) throw new IllegalArgumentException("date is missing"); LocalDate parsedDate; try { parsedDate = LocalDate.parse(date); } catch (DateTimeParseException e) { throw new IllegalArgumentException("Invalid format for date", e); } if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today"); if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null"); if (numberOfSeats < 1) throw new IllegalArgumentException("number of seats must be positive"); }
      
      







これが、通常の検証方法です。 データにいくつかの検証オプションを適用します(上記はクラス全体のほんの数フィールドです)。 少なくとも1つの検証ステップが失敗すると、エラーメッセージとともに例外がスローされます。



このアプローチにはいくつかの問題がありました。 まず、このような場合に例外を使用するのは好きではありません。 例外は、問題のコード内の何らかの異常なイベントのシグナルです。 ただし、外部入力をチェックする場合、入力するメッセージにエラーが含まれている可能性があると想定します。つまり、エラーが予想され、この場合は例外を適用するのは正しくありません。



このようなコードの2番目の問題は、最初のエラーが検出された後にクラッシュした場合、最初のエラーだけでなく、入力データで発生したすべてのエラーを報告することをお勧めします。 この場合、クライアントはすべてのエラーをユーザーにすぐに表示できるため、ユーザーは1回の操作でエラーを修正し、ユーザーがコンピューターで猫とマウスを再生することを強制しません。



そのような場合、「通知」パターンを使用して検証エラーのレポートを整理することを好みます。 通知はエラーを収集するオブジェクトであり、検証に失敗するたびに、別のエラーが通知に追加されます。 検証メソッドは通知を返します。通知を解析して、詳細情報を確認できます。 簡単な例は、チェックを実行するための次のコードです。



 private void validateNumberOfSeats(Notification note) { if (numberOfSeats < 1) note.addError("number of seats must be positive"); //    }
      
      





その後、aNotification.hasErrors()のような単純な呼び出しを行って、エラーがあれば応答します。 通知の他の方法では、より詳細なエラー情報を掘り下げることができます。







そのようなリファクタリングをいつ適用するか



ここで、コードベース全体の例外を取り除くことをお勧めしません。 例外は、緊急事態を処理し、それらをメインの論理ストリームから削除する非常に便利な方法です。 提案されたリファクタリングは、例外を使用して報告された結果が実際に例外ではないため、プログラムのメインロジックによって処理される必要がある場合にのみ適切です。 ここで考慮される検証は、まさにそのような場合です。



例外を実装するときに使用する必要がある便利な「鉄則」は、Pragmatic Programmersにあります。



例外は、プログラムの通常の過程の一部として散発的にのみ使用されるべきであると考えています。 例外的なイベントでそれらに正確に頼る必要があります。 キャッチされなかった例外がプログラムを終了させ、「例外ハンドラーが削除された場合、このコードは機能し続けますか」と自問してください。 答えが「いいえ」の場合、例外は非排他的な状況でおそらく適用されます。




-デイブ・トーマスとアンディ・ハント



したがって、重要な結果:特定のタスクに例外を使用するかどうかの決定は、そのコンテキストに依存します。 そのため、DaveとAndyは続けて、未知のファイルからの読み取りは異なるコンテキストで例外になる場合とそうでない場合があります。 Unixシステムの/ etc / hostsなど、よく知られている場所からファイルを読み取ろうとしている場合、ファイルがそこにあると想定することは論理的であり、そうでない場合は例外をスローすることをお勧めします。 一方、コマンドラインでユーザーが入力したパスに沿ってあるファイルを読み取ろうとする場合、ファイルが存在しないと想定し、別のメカニズム(エラーの非排他的な性質を示すもの)を使用する必要があります。



検証エラーに例外を使用するのが賢明な場合があります。 状況は暗示されています:処理の初期段階で既に検証されているはずのデータがありますが、ソフトウェアエラーから安全にするために、このようなチェックを再度行いたいため、無効なデータがすり抜けることがあります。



この記事では、生の入力検証のコンテキストで、除外を通知に置き換えることについて説明します。 このような手法は、通知が例外よりも適切なオプションである場合に役立ちますが、ここでは検証が最も一般的なオプションに焦点を当てます。



開始する



これまでのところ、コードの最も一般的な構造のみを説明したため、サブジェクト領域については言及していません。 しかし、その後、この例の開発では、主題領域をより正確に概説する必要があります。 これは、劇場の座席の予約に関するメッセージをJSON形式で受け取るコードになります。 このコードは予約リクエストクラスで、gsonライブラリを使用してJSON情報に基づいて入力されます。



 gson.fromJson(jsonString, BookingRequest.class)
      
      





Gsonはクラスを取得し、JSONドキュメント内のキーに一致するフィールドを探し、それらのフィールドに入力します。



この予約リクエストには、検証する2つの要素のみが含まれます。イベントの日付と指定席数です。



クラスBookingRequest ...



  private Integer numberOfSeats; private String date;   — ,     class BookingRequestpublic void check() { if (date == null) throw new IllegalArgumentException("date is missing"); LocalDate parsedDate; try { parsedDate = LocalDate.parse(date); } catch (DateTimeParseException e) { throw new IllegalArgumentException("Invalid format for date", e); } if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today"); if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null"); if (numberOfSeats < 1) throw new IllegalArgumentException("number of seats must be positive"); }
      
      





通知を作成



通知を使用するには、そのための特別なオブジェクトを作成する必要があります。 通知は非常に単純な場合があり、行のリストのみで構成される場合もあります。



通知はエラーを蓄積します



 List<String> notification = new ArrayList<>(); if (numberOfSeats < 5) notification.add("number of seats too small"); //     // … if ( ! notification.isEmpty()) //   
      
      





単純なリストのイディオムはパターンの軽量な実装を提供しますが、そこで停止して単純なクラスを作成することは好みません。



 public class Notification { private List<String> errors = new ArrayList<>(); public void addError(String message) { errors.add(message); } public boolean hasErrors() { return ! errors.isEmpty(); } …
      
      







実際のクラスを使用して、意図をより明確に表現します。コードリーダーは、イディオムとその完全な意味を精神的に関連付ける必要はありません。



検証方法を部品に分解します



まず、検証方法を2つの部分に分けます。 内部部分は通知でのみ機能し、例外をスローしません。 外部部分は、検証メソッドの実際の動作を保持します。つまり、検証が失敗すると例外がスローされます。



これを行うために、私が最初に使用するのは、異常な方法でメソッドを選択することです。検証メソッドの全体を検証メソッドに入れます。



クラスBookingRequest ...



  public void check() { validation(); } public void validation() { if (date == null) throw new IllegalArgumentException("date is missing"); LocalDate parsedDate; try { parsedDate = LocalDate.parse(date); } catch (DateTimeParseException e) { throw new IllegalArgumentException("Invalid format for date", e); } if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today"); if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null"); if (numberOfSeats < 1) throw new IllegalArgumentException("number of seats must be positive"); }
      
      







次に、通知を作成して返すように検証メソッドを修正します。



クラスBookingRequest ...



  public Notification validation() { Notification note = new Notification(); if (date == null) throw new IllegalArgumentException("date is missing"); LocalDate parsedDate; try { parsedDate = LocalDate.parse(date); } catch (DateTimeParseException e) { throw new IllegalArgumentException("Invalid format for date", e); } if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today"); if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null"); if (numberOfSeats < 1) throw new IllegalArgumentException("number of seats must be positive"); return note; }
      
      







これで、通知を確認し、エラーが含まれている場合は例外をスローできます。



クラスBookingRequest ...



  public void check() { if (validation().hasErrors()) throw new IllegalArgumentException(validation().errorMessage()); }
      
      





検証メソッドを公開しました。ほとんどの呼び出し元は、テストメソッドよりも見込み客よりもこのメソッドを使用することを好むと思われるためです。



元の方法を2つの部分に分けることで、検証チェックとエラーへの対応方法の決定を区別します。



この段階では、コードの動作にまったく触れていません。 通知にはエラーが含まれず、失敗した検証チェックは引き続き例外をスローし、ここで追加する新しい機構を無視します。 しかし、例外の問題を通知に置き換える段階を設定しています。



これを開始する前に、エラーメッセージについて何か言う必要があります。 リファクタリングの際には、観察された動作の変更を避けるというルールがあります。 このような状況では、このルールはすぐに疑問を提起します。どのような動作が観察可能ですか? 明らかに、適切な例外をスローすることは外部プログラムにとってある程度顕著ですが、エラーメッセージはどの程度関連していますか? その結果、通知には多くのエラーが蓄積され、次のような1つのメッセージにまとめられます。



クラス通知...



  public String errorMessage() { return errors.stream() .collect(Collectors.joining(", ")); }
      
      





しかし、ここで問題が発生するのは、より高いレベルで、最初に見つかったエラーについてのみメッセージを受信することにプログラムの実行が結びついており、次のようなものが必要な場合です。



クラス通知...



  public String errorMessage() { return errors.get(0); }
      
      







この状況に対する適切な応答を判断するには、呼び出し側の関数だけでなく、すべての例外ハンドラーにも注意を払う必要があります。



ここで問題を引き起こすことはできませんでしたが、新しい変更を加える前にこのコードをコンパイルしてテストします。



番号検証



この場合の最も明らかなステップは、最初の検証を置き換えることです。



クラスBookingRequest ...



  public Notification validation() { Notification note = new Notification(); if (date == null) note.addError("date is missing"); LocalDate parsedDate; try { parsedDate = LocalDate.parse(date); } catch (DateTimeParseException e) { throw new IllegalArgumentException("Invalid format for date", e); } if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today"); if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null"); if (numberOfSeats < 1) throw new IllegalArgumentException("number of seats must be positive"); return note; }
      
      





明らかなステップですが、コードを壊すため、悪いです。 関数に空の日付を渡すと、コードは通知にエラーを追加しますが、すぐに解析し、nullポインター例外をスローします。この例外には関心がありません。



したがって、この場合は、それほど単純ではないが、より効果的なこと、つまり後退することをお勧めします。



クラスBookingRequest ...



  public Notification validation() { Notification note = new Notification(); if (date == null) throw new IllegalArgumentException("date is missing"); LocalDate parsedDate; try { parsedDate = LocalDate.parse(date); } catch (DateTimeParseException e) { throw new IllegalArgumentException("Invalid format for date", e); } if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today"); if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null"); if (numberOfSeats < 1) note.addError("number of seats must be positive"); return note; }
      
      





前のチェックはゼロチェックであるため、nullポインター例外をスローしないようにする条件付きコンストラクトが必要です。



クラスBookingRequest ...



  public Notification validation() { Notification note = new Notification(); if (date == null) throw new IllegalArgumentException("date is missing"); LocalDate parsedDate; try { parsedDate = LocalDate.parse(date); } catch (DateTimeParseException e) { throw new IllegalArgumentException("Invalid format for date", e); } if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today"); if (numberOfSeats == null) note.addError("number of seats cannot be null"); else if (numberOfSeats < 1) note.addError("number of seats must be positive"); return note; }
      
      





次のチェックは別のフィールドに影響することがわかります。 リファクタリングの前の段階で条件付き構成を導入する必要があるだけでなく、検証方法が非常に複雑で分解される可能性があるように思えます。 そのため、数値の検証を担当するパーツを選択します。



クラスBookingRequest ...



  public Notification validation() { Notification note = new Notification(); if (date == null) throw new IllegalArgumentException("date is missing"); LocalDate parsedDate; try { parsedDate = LocalDate.parse(date); } catch (DateTimeParseException e) { throw new IllegalArgumentException("Invalid format for date", e); } if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today"); validateNumberOfSeats(note); return note; } private void validateNumberOfSeats(Notification note) { if (numberOfSeats == null) note.addError("number of seats cannot be null"); else if (numberOfSeats < 1) note.addError("number of seats must be positive"); }
      
      





強調表示された数値検証を見て、その構造が好きではありません。 検証時にif-then-elseブロックを使用するのは好きではありません。添付ファイルの数が多すぎるコードは非常に簡単なためです。 私は、プログラムの実行が不可能になるとすぐに動作を停止する線形コードを好みます。そのようなポイントは、境界条件を使用して決定できます。 これが、ネストされた条件構成の境界条件への置換を実装する方法です。



クラスBookingRequest ...



  private void validateNumberOfSeats(Notification note) { if (numberOfSeats == null) { note.addError("number of seats cannot be null"); return; } if (numberOfSeats < 1) note.addError("number of seats must be positive"); }
      
      







リファクタリングするときは、常に最小限の手順で既存の動作を維持するようにしてください。



一歩後退するという私の決定は、リファクタリングにおいて重要な役割を果たします。 リファクタリングの本質は、コードの構造を変更することですが、実行された変換によって動作が変更されないようにします。 したがって、リファクタリングは常に小さな手順で実行する必要があります。 そのため、デバッガで追い越す可能性のあるエラーに対して保証します。



日付検証



日付を検証するとき、メソッドを強調表示することから再び始めます



クラスBookingRequest ...



  public Notification validation() { Notification note = new Notification(); validateDate(note); validateNumberOfSeats(note); return note; } private void validateDate(Notification note) { if (date == null) throw new IllegalArgumentException("date is missing"); LocalDate parsedDate; try { parsedDate = LocalDate.parse(date); } catch (DateTimeParseException e) { throw new IllegalArgumentException("Invalid format for date", e); } if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today"); }
      
      





IDEで自動メソッド割り当てを使用した場合、結果のコードには通知引数が含まれていませんでした。 そこで、手動で追加しようとしました。



日付を検証するときに戻りましょう:



クラスBookingRequest ...



  private void validateDate(Notification note) { if (date == null) throw new IllegalArgumentException("date is missing"); LocalDate parsedDate; try { parsedDate = LocalDate.parse(date); } catch (DateTimeParseException e) { throw new IllegalArgumentException("Invalid format for date", e); } if (parsedDate.isBefore(LocalDate.now())) note.addError("date cannot be before today"); }
      
      





2番目の段階では、スローされた例外に条件例外があるため、エラー処理で問題が発生します。 処理するには、そのような例外を受け入れることができるように通知を変更する必要があります。 私はちょうどそこにいるので、例外をスローすることを拒否し、通知を処理します-私のコードは赤です。 したがって、ロールバックして、上記のフォームのvalidateDateメソッドを終了し、条件付き例外を受け取る通知を準備します。



通知の変更を開始し、条件を受け入れるaddErrorメソッドを追加してから、元のメソッドを変更して、新しいメソッドを呼び出せるようにします。



クラス通知...



  public void addError(String message) { addError(message, null); } public void addError(String message, Exception e) { errors.add(message); }
      
      





したがって、条件付き例外を受け入れますが、無視します。 どこかに配置するには、エラーレコードを単純な文字列から少し複雑なオブジェクトに変換する必要があります。



クラス通知...



 private static class Error { String message; Exception cause; private Error(String message, Exception cause) { this.message = message; this.cause = cause; } }
      
      







Javaのプライベートフィールドは好きではありませんが、ここではプライベートの内部クラスを扱っているため、すべてが私に合っています。 通知以外の場所でこのエラークラスへのアクセスを開く場合、これらのフィールドをカプセル化します。



クラスがあります。 ここで、文字列ではなく、通知を使用するように変更する必要があります。



クラス通知...



  private List<Error> errors = new ArrayList<>(); public void addError(String message, Exception e) { errors.add(new Error(message, e)); } public String errorMessage() { return errors.stream() .map(e -> e.message) .collect(Collectors.joining(", ")); }
      
      







新しい通知があると、予約リクエストに変更を加えることができます



クラスBookingRequest ...



 private void validateDate(Notification note) { if (date == null) throw new IllegalArgumentException("date is missing"); LocalDate parsedDate; try { parsedDate = LocalDate.parse(date); } catch (DateTimeParseException e) { note.addError("Invalid format for date", e); return; } if (parsedDate.isBefore(LocalDate.now())) note.addError("date cannot be before today");
      
      





すでに選択したメソッドにいるので、returnコマンドを使用して残りの検証を簡単に取り消すことができます。



最後の変更は非常に簡単です。



クラスBookingRequest ...



 private void validateDate(Notification note) { if (date == null) { note.addError("date is missing"); return; } LocalDate parsedDate; try { parsedDate = LocalDate.parse(date); } catch (DateTimeParseException e) { note.addError("Invalid format for date", e); return; } if (parsedDate.isBefore(LocalDate.now())) note.addError("date cannot be before today"); }
      
      







スタックアップ



新しいメソッドが作成されたので、次のタスクは、元の検証メソッドを呼び出す人を確認し、これらの要素が将来新しい検証メソッドを使用するように調整することです。 ここでは、このような構造がアプリケーションの一般的なロジックにどのように適合するかをより広いコンテキストで検討する必要があるため、この問題はここで検討するリファクタリングの範囲を超えています。 しかし、中期的な目標は、検証が失敗する可能性があるすべての場合に広く適用されている例外の使用を取り除くことです。



多くの場合、これにより検証メソッドを完全に取り除くことができます。検証メソッドで動作するように、それに関連するすべてのテストを書き換える必要があります。 さらに、通知のエラーが正しく蓄積されていることを確認するために、テストの修正が必要になる場合があります。



フレームワーク



多くのフレームワークは、通知パターンを使用して検証する機能を提供します。 Javaでは、 Java Bean ValidationSpring Validationです。 これらのフレームワークは、検証を開始し、通知を使用してエラーを収集する一種のインターフェースとして機能します( Set Errors

Spring).



, , . , .








Set Errors





Spring).



, , . , .







All Articles