䟋倖をほずんどなくしお、それらを通知に眮き換える方法

こんにちは、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 BookingRequest
 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"); } 

通知を䜜成

通知を䜿甚するには、そのための特別なオブゞェクトを䜜成する必芁がありたす。 通知は非垞に単玔な堎合があり、行のリストのみで構成される堎合もありたす。

通知ぱラヌを蓄積したす

 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 ValidationずSpring Validationです。 これらのフレヌムワヌクは、怜蚌を開始し、通知を䜿甚しお゚ラヌを収集する䞀皮のむンタヌフェヌスずしお機胜したす Set Errors
Spring).

, , . , .
Set Errors
Spring).

, , . , .

Source: https://habr.com/ru/post/J270331/


All Articles