├── .gitignore ├── README.md ├── 0-title.md ├── 4-bibliography.md ├── 1-exception-principle.md ├── 2-java_exception.md └── 3-practice.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JJUG CCC 2019 Fall C+D 11:00 〜 11:45 『入門 例外』 発表資料 2 | === 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /0-title.md: -------------------------------------------------------------------------------- 1 | 入門 例外 2 | === 3 | 4 | * このセッションは Java の初級者・入門者を対象に Java の例外について学んでいくセッションです 5 | * 例外の基本的な考え方、 Java での例外の扱い方、そしてプラクティスを学んでいきます 6 | 7 | 自己紹介 8 | --- 9 | 10 | * もちだ 11 | * twitter: @mike_neck 12 | * 単なる Java 書いているおっさん 13 | * L is B という会社で direct というチャットサービスのサーバーアプリを Java で書いてる 14 | 15 | 16 | -------------------------------------------------------------------------------- /4-bibliography.md: -------------------------------------------------------------------------------- 1 | 参考文献 2 | === 3 | 4 | * バートランド・メイヤー『オブジェクト指向入門 第2版 原則・コンセプト』(翔泳社、2007年) 5 | * バートランド・メイヤー『オブジェクト指向入門 第2版 方法論・実践』(翔泳社、2007年) 6 | * ジョシュア・ブロック『Effective Java 第3版』(丸善、2018年) 7 | * Kevlin Henney編『プログラマが知るべき97のこと』(通称:きのこ本)(オライリー、2010年) 8 | * アンディー・ハント『達人プログラマー』(オーム社、新装版、2016年) 9 | * Benjamin C. Pierce 『型システム入門』(オーム社、2013年) 10 | * エリック・エヴァンス『ドメイン駆動設計』(翔泳社、2011年) 11 | * Vaughn Vernon 『実践ドメイン駆動設計』(翔泳社、2015年) 12 | * Robert C. Martin 『Clean Architecture』(アスキードワンゴ、2018年) 13 | 14 | 参考URL 15 | === 16 | 17 | * Replacing Throwing Exceptions with Notification in Validations - https://martinfowler.com/articles/replaceThrowWithNotification.html 18 | * What Is an Exception (The Java™ Tutorials) - https://docs.oracle.com/javase/tutorial/essential/exceptions/definition.html 19 | * 例外設計における大罪 - https://www.slideshare.net/t_wada/exception-design-by-contract 20 | * PHP7 で堅牢なコードを書く - https://speakerdeck.com/twada/php-conference-2016 21 | * Java の理論と実践 例外をめぐる議論 - https://www.ibm.com/developerworks/jp/java/library/j-jtp05254/index.html 22 | * Java の検査例外は、呼び出し側で「どんなに注意しても防げない」異常系 - https://qiita.com/yuba/items/d41290eca726559cd743 23 | * 段階的に理解する Java 例外処理 - https://qiita.com/ts7i/items/d7f6c1cd5a14e55943d4 24 | * 例外設計の話 - https://gist.github.com/sunaot/6138546 25 | * そのトランザクションは果たして本当にトランザクションなのだろうか? - https://yoskhdia.hatenablog.com/entry/2018/05/05/235452 26 | * 集約とトランザクション境界に関するメモ - https://dnskimox.hateblo.jp/entry/2018/12/22/154038 27 | -------------------------------------------------------------------------------- /1-exception-principle.md: -------------------------------------------------------------------------------- 1 | 例外の原理 2 | === 3 | 4 | 契約による設計の概念を通して、例外の定義や例外処理を確認します 5 | 6 | 1.例外の定義 7 | --- 8 | 9 | * メソッドの実行時に発生する、メソッドの仕様が満たせなくなるような状態やイベントのこと 10 | 11 | > An “exception” is an event, which occurs during the execution of a program, that disrupts the normal flow of the program’s instructions. 12 | > 13 | > (The Java™ Tutorial) 14 | 15 | * 「例外」とはプログラム実行中に発生するイベントである。そのイベントによってプログラムの命令の通常フローが阻害されます。 16 | 17 | ### まとめ 18 | 19 | * 例外とはメソッド実行中の状態・イベントである 20 | 21 | --- 22 | 23 | 2.メソッドの仕様 24 | --- 25 | 26 | * メソッドの内部動作ではなく、外から見た条件(ふるまいを記述したもの) 27 | * 条件には2種類ある 28 | * 事前条件 29 | * メソッドの実行開始時に満たす条件 30 | * メソッドの呼び出し側に責任がある 31 | * メソッドにとっての利益・信頼していい条件 32 | * 事後条件 33 | * メソッドの実行後に満たす条件 34 | * メソッドが保証する義務がある 35 | * メソッドの呼び出し元にとっての利益(期待している状態) 36 | * これとは別に事前条件・事後条件には以下の条件が含まれる 37 | * 不変条件(不変表明) 38 | * メソッドをもつオブジェクトが常に満たす条件 39 | * オブジェクトの特性をあらわす 40 | * メソッドの実行開始前、メソッドの実行終了後は必ず満たしている必要がある 41 | * メソッドの呼び出しは上記の責務 42 | 43 | ### 例 44 | 45 | * ビールの販売(1杯 `1,000 JPY`) 46 | * 事前条件 47 | * 購入する人は 20 歳以上 48 | * 購入する人は運転以外の手段で来場 49 | * `1,000 JPY` 以上のお金を支払う 50 | * 事後条件 51 | * キンキンに冷えたビール(`6 ± 2 ℃`)が渡される 52 | * 必要であればお釣りが渡される 53 | 54 | --- 55 | 56 | 3.メソッドの成功・失敗 57 | --- 58 | 59 | * 事前条件・事後条件を満たした状態でメソッドを開始・終了した場合を成功 60 | * メソッドが事前条件を満たした状態で開始される 61 | * 事後条件を満たした状態でメソッドの実行が終了する 62 | * 成功でないものを失敗 63 | 64 | ### 例外と成功・失敗 65 | 66 | * メソッドの失敗につながる実行時に発生する状態・イベントのことを例外 67 | * 依存する(呼び出した)メソッドの失敗は、呼び出し元のメソッドにとっての例外 68 | * 呼び出したメソッドが失敗した状態は、呼び出し元にとって事後条件の達成を阻害するため 69 | 70 | ### 例 ビールの販売 71 | 72 | * ビールの販売を例にメソッドの成功・失敗を考える 73 | * ビールの販売の事前条件(再掲) 74 | * 購入する人は 20 歳以上 75 | * 購入する人は運転以外の手段で来場 76 | * `1,000 JPY` 以上のお金を支払う 77 | * ビールの販売の事後条件(再掲) 78 | * キンキンに冷えたビール(`6 ± 2 ℃`)が渡される 79 | * 必要であればお釣りが渡される 80 | * ビールの販売の不変条件 81 | * ビールの販売数 x `1,000 JPY` `+` お釣りの準備金 `=` ビールの売り子が所持している金額 82 | 83 | ##### 例1 ビールの販売 - 成功 84 | 85 | * バスで来場した 20 歳以上のお客さんが、 `5,000 JPY` を渡して、ビールの売り子にビールを求める(事前条件を満たしたので実行開始) 86 | * ビールの売り子がビールをカップに注ぐ 87 | * お釣り入れに `5,000 JPY` をしまい、 `1,000 JPY` を `4` 枚取り出す 88 | * ビールの売り子がビールと、お釣りを渡して実行を終了する(事後条件を満たしているので成功) 89 | 90 | ##### 例2 ビールの販売 - 失敗 91 | 92 | * 徒歩で来場した 20 歳以上のお客さんが、 `1,000 JPY` を渡して、ビールの売り子にビールを求める(事前条件を満たしているので実行開始) 93 | * ビールの売り子がビールをカップに注ごうとしたところ、ビールが切れてしまう 94 | * すぐにはビールを調達できないので、諦める 95 | * ビールの売り子が「ビールを切らしてしまいました」と言い、受け取った `1,000 JPY` を返す 96 | 97 | ##### 例3 ビールの販売 - 失敗 98 | 99 | * バイクで来場した 18 歳の少年がビールの売り子にビールを求める(事前条件を満たさないので実行開始しない) 100 | * ビールの売り子が「ビールは 20 歳以上になってから」と言い、終了する 101 | 102 | --- 103 | 104 | 4.例外処理 105 | --- 106 | 107 | * メソッドの事後条件の達成が阻害された状態(例外状態)から、メソッドの実行終了までの制御 108 | * 例外処理には以下が含まれる 109 | * メソッドの成功・失敗を決定する 110 | * 様々な手段を駆使して、事後条件を満たす(リトライ) 111 | * 同じ処理を繰り返す 112 | * 異なる依存オブジェクトを通じて獲得する 113 | * 予め決められたものを適用する(デフォルト値) 114 | * 実行を終了して、失敗を呼び出し元に通知する(失敗/組織的パニック) 115 | * メソッドの実行によって壊れてしまった不変条件を元に戻す 116 | 117 | ### 例 ビールの販売 118 | 119 | * 徒歩で来場した 20 歳以上のお客さんが、 `5,000 JPY` を渡して、ビールの売り子にビールを求める(事前条件を満たしているので実行開始) 120 | * ビールの売り子がビールをカップに注ぐ 121 | * お釣りを手にとったところ、お釣りが `3,000 JPY` しかないため、お釣りが用意できない(例外) 122 | * 売り子が自分の財布を開けてみたところ、 `1,000 JPY` が `5` 枚入っていることを発見、お客の支払った `5,000 JPY` 紙幣と交換する 123 | * ビールの売り子がお客さんにビールと、お釣り `4,000 JPY` を渡して実行を終了する 124 | 125 | --- 126 | 127 | 5.例外の伝播 128 | --- 129 | 130 | * あるメソッドが失敗で終了した場合、その呼び出し元の例外として現れる 131 | * 呼び出し元がその例外を処理できない場合は、呼び出し元のメソッドが失敗として、さらにその呼び出し元に通知する 132 | * さらにその呼び出し元が例外を処理できない場合は...と例外は上位の呼び出し元に伝播していく 133 | 134 | 135 | --- 136 | 137 | まとめ 138 | --- 139 | 140 | * オブジェクトには特性をあらわす不変条件がある 141 | * メソッドの仕様は事前条件と事後条件で記述できる 142 | * 事前条件を保証するのはメソッドの呼び出し元の責任 143 | * 事後条件・不変条件を保証するのはメソッドの責任 144 | * 事前条件を満たす状態でメソッドが呼び出され、そのメソッドが事後条件を満たして実行を終了することを成功、そうでない場合は失敗 145 | * メソッドの失敗につながる実行時の諸状態が例外 146 | * メソッドの失敗は呼び出し元のメソッドにとっての例外 147 | * 例外処理 148 | * あらゆる方法で事後条件をみたすように回復する(リトライ) 149 | * メソッドの失敗として実行を終了し、呼び出し元に通知する(失敗) 150 | -------------------------------------------------------------------------------- /2-java_exception.md: -------------------------------------------------------------------------------- 1 | Java における例外 2 | === 3 | 4 | 1.例外はオブジェクト 5 | --- 6 | 7 | * Java における例外は実行中のメソッドの失敗を呼び出し元に通知するオブジェクト 8 | * すべての例外は `Exception` クラスの直接または間接のサブクラスのインスタンス 9 | * オブジェクトにする以外にも以下の方法があるが、オブジェクトが拡張性とシンプルさでよさそう 10 | * `int` 値を用いる方法 11 | * メリット 12 | * シンプル 13 | * C 言語などの API と同様な扱い方ができる 14 | * デメリット 15 | * 関数・ライブラリーごとにエラー値の体系が異なりミスを多発しやすい 16 | * 詳細なエラーの情報を添付できない 17 | * `String` を用いる 18 | * メリット 19 | * 例外の表現として柔軟・シンプル 20 | * 詳細情報が付与できる 21 | * デメリット 22 | * 例外ごとに情報を取り出すためのパーサーが必要になる 23 | 24 | 2.独自例外を作る 25 | --- 26 | 27 | * 独自の例外を作ると、きめ細かい例外処理が行える 28 | 29 | ### 独自例外の作り方 30 | 31 | * `Exception` クラスあるいは `RuntimeException` クラスを継承したクラスを作る 32 | 33 | ```java 34 | class NoBeerException extends Exception { 35 | NoBeerException(String message) { 36 | super(message); 37 | } 38 | } 39 | ``` 40 | 41 | * 独自例外クラスを用いると `Exception` クラスのインスタンスだけでは実現できない、豊富な情報を伝えられる 42 | 43 | ```java 44 | class ItemNotFoundException extends Exception { 45 | 46 | final int itemId; 47 | final int categoryId; 48 | 49 | ItemNotFoundException(int itemId, int categoryId) { 50 | super( 51 | "item not found, id: %d, category: %d" 52 | .format(itemId, categoryId)); 53 | this.itemId = itemId; 54 | this.categoryId = categoryId; 55 | } 56 | } 57 | ``` 58 | 59 | * 呼び出し元に例外処理を強制する場合は `Exception` クラスのサブクラスを作る 60 | * ただし `RuntimeException` のサブクラスにしない 61 | * 呼び出し元に例外処理を強制しない場合は `RuntimeException` クラスのサブクラスを作る 62 | 63 | --- 64 | 65 | 3.例外にメッセージ、原因を設定する 66 | --- 67 | 68 | `Exception` クラスのコンストラクターには以下のものがあり、例外の原因を伝えられる 69 | 70 | * `Exception(String message)` 71 | * `Exception(Exception cause)` 72 | * `Exception(String message, Exception cause)` 73 | 74 | `Exception` クラスから情報を取得する方法 75 | 76 | * `getMessage()` メソッドによって、例外に設定されたメッセージを取得できる 77 | * `getCause()` メソッドによって、例外の原因の例外を取得できる 78 | 79 | ```java 80 | Exception outOfGas = new NoBeerException("out of gas"); 81 | Exception exception = new NoBeerException("Beer has gone", outOfGas); 82 | 83 | exception.getMessage(); // Beer has gone 84 | exception.getCause(); // -> NoBeerException : out of gas 85 | ``` 86 | 87 | --- 88 | 89 | 4.発生箇所を特定する 90 | --- 91 | 92 | * `Exception` クラスには、呼び出しの階層を記録する機能がついている 93 | * `getStackTrace()` メソッドにより、呼び出しの階層の記録 `StackTraceElement` の配列を取得できる 94 | * `StackTraceElement` には次の情報が含まれている 95 | * クラス名 96 | * メソッド名 97 | * ファイル名 98 | * 行番号 99 | 100 | ### 例外発生箇所を特定する 101 | 102 | ```text 103 | java.lang.NullPointerException 104 | at com.example.Library.nameOf(Library.java:22) 105 | at com.example.UserCode$LibraryExecutor.execute(UserCode.java:49) 106 | at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Me 107 | at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMeth 108 | at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Delega 109 | at java.base/java.lang.reflect.Method.invoke(Method.java:567) 110 | at com.example.UserCode.reflectiveInvoke(UserCode.java:63) 111 | at com.example.UserCode.main(UserCode.java:31) 112 | ``` 113 | 114 | このスタックトレースからは... 115 | 116 | * `Library.java` というファイルの `22` 行目 117 | * `Library` というクラスにある `nameOf` というメソッドの中で 118 | * `NullPointerException` が発生した 119 | 120 | 5.失敗を呼び出し元に通知する方法 121 | --- 122 | 123 | * `return` の代わりに `throw` キーワードに例外オブジェクトを指定することでメソッドの実行を終了し、失敗したと呼び出し元に通知できる 124 | 125 | ```java 126 | if (beer.isPresent()) return Pair.of(beer.get(), change); 127 | 128 | // メソッドの実行を終了して失敗を通知 129 | throw new NoBeerException("beer has gone"); 130 | ``` 131 | 132 | * なお、通知する例外が `Exception` クラスのサブクラスで、 `RuntimeException` のサブクラスではない場合、 133 | メソッドのシグニチャー(メソッドの型情報)に `throws` キーワードと通知する例外クラスを記述する必要がある 134 | 135 | ```java 136 | public Pair pouringBeer() 137 | throws NoBeerException, NoChangeException { 138 | ... 139 | if (beer.isPresent()) return Pair.of(beer.get(), change); 140 | 141 | // メソッドの実行を終了して失敗を通知 142 | throw new NoBeerException("beer has gone"); 143 | } 144 | ``` 145 | 146 | 6.例外処理 147 | --- 148 | 149 | ### 例外処理をおこなう場合... 150 | 151 | * 例外が発生しうる箇所を `try {}` ブロックで囲む 152 | * 処理対象の例外型を `catch()` で指定し、例外パラメーターを受け取る 153 | * 続くブロックに例外処理を記述する 154 | 155 | ```java 156 | try { 157 | Beer beer = beerServer.pourBeer(1); 158 | Change change = changeReserve.prepareChange(payment); 159 | return Pair.of(beer, change); 160 | } catch (NoBeerException e) { 161 | // ビールがない場合の例外処理を記述する 162 | } catch (NoChangeException e) { 163 | // お釣りがない場合の例外処理を記述する 164 | } 165 | ``` 166 | 167 | ### `catch` のマッチング 168 | 169 | * `catch()` でのマッチングは上から順番に行われる 170 | * `Exception` や `RuntimeException` などのルートになる例外を上の方に指定すると、処理できないルートが発生し、コンパイルエラーになる 171 | 172 | ```java 173 | try { 174 | Beer beer = beerServer.pourBeer(1); 175 | Change change = changeReserve.prepareChange(payment); 176 | return Pair.of(beer, change); 177 | } catch (Exception e) { 178 | ... 179 | } catch (NoBeerException e) { // !!コンパイルエラーになる!! 180 | ... 181 | } 182 | ``` 183 | 184 | ### 最後にかならず行いたい処理がある場合 185 | 186 | * `finally {}` ブロックに処理を記述する 187 | * `AutoCloseable` の実装クラスを作り、 `close()` メソッドに処理を記述した上で、 `try() {}` のカッコ内に指定する 188 | 189 | ```java 190 | BeerValve valve = ...; 191 | try { 192 | valve.open(); 193 | MugOfBeer mugOfBeer = pourBeer(mugCup); 194 | return mugOfBeer; 195 | } finally { 196 | valve.close(); 197 | } 198 | ``` 199 | 200 | ```java 201 | BeerValve valve = ...; 202 | AutoCloseable closingValve = new AutoCloseable() { 203 | @Override public void close() throws Exception { 204 | valve.close(); 205 | } 206 | }; 207 | try(closingValve) { 208 | valve.open(); 209 | MugOfBeer mugOfBeer = pourBeer(mugCup); 210 | return mugOfBeer; 211 | } 212 | ``` 213 | 214 | * `AutoCloseable` は複数個指定できる 215 | 216 | ```java 217 | BeerValve valve = ...; 218 | AutoCloseable closingValve = valve::close; 219 | 220 | try(closingValve; MugHolder mugHolder = openMugHolder()) { 221 | MugCup mugCup = mugHolder.cupAtHead(); 222 | valve.open(); 223 | MugOfBeer mugOfBeer = pourBeer(mugCup); 224 | return mugOfBeer; 225 | } // mugHolder -> closingValve の順に実行される 226 | 227 | // try() の中にて取得したオブジェクトのスコープは try {} ブロック内 228 | ``` 229 | -------------------------------------------------------------------------------- /3-practice.md: -------------------------------------------------------------------------------- 1 | プラクティス 2 | === 3 | 4 | ここでは、これまでの自分の経験と書籍で学んだプラクティスを紹介していく。 5 | 6 | 1.ルート例外を使わない 7 | --- 8 | 9 | * ルートの例外(`Exception`/`RuntimeException`)で失敗を通知すると、メソッドの利用者にはどのような問題が発生したか判別がつかない 10 | 11 | メソッド 12 | 13 | ```java 14 | Beer pourBeer(Mug mug) throws Exception { 15 | MugOfBeer mugOfBeer = new MugOfBeer(mug.size()); 16 | while(tank.remains()) { 17 | BeerBuffer buffer = tank.bufferOfBeer(); 18 | mugOfBeer.add(buffer); 19 | if (nugOfBeer.isFilled()) return mugOfBeer.finish(tank.bubbles()); 20 | } 21 | throw new Exception("out of beer"); 22 | } 23 | ``` 24 | 25 | メソッドの呼び出し元メソッド 26 | 27 | ```java 28 | try { 29 | // RuntimeException のサブクラス NoMoreMugException で通知する 30 | Mug mug = mugHolder.head(); 31 | // Exception で通知する 32 | Beer beer = beerServer.pourBeer(mug); 33 | // NoChangeException で通知する 34 | Change change = changeReserve.prepareChange(payment); 35 | return Contract.success(beer, change); 36 | } catch (Exception e) { 37 | // BeerServer#pourBeer から通知された例外のハンドリング 38 | } 39 | ``` 40 | 41 | * `MugHolder#head` で発生した `NoMoreMugException` はこのメソッド内で例外処理をしたくないのだが、 42 | `Exception` を捕捉してしまうため、従来の意図に反して例外処理されてしまう 43 | * `ChangeReserve#prepareChange` から `NoChangeException` で通知された例外のハンドリングがないため 44 | 機能落ちしているが、そのバグ(事後条件違反)をコンパイラーで検出できない 45 | 46 | --- 47 | 48 | 2.例外のデフォルトコンストラクターを使わない - あるいは例外オブジェクトを生成する場合は必ず詳細メッセージを設定する 49 | --- 50 | 51 | * 例外を通知するメソッドのコードを書いている人には自明な状態でも、そのメソッドのユーザーにとっては自明ではない 52 | * 運用フェーズのエンジニア(エラーを分析してソフトウェアを改善する)にとっては、エラーメッセージだけが唯一の問題に関する情報なので、改善の機会の芽を摘まない 53 | 54 | ##### Slf4J - logback のログ出力 1 55 | 56 | クイズ : 以下のログから例外が発生した原因は何か分析せよ 57 | 58 | ```text 59 | 2019/05/21 21:16:11.871, main, INFO, com.example.web.OrderController, unexpected error 60 | com.example.beer.vendor.BeerLeakingException: null 61 | at com.example.beer.vendor.BeerValve.closeValve(BeerValve.java:1034) 62 | at com.example.beer.vendor.BeerServer.pourBeer(BeerServer.java:429) 63 | at java.base/jva.util.stream.ReferencePipleline$3$1.accept(Referencepipeline.j 64 | at java.base/jva.util.stream.ReferencePipleline$3$1.accept(Referencepipeline.j 65 | at java.base/jva.util.stream.ReferencePipleline$3$1.accept(Referencepipeline.j 66 | at java.base/java.util.AbstractList$RandomAccessSpliterator.forEachRemaining(A 67 | ... 68 | at com.example.beer.BeerService.newMugOfBeer(BeerService.java:643) 69 | at com.example.beer.OrderController.newMugOfBeer(OrderController.java:311) 70 | ``` 71 | 72 | 答え : わからん 73 | 74 | ##### Slf4J - logback のログ出力 2 75 | 76 | クイズ : 以下のログから例外が発生した原因は何か分析せよ 77 | 78 | ```text 79 | 2019/05/21 21:16:11.871, main, INFO, com.example.web.OrderController, unexpected error 80 | com.example.beer.vendor.BeerLeakingException: valve not closed completely, 81 | beer-temperature: 4C, outside-temperature: 33C, usage-count: 3045 82 | at com.example.beer.vendor.BeerValve.closeValve(BeerValve.java:1034) 83 | at com.example.beer.vendor.BeerServer.pourBeer(BeerServer.java:429) 84 | at java.base/jva.util.stream.ReferencePipleline$3$1.accept(Referencepipeline.j 85 | at java.base/jva.util.stream.ReferencePipleline$3$1.accept(Referencepipeline.j 86 | at java.base/jva.util.stream.ReferencePipleline$3$1.accept(Referencepipeline.j 87 | at java.base/java.util.AbstractList$RandomAccessSpliterator.forEachRemaining(A 88 | ... 89 | at com.example.beer.BeerService.newMugOfBeer(BeerService.java:643) 90 | at com.example.beer.OrderController.newMugOfBeer(OrderController.java:311) 91 | ``` 92 | 93 | 答え 94 | 95 | * バルブが完全に閉じていない 96 | * ビールの温度 4 ℃ 97 | * 外気温 33 ℃ 98 | * バルブの開閉回数は `3045` 回の時に発生 99 | 100 | ##### 回避方法 101 | 102 | * 独自例外クラスを作る場合は、デフォルトコンストラクターを利用できないようにする 103 | 104 | ```java 105 | class BeerLeakingException extends Exception { 106 | 107 | final int beerTemperature; 108 | final int outsideTemperature; 109 | final int usageCount; 110 | 111 | BeerLeakingException(String message, int beerTemperature, 112 | int outsideTemperature, int usageCount) { 113 | super(message); 114 | this.beerTemperature = beerTemperature; 115 | this.outsideTemperature = outsideTemperature; 116 | this.usageCount = usageCount; 117 | } 118 | // BeerLakingException() デフォルトコンストラクターを作らない 119 | } 120 | ``` 121 | 122 | ##### 注意 123 | 124 | * 例外のコンストラクターに設定するメッセージは、必ずしもエンドユーザーに見せるメッセージではない 125 | * `ResourceBundle` などの仕組みで解決する 126 | 127 | ```java 128 | try { 129 | 130 | } catch (BeerLeakingException e) { 131 | logger.info("unexpected error", e); 132 | return Response 133 | .temporaryUnavailable( 134 | Map.of("message", e.getMessage())); 135 | } 136 | ``` 137 | 138 | --- 139 | 140 | 141 | 3.例外を無視しない 142 | --- 143 | 144 | * 事後条件を保証しないバグ 145 | * 不変条件も壊している可能性があり、後で想定もできないような不具合に発展する可能性がある 146 | 147 | ### 例外無視の3大パターン 148 | 149 | * 何もしてない 150 | * 翻訳した例外が連鎖していない 151 | * `finally` で `return`/`throw` している 152 | 153 | ##### 何もしてない 154 | 155 | * 例外の発生に対して回復もせずに、成功のように振る舞うやつ 156 | * 問題の発生を検知できないため、発見が遅れて、その結果問題をややこしくしてしまう 157 | 158 | ```java 159 | void doPost(Request request, Buffer buffer) { 160 | try { 161 | Result result = service.doSomeService(request.getParam()); 162 | buffer.put(result.bytes()); 163 | } catch (SomeException e) { 164 | } 165 | } 166 | ``` 167 | 168 | * コンパイラーをうまく使えば発見・修正できるかもしれない 169 | * 例外処理を忘れたらコンパイルエラーになるプログラミングモデルにする 170 | * 具体的には `void` のメソッドをなくす 171 | * 弱点は例外を無視される場合、 `null` を返される 172 | 173 | ```java 174 | // void をやめて、 Result を返すようなメソッドにする 175 | Result doPost(Request request) { 176 | try { 177 | return service.doSomeService(request.getParam()); 178 | } catch (SomeException e) { 179 | return Result.defaultError(e.errorType()); 180 | } 181 | } 182 | ``` 183 | 184 | ##### 翻訳した例外が連鎖していない 185 | 186 | * 例外翻訳した際に発生する 187 | * ログ等から翻訳した場所はわかるものの、根本原因にたどり着けない 188 | 189 | ```java 190 | void doPost(Request request, Buffer buffer) { 191 | try { 192 | Result result = service.doSomeService(request.getParam()); 193 | buffer.put(result.bytes()); 194 | } catch (SomeException e) { // e が無視されている 195 | // 連鎖していない 196 | throw new ApplicationException("some exception”); 197 | } 198 | } 199 | ``` 200 | 201 | * IntelliJ IDEA なら対策はできそう 202 | * Preferences -> Inspections -> Java -> Error Handling -> Catch block may ignore exception にチェックする 203 | * Do not warn when 'catch' block is not empty にチェックする 204 | * Eclipse? ごめん... 205 | * spotbugs でできないか調べたが、ちょっとできなさそうだった(詳しい人教えて) 206 | 207 | ##### `finally` で `return`/`throw` している 208 | 209 | * `finally {}` での `return`/`throw` は `try {}` および `catch(){}` での `return`/`throw` を上書きしてしまう 210 | * `try {}` の中を適切に記述したのに、挙動がおかしくなる 211 | 212 | ```java 213 | Result doPost(Request request) { 214 | try { 215 | return service.doSomeService(request.getParam()); 216 | } catch (SomeException e) { 217 | throw new ApplicationException(e); 218 | } finally { 219 | return Result.getDefault(); // 優先される 220 | } 221 | } 222 | ``` 223 | 224 | * 一応、コンパイルエラーで防げる 225 | 226 | ```shellsession 227 | $ javac -Xlint:finally -Werror BadFainallyReturn.java 228 | BadFainallyReturn.java:13: 警告:[finally] finally節が正常に完了できません 229 | } 230 | ^ 231 | エラー: 警告が見つかり-Werrorが指定されました 232 | エラー1個 233 | 警告1個 234 | ``` 235 | 236 | * `AutoCloseable` を使って try-with-resources を徹底する 237 | * `finally` を使わせない 238 | 239 | 4.概念に適切な例外を用いる 240 | --- 241 | 242 | ### 適切なレイヤーの例外を返すようにする。業務ロジックに実装の詳細がわかってしまうような例外を通過させない 243 | 244 | * ビジネスロジック(業務専門のロジックが記述されている)とインフラ(データベースや外部のAPIと通信するレイヤー)の 245 | 境界にあたるインターフェースには実装がどのようであっても実装技術の例外を宣言しない 246 | * また、そのような例外を素通しさせない 247 | 248 | ```java 249 | interface FileStorage { 250 | ReservedFileSpace reserveFileSpace( 251 | UserId userId, AttachmentFile file); 252 | } 253 | ``` 254 | 255 | 上記のインターフェースに対して、 AWS を使っていてどうしても避けられないといって、次のような `throws` をつけない 256 | 257 | ```java 258 | interface FileStorage { 259 | ReservedFileSpace reserveFileSpace( 260 | UserId userId, AttachmentFile file) 261 | throws AmazonS3Exception; 262 | } 263 | ``` 264 | 265 | 実装クラスで発生を完全に防げない実装技術に関する例外が発生してしまう場合、 266 | 例外翻訳をおこなって、実装技術の情報が業務ロジックに現れないようにする 267 | 268 | ```java 269 | @Override 270 | public ReservedFileSpace reserveFileSpace( 271 | UserId userId, AttachmentFile file) 272 | throws TechnicalException { 273 | try { 274 | ...... 275 | } catch (AmazonS3Exception e) { 276 | throw new TechnicalException("URLの生成に失敗しました", e); 277 | } 278 | } 279 | ``` 280 | 281 | 5.検査例外の使い分け 282 | --- 283 | 284 | 特に事情がなければ検査例外を使わない 285 | 286 | ### 検査例外を使って良い条件 287 | 288 | Effective Java 71 より 289 | 290 | * API の適切な使用では例外状態を防げない 291 | * API を使っているプログラマが有用な例外処理ができる 292 | * API の利用者がモジュールの状態に関する知識を持っており、それを制御する API も提供している 293 | * API の利用者の不変条件がモジュールの状態を含んでいる 294 | 295 | ```java 296 | try { 297 | dao.beginTransaction(); 298 | dao.insert(userId, messageId, message); 299 | dao.commit(); 300 | } catch (DatabaseException e) { 301 | dao.rollback(); 302 | } 303 | ``` 304 | 305 | 6.本当に例外が必要なところだけに例外を使う 306 | --- 307 | 308 | * Effective Java 69 309 | * 例外は、その名が示す通り、例外的条件に対してのみ使うべきです。通常の制御フローに対しては、使うべきではありません。 310 | * 達人プログラマー 24 311 | * 「すべての例外ハンドラを除去しても、このプログラムは動作可能だろうか?」と自問してください。答えが「ノー」であれば、例外ではない状況下で例外が使われているはずです。 312 | 313 | ```java 314 | try { 315 | Iterator iterator = items.iterator(); 316 | while (true) { 317 | Item item = iterator.next(); 318 | if (item.hasStock()) { 319 | throw new ItemFoundException(item); 320 | } 321 | } 322 | } catch (ItemFoundException e) { 323 | return Optional.of(e.getItem()); 324 | } catch (NoSuchElementException e) { 325 | return Optional.empty(); 326 | } 327 | ``` 328 | 329 | * 不適切な用途に用いた例外は、読む人の注意を逸らせてしまう 330 | * 例外に基づくループは動作を保証できない 331 | * ループ内で同じ例外を発生させる箇所があった場合、ループ終了の例外であるかを判定できない 332 | * 結果、バグの存在を隠蔽してしまう 333 | 334 | ##### 意外なところに潜んでいる通常の制御フローにひそむ例外 335 | 336 | ```java 337 | class UserInputReport { 338 | final String reportDate = …; 339 | final String reportTitle = …; 340 | final String reportContent = …; 341 | 342 | void validate() { 343 | if (isInvalidDateFormat(reportDate)) { 344 | throw new BadUserInputException("report_date", "invalid format"); 345 | } 346 | if (!isLessThan120BytesInUtf8(reportTitle)) { 347 | throw new BadUserInputException("report_title", “too long"); 348 | } 349 | if (!isLessThan6000BytesInUtf8(reportTitle)) { 350 | throw new BadUserInputException("report_content", "too long"); 351 | } 352 | } 353 | } 354 | ``` 355 | 356 | 利用クラス 357 | 358 | ```java 359 | UserInputReport userInputReport = …; 360 | try { 361 | userInputReport.validate(); 362 | } catch (BadUserInputException e){ 363 | return Response 364 | .badRequest() 365 | .body(Map.of(e.getField(),e.getErrorMessage())); 366 | } 367 | ``` 368 | 369 | martinfowler.com より 370 | 371 | * Exceptions signal something outside the expected bounds of behavior of the code in question. But if you’re running some checks on outside input, this is because you expect some messages to fail - and if a failure is expected behavior, then you shouldn’t be using exceptions. 372 | (要約:ユーザー入力が間違えているのは予測の範囲内なので例外は使うべきでない) 373 | 374 | --- 375 | 376 | 修正方法 377 | 378 | * Notification パターンを使う 379 | 380 | ```java 381 | class UserInputReport { 382 | final String reportDate = …; 383 | final String reportTitle = …; 384 | final String reportContent = …; 385 | 386 | ErrorNotification validate() { 387 | List inputErrors = new ArrayList<>(); 388 | if (isInvalidDateFormat(reportDate)) { 389 | inputErrors.add(new InputError("report_date", "invalid format")); 390 | } 391 | if (!isLessThan120BytesInUtf8(reportTitle)) { 392 | inputErrors.add(new InputError("report_title", “too long")); 393 | } 394 | if (!isLessThan6000BytesInUtf8(reportTitle)) { 395 | inputErrors.add(new InputError("report_content", "too long")); 396 | } 397 | return new ErrorNotification(inputErrors); 398 | } 399 | } 400 | ``` 401 | 402 | 利用クラス 403 | 404 | ```java 405 | UserInputReport userInputReport = …; 406 | InputErrorNotification notification = userInputReport.validate(); 407 | if (notification.hasError()){ 408 | return Response 409 | .badRequest() 410 | .body(notification.errorMessages()); 411 | } 412 | ``` 413 | 414 | 制御フローから例外がいなくなったのがわかる 415 | --------------------------------------------------------------------------------