Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
レガシーシステムから学ぶ エラー処理との付き合い方
Search
uiui
January 29, 2025
1
82
レガシーシステムから学ぶ エラー処理との付き合い方
レガシーシステムで起きている問題を踏まえて、どうエラー処理と付き合うべきなのか、例外処理はどう使うべきなのかを探求するお話
uiui
January 29, 2025
Tweet
Share
Featured
See All Featured
Fantastic passwords and where to find them - at NoRuKo
philnash
51
3k
The Psychology of Web Performance [Beyond Tellerrand 2023]
tammyeverts
46
2.3k
Raft: Consensus for Rubyists
vanstee
137
6.8k
A Modern Web Designer's Workflow
chriscoyier
693
190k
[Rails World 2023 - Day 1 Closing Keynote] - The Magic of Rails
eileencodes
33
2.1k
Statistics for Hackers
jakevdp
797
220k
Thoughts on Productivity
jonyablonski
69
4.5k
Easily Structure & Communicate Ideas using Wireframe
afnizarnur
193
16k
The Illustrated Children's Guide to Kubernetes
chrisshort
48
49k
The World Runs on Bad Software
bkeepers
PRO
67
11k
Speed Design
sergeychernyshev
27
790
How STYLIGHT went responsive
nonsquared
98
5.4k
Transcript
エラー処理の付き合い方 2024-01-29 PHP勉強会 @uiui レガシーシステムから学ぶ
まず言葉の定義から この発表では、 プログラムが通常の処理を続行できない異常な事態を エラーと呼び、 その事態に対応することを エラー処理 と呼びます。 またtry-catch-finally, throwなどの構文を用いて、プログラム内で検知された異
常な事態を意図的に捕捉し、処理する仕組みを 例外処理と呼びます。
株式会社Clairvoyance Web・スマホアプリエンジニア エンジニア/PHP歴3年 PHPを中心にフロントエンド〜バックエンド
自己紹介 uiui 初めて発表します!ドキドキ …
皆さん、例外処理使ってますか?? *try-catch, throwを使っているかという意味でお聞きします
今、担当しているプロジェクト ・15年強の歴史を持つレガシーシステム コアな部分はPHP4時代に作られたものがベースとなっている ・Laravelを使った新プロジェクト
今、担当しているプロジェクト ・15年強の歴史を持つレガシーシステム コアな部分はPHP4時代に作られたものがベースとなっている ・Laravelを使った新プロジェクト try-catch, throw がほぼない
そう、例外処理を使わずとも、 書けるには書ける
ではPHPの例外処理は何のために存在するのか どう扱うべきなのか
本日のアジェンダ ・レガシーシステムで起きているエラー処理まわりの問題 ・例外処理を使用するメリット・デメリット ・他の言語の例外処理を見てみる ・エラー処理・例外処理とどう付き合っていくか
レガシーシステムで起きている エラー処理まわりの問題
PHP4時代のエラー処理 ・例外処理なし ・E_ERROR, E_WARNING, E_NOTICE 等のエラーが発生 し、警告が出力されたり、実行を中断したりする 制御する方法①error_reporting() で検知するエラーレベルを設定する
(出力する かどうかのみ。0に設定するとE_ERRORの時検知しないで止まってしまい、真っ 白になってしまう) display_errorsは出力を設定 制御する方法②set_error_handlerでエラー発生時の処理を定義する エラーレベルごとに処理を設定する(ログとか) 初期PHPはどうやってエラー処理をしていたのか?
file_get_contentsの仕様を見てみよう (PHP 4 >= 4.3.0, PHP 5, PHP 7,
PHP 8) file_get_contents — ファイルの内容を全て文字列に読み込む 読み込んだデータを返します。 失敗した場合false を返します。 戻り値 また多くの場合 E_WARNING レベルのエラーが発生する
PDO::queryは? (PHP 5 >= 5.1.0, PHP 7, PHP 8,
PECL pdo >= 0.2.0) PDO::query — プレースホルダを指定せずに、SQL ステートメントを 準備して実行する PDOStatement オブジェクトを返します。 失敗した場合に false を返します 戻り値 *例外を投げるモードに変更可能
PHPは戻り値でほとんどのエラーを扱っていた? この方式のまま 複雑なアプリケーションコードを実装していくと...
public function uploadIconImage($request_img) { /** @var ImageUploadService $this->uploader **/ $info
= $this->uploader->getInfo($request_img); if ($info['result'] !== false) { if ($info["size"] < ICON_SIZE) { if ($info["type"] == "png") { $result = $uploader->uploadImage($request_img, ICON_PATH); if ($result['result']) { return ICON_PATH; } else { return false; } } else { return false; } } else { return false; } } else { return false; } } うわぁ、分岐が … 早期リターンで 書いてみよう
public function getInfo($img) { if ($err) { return false; }
... return $info; } function UploadImage($img, $file_name = null) { if (!$img['file']['tmp_name']) { return false; } ... if ($err) { return false; } … } public function uploadIconImage($request_img) { /** @var ImageUploadService $this->uploader **/ $info = $uploader->getInfo($request_img); if ($info['result'] === false) { return false; } if ($info["size"] > ICON_SIZE) { return false; } if ($info["type"] != "png") { return false; } $result = $uploader->uploadImage($request_img, ICON_PATH); if (!$result['result']) { return false; } return ICON_PATH; } ImageUploadService IconImageService 🫠下層のどこでエラーが起きたかわからない … 🫠エラーに対するコード量が多い 🫠戻り値の型が mixed
public function uploadIconImage($request_img): array { /** @var ImageUploadService $this->uploader **/
$info = $this->uploader->getInfo($request_img); if ($info[“result”] === false) { return ["result" => false, "err_msg" => "エラー..."]; } if ($info["size"] != ICON_SIZE) { return ["result" => false, "err_msg" => "サイズが..."]; } if ($info["type"] != "png") { return ["result" => false, "err_msg" => "png形式..."]; } $result = $uploader->uploadImage($request_img, ICON_PATH); if ($result['result']) { return ["result" => true, "path" => ICON_PATH]; } else { return [ "result" => false, "err_msg" => $result[“err-mes”] ]; } } IconImageService public function getInfo($img): array { if ($err) { return ["result" => false,"err-mes" => "エラー..."]; } ... } function UploadImage($img, $file_name = null) { if (!$img['file']['tmp_name']) { return [ 'result' => false, 'err-mes' => 'ファイルを指定してください' ]; } ... if ($err) { return ["result" => false, "err-mes" => $err]; } … } ImageUploadService 🫠エラーをバケツリレーしなきゃいけない 🫠開発者によってキー名にばらつきが … 連想配列でエラーメッセージも一緒に返して、どこでエラーが起きたか一応わかるバージョン
//システム的なエラーだったらログ残したほうが … //でもバリデーションエラーでログ書かれたらめんどくさい $res = uploadIconImage($_FILES["icon_img"]); if (!$res["result"]) { $error_message_to_user
= $res["err_msg"]; } //失敗したときの処理書いてないような …? uploadIconImage($_FILES["icon_img"]); 🫠無視されやすい 🫠なんの種類のエラーなのかわからない (開発中もデバッグばかりする )
//システム的なエラーだったらログ残したほうが … //でもバリデーションエラーでログ書かれたらめんどくさい $res = uploadIconImage($_FILES["icon_img"]); if (!$res["result"]) { $error_message_to_user
= $res["err_msg"]; } //失敗したときの処理書いてないような …? uploadIconImage($_FILES["icon_img"]); 🫠無視されやすい なんの種類のエラーなのかわからない これは特に厄介!!
調査が長引いた謎バグを 思い浮かべてください
調査が長引く謎バグの特徴 ・エラーが出てない (だいぶ遠くでエラーが出る ) ・ログが残ってない ・想定外の事が起きても、終了せずになんとか生き永らえちゃって るプログラム if (ユーザが特定できたら)
{ if (データ更新できたら) { ... } } //なにもしてない!! 🫠開発者によってエラー処理にばらつきがある
課題まとめ 🫠戻り値の型が mixed 🫠下層のどこでエラーが起きたかわからない … 🫠エラーに対するコード量が多い 🫠エラーをバケツリレーしなきゃいけない 🫠なんの種類のエラーなのかわからない 🫠開発者によってエラー表現にばらつきがある
🫠エラーが無視されやすい 🫠開発者によってエラー処理にばらつきがある
例外処理を使用する メリット
public function uploadIconImage($request_img): string { /** @var ImageUploadService $this->uploader **/
$info = $this->uploader->getInfo($request_img); if ($info["size"] != ICON_SIZE) { throw new IconImageInvalid("サイズが…"); } if ($info["type"] != "png") { throw new IconImageInvalid("png形式で.."); } $uploader->uploadImage($request_img, ICON_PATH); return ICON_PATH; } IconImageService 😎戻り値の型を一貫して保てる mixedじゃなく、エラーを返す表現ができる! 正常ならstring型が、 異常時はエラーがスローされる
public function uploadIconImage($request_img): array { /** @var ImageUploadService $this->uploader **/
$info = $this->uploader->getInfo($request_img); if ($info["size"] != ICON_SIZE) { throw new IconImageInvalid("サイズが…"); } if ($info["type"] != "png") { throw new IconImageInvalid("png形式で.."); } $uploader->uploadImage($request_img, ICON_PATH); return ICON_PATH; } IconImageService public function getInfo($img): array { if ($err) { throw new ImageDataRetrievalException("取得できず"); } ... } public function UploadImage($img, $file_name = null) { if (!$IMG['file']['tmp_name']) { throw new FailedUploadException("ファイルが..."); } … if ($err) { throw new FailedUploadedException("なにかのエラー "); } … } ImageUploadService 😎上位層でtry-catch を書くだけ (バケツリレーなし ) 😎コード量が減った 大域脱出 try { $service->uploadIconImage($file); } catch (Exception $e) { showError($e->getMessage()); }
Exception自体が便利なんだよな Exception::getMessage — 例外メッセージを取得する Exception::getCode — 例外コードを取得する Exception::getFile —
例外が作られたファイルを取得する Exception::getTrace — スタックトレースを取得する (プログラム実行時の関数呼び出し履歴) 😎エラー表現は Exceptionで統一 欲しい機能は備わっている 😎どこでエラーが起きたのかスタックトレースでわかる
独自例外クラス class FailedUploadedException extends RuntimeException { } class IconImageInvalidException
extends ValidationException { } try { $service->uploadIconImage($file); } catch (ValidationException $e) { $error_message_to_user = $e->getMessage(); } 外部環境要因なのでRuntimeExceptionをベースに ユーザの入力の問題なのでバリデーションエラー バリデーションエラー以外は キャッチしない 😎エラーの種類別に 処理を分けられる
PHPDocで例外出るメソッドだよと伝える /** * @throws ValidationException */ public function uploadIconImage($request_img):
string { … $service->uploadIconImage($file); 🧐戻り値で処理してた時よりは無視されなさそう? 😃もしエラーが起きても落ちてくれるのでまあ良さそう PHPStormで波線引かれる
例外処理で解決できそうなこと 😎戻り値の型が mixed 😎下層のどこでエラーが起きたかわからない … 😎エラーに対するコード量が多い 😎エラーをバケツリレーしなきゃいけない 😎なんの種類のエラーなのかわからない 😎開発者によってエラー表現にばらつきがある
😃エラーが無視されやすい 🫠開発者によってエラー処理にばらつきがある
例外処理を使用する デメリット ここからは体験より、調べて学んだことが多いです
ちゃんと例外安全な環境じゃないと throw文を追加する時の影響範囲が広い 関数の呼び出し元について、推移的なものまで含めて、すべて調べ、 最低限 の基本的な例外安全保証を満たしているか、途中で終了しても問題ないよう な作りになっているか確認しなければなりません。 By Google
C++ スタイルガイド https://ttsuki.github.io/styleguide/cppguide.ja.html#Exceptions 🧐途中で終了した時のリソースの解放やロールバックなどの考慮 🧐ユーザに直接的なエラー画面が出ないように
呼び出し元は例外を投げるかどうか知っておく必要がある (状況・環境によりそう) ・これは戻り値のパターンでも、どの状態だと失敗するのか知らないと詰まるし なぁ ・例外安全の環境が約束できなければ確認しなくてはいけない ・PHPDocをちゃんと書けばIDEが教えてくれる 🧐例外を投げるとは上位の誰かに処理を委ねること
(他人任せ)
プログラムの流れを追うのが難しくなる 例外が投げられると、関数の途中の思いもしないところから、唐突にその呼び出し元へと処理が 戻ってしまいます。 このことは、メンテナンス性の低下や、デバッグの難しさにつながります。 エラーが特別な制御構造を使用する場合、エラー処理によって、エラーを処理するプログラムの 制御フローが歪んでしまいます。 Java のようなスタイルのtry-catch-finally ブロックは、複
雑に相互作用する複数の重複する制御フローを織り交ぜます。 By Google C++ スタイルガイド https://ttsuki.github.io/styleguide/cppguide.ja.html#Exceptions Google で学ぶ: ソフトウェア エンジニアリングに役立つ言語設計 https://go.dev/talks/2012/splash.article#TOC_16. 🧐複雑な構成・変な使い方するとプログラムを追うのがしんどそう
適切でない例外の使い方をしたときによりデバッグ・運用が大変になりそう① try { validateInput($input); saveToDatabase($input); sendConfirmationEmail($input); } catch (Exception
$e) { Log::logput("エラーが発生しました : " . $e->getMessage()); } 🫠抽象的すぎて何が起きたかわからない、、 メッセージだけ残されましても ....
適切でない例外の使い方をしたときによりデバッグ・運用が大変になりそう② try { // 何らかの処理 } catch (Exception $e)
{ // スタックトレースなしで再スロー throw new Exception("エラーが発生しました"); //throw new Exception("エラーが発生しました: " . $e->getMessage(), 0, $e); } 🫠スタックトレースが途中で消えて流れを追えなくなる 階層が深い時スタックトレースないと厳しいよね …
適切でない例外の使い方をしたときによりデバッグ・運用が大変になりそう③ try { $service->purchase($item); } catch (Throwable $e) {
// 実装ミスなども含めて全ての例外をキャッチしてしまう echo "購入に失敗しました "; //本来は業務エラーだけキャッチする予定だった } 🫠意図しないエラーもキャッチしちゃう
例外処理 デメリット まとめ 🫠例外安全なプログラム・環境でないと考えることが多くなる 🫠プログラムの流れを追う時のコストが高くなる 🫠開発者に例外処理を扱う知識・もしくはガイド /規約の用意が必要
例外処理 デメリット まとめ 🫠例外安全なプログラム・環境でないと考えることが多くなる 🫠プログラムの流れを追う時のコストが高くなる 🫠開発者に例外処理を扱う知識・もしくはガイド /規約の用意が必要 下手すると、エラーが追えなくなったり・気が付かなかったりと メリットを帳消しにしかねない
他の言語の例外処理を見てみ る
Rust・Go言語は基本的に戻り値でエラー処理 Go ・errを検査しないと正常値を取得できない関数がほとんど ・大域脱出などは基本的にしない ・プログラムが続行不可能な致命的なエラーの時はpanicを発生させ、 recoverで異常終了を防いだり、回復させたりする(多分あまり使わない) Rust ・Result型で返し、errを検査できる ・「?」をつけてearly
returnの短縮系を書ける。 コード量は比較的少なそう
なぜ例外処理を採用しなかったのか ・例外は例外的な問題にのみ使用するべき →通知が多すぎて埋もれる・消したくなる ・エラー処理によってプログラムの制御フローを歪ませたくない →特に間違った使い方の時に、処理の流れを追う時のコスト増。 ・エラー処理を強く意識させたい →例外を投げると他人任せで何も考えなくなる
Go言語について調べてみた
例外多すぎ問題 例外は例外的なものではなくなり、ありふれたものになりました。無害な ものから壊滅的なものまであらゆる場合に使用され、例外の重大度の違 いは関数の呼び出し元によって異なります。 Go 言語の貢献者 Dave Cheneyのブログ https://dave.cheney.net/2012/01/18/why-go-gets-exceptions-right
catch (Exception e) { // 無視 } 多すぎる例外に嫌気がさしたプログラマはこうする 🫠やっぱり例外も 容易に無視できる
例外的なものとそうでないものってなんだろう エラーはどう分類できるだろうか ・システムエラー 業務上想定しない技術的な問題のエラー ・ネットワークエラー ・データベース接続エラー ・バグ 等々
・業務エラー 業務上想定されている範囲内の異常事態で、誤入力・誤操作などが原因 ・入力のバリデーション ・ポイント不足ですよ とか ・回数上限に到達してますよ とか
・システムエラー 業務上想定しない技術的な問題のエラー ・ネットワークエラー ・データベース接続エラー ・バグ 等々 ・業務エラー 業務上想定されている範囲内の異常事態で、誤入力・誤操作などが原因 ・入力のバリデーション ・ポイント不足ですよ とか
・回数上限に到達してますよ とか ログに残し、管理者に連絡 ユーザに失敗理由を表示 管理者には連絡しない! 例外的なものとそうでないものってなんだろう エラーはどう分類できるだろうか
新たに浮上した問題 🧐業務エラーは例外処理で対応すべきなのか ・例外的(予期してない致命的)な事柄ではない(準正常系とも言われている) ・頻繁に起こる事態に対して強制力がありすぎる (処理をやめてコールスタックをさかのぼり、その問題にすぐ向き合わなくてはいけない という)
エラー処理・例外処理とどう付き合っていくか
例外処理のメリット・デメリット・議題まとめ 😎戻り値の型を一貫して保てる 😎バケツリレーなしでコード量が減る 😎エラー表現の統一 😎スタックトレース 😎エラー種類別に処理を書ける 😎(catch しなければ )その場で落とせる
🫠例外安全か気にする必要がある 🫠プログラムの流れを追う時のコスト増 🫠例外の扱い方とガイドの共有コスト増 (🫠エラー処理が他人任せ ) *↑が守られないと、デメリットの方が上回る メリット デメリット 🧐業務エラーは例外処理で対応すべきなのか 議題
🫠例外安全か気にする必要がある 🫠プログラムの流れを追う時のコスト増 🫠例外の扱い方とガイドの共有コスト増 (🫠エラー処理が他人任せ ) デメリット 我々次第 ・アプリのシステムデザイン ・プログラマの知識 ・ガイドの共有
エラーのあるべき姿は何か? ・本当に致命的で続行不可能な、通知すべきエラーだけ通知する ・重要度が低いエラーはやかましく通知しない ・エラーの詳細・流れはすぐわかるように ・アプリケーションのコードの可読性は損ないたくない
PHPの言語特性・Laravelの環境などから考えてみる Laravelだったら「例外安全な環境」になってるはず。 独自フレームワーク(もしくは素のPHP?)の場合は当たり前だがその環境による。 いずれにしろ、アプリ開発者が例外を投げられても問題ないように作っている かは気 にしなくてはいけない(ロールバックとか) 例外安全な環境なのか
PHPの言語特性・Laravelの環境などから考えてみる チーム全体で例外の使い方・アンチパターンなどを確認するべき。 また意図せず重大なエラーをキャッチしてしまったりしないように、 アプリ全体で使用するエラー クラス・継承するエラー等のルール の設定が必要かも(できるだけシンプルに) どういう時に例外処理を使うか・キャッチするか のルール設定も必要 すべての開発者がそのルールを知って理解しておくことが必要になり、ここにコストがかかる
ことを受け入れなくてはいけない 例外の扱い方とガイドの共有
PHPの言語特性・Laravelの環境などから考えてみる (まず例外処理の使い方は開発者全員がマスターしている前提で話す) ・アプリの超上位層(普段は触らないような)でエラー処理を用意しておき、エラーをキャッチす る場面を限定してしまえば、エラー処理を比較的一元管理・定型化できるのでは? 例外処理だとエラー処理が他人任せになる
PHPの言語特性・Laravelの環境などから考えてみる ・LaravelではバリデーションとかもExceptionで扱っているし、基本的に業務エラーも例外処理で 対応するという考え方なのではないかと思う。 ・どのシチュエーションの時にどの Exceptionクラスを使う(継承する)か決めて、そのエラーごとに エラー処理をアプリの超上位層で定義しておけば、重要度によって処理分けできて解決する? ・思想にもよる 業務エラーは例外処理で対応すべきなのか
まとめ
PHPでのエラー処理・例外処理の向き合い方 • 例外を扱う準備が整っているか(例外安全な環境か) • 開発者に例外の扱い方・ガイドを知ってもらうように動けるか 例外処理を使うかどうかの観点 例外処理を使う際に気をつけること
• チームで例外の扱い方・アンチパターンなども確認すること • アプリのシステムデザインでも例外処理が扱いやすいように設計すること
ご清聴ありがとうございました!
参考文献・資料 Google C++ スタイルガイド https://ttsuki.github.io/styleguide/cppguide.ja.html#Exceptions Go 言語の貢献者 Dave Cheneyのブログ
https://dave.cheney.net/2012/01/18/why-go-gets-exceptions-right Google で学ぶ: ソフトウェア エンジニアリングに役立つ言語設計 https://go.dev/talks/2012/splash.article#TOC_16. PHP7で堅牢なコードを書く https://speakerdeck.com/twada/php-conference-2016 PHPでthrowしない例外ハンドリング https://speakerdeck.com/tanden/phpdethrowsinaili-wai-handoringu ちょっと広く例外を学んでみた https://qiita.com/TairaNozawa/items/8788c4b20c60046ee80c ソフトウェアデザイン 2024年9月号