31
2018

業務エラー例外の作り方の考察

CATEGORYPHP
今更なネタではあるものの、かれこれ1x年ぐらいサーバーサイド中心に開発してるけどいまだに悩むので考察を整理。

まずここでいう業務エラー例外ってのは、業務固有の例えば「ポイントが足りません」「利用期間が終了しました」みたいなエラーを扱う例外のこと。
業務って言ってるけど別にお仕事的なシステムに限らず、ソシャゲでもWebサービスにでも何でもある(だろう)やつ。

昔っからあるはずなのに、何故か定番という感じのものにお目にかからず、プロジェクトごとに実装方法が違ったりするので、ちょっと今まで目にしたやつとか考察してみる。
(なお、Webアプリや特定の言語に限った話ではないけれど、以下PHPのWebアプリをイメージして考察。)

1. 業務例外クラス+エラーコード

たぶん一番基本的であろうパターン。単純に ApplicationException みたいな専用の例外クラスを作って、そこにエラーの種類ごとに採番したエラーコードを指定するやつ。エラーコードはExcelやWikiの台帳で管理する。
class ApplicationException extends Exception {
public function __construct(int $code) {
parent::__construct('', $code);
}
}

function checkPoint(int $point) : void {
if ($point < 0) {
throw new ApplicationException(1001);
}
}
メリットは、簡単だし要件としてはこれでも一応十分なこと。
デメリットは、エラーコードだけだと何のエラーか分かりにくいっていうのと、エラーの種類を判定する手段が $e->getCode() === 1001 みたいなイケてない形になってしまうこと。

2. 業務例外クラス+エラーコード定数

1のパターンの派生版で、エラーコードをアプリ内でconst定義しただけ。
const ERROR_CODE_FATAL = 9999;
const ERROR_CODE_INVALID_ID = 1000;
const ERROR_CODE_POINT_IS_NOT_ENOUGH = 1001;

class ApplicationException extends Exception {
public function __construct(int $code) {
parent::__construct('', $code);
}
}

function checkPoint(int $point) : void {
if ($point < 0) {
throw new ApplicationException(ERROR_CODE_POINT_IS_NOT_ENOUGH);
}
}
const化されることで、さっきの何のエラーか分かりにくいという問題は解決する。
しかし、constが数百行に渡って定義される羽目になったり、コンフリクトしまくったりするので、個人的にはお勧めしない。
それにconstでやるぐらいなら、素直に次の3のパターンを使った方がよいと思う。

3. エラーの種類ごとの業務例外クラス

たぶんこれが一番正統なやり方?普通に業務エラーの種類ごとに例外クラスを定義する奴。
class ApplicationException extends Exception {}

class PointIsNotEnoughException extends ApplicationException {
public function __construct() {
parent::__construct('point is not enough', 1001);
}
}

function checkPoint(int $point) : void {
if ($point < 0) {
throw new PointIsNotEnoughException();
}
}
メリットは、分かりやすいし拡張も利用も容易だし、例外になんか情報(後述の5のマスタみたいなの)を持たせることだってできるしと多数。
デメリットは、業務エラーだとエラーの種類が場合によっては数百やそれ以上に達することもあるのだけれど、全部別の例外にすると、夥しい数のクラスが出来てしまうこと。
違うエラーなんだからちゃんと一つ一つクラスを作るのが正しい、という考え方もあるんだろうけど…この数はちょっとインパクトがある。

4. HTTPエラーで代用

3のWebアプリ限定な機能制限バージョン。業務例外を細かく区別することを諦め、全部400や500といったHTTPエラーにまとめてしまうという方法。
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

function checkPoint(int $point) : void {
if ($point < 0) {
throw new BadRequestHttpException('point is not enough');
}
}
メリットは、非常に単純で分かりやすいこと。HTTPエラーなら大半のプログラマーは理解できるから、学習コストが0で済む。例外クラスもSymfonyやらフレームワーク側で用意されてたりするし、エラーメッセージを変えれば最低限エラーの内容を伝えることもできる。
デメリットは、まあ一目瞭然だけど、エラーを細かく区別できないので、エラーに応じた制御などを行えないこと。なので、簡単な管理画面などごく小規模なシステムでしか使えない。後から直すのも手間なので、ちゃんとした開発では採用できないだろう。

なお、エラーに応じた制御を行うためにエラーメッセージの内容で区別する、という手も無くはないけど…それは単にエラーコードを文字列で再発明してるだけなので止めましょう。

5. 業務例外クラス+エラーコード+エラーコードマスタ

1のパターンの派生版で、アプリ内にCSVなりでエラーコードを管理するマスタを持たせるやり方。
ErrorCode,Message,LogLevel,HttpStatusCode,ResultCode
9999,"Fatal Error",Error,500,ERR9999
1000,"Invalid Id",Warn,404,ERR1000
1001,"Point is not enough",Debug,400,ERR2001
$errorMasters = /* CSVを連想配列で読み込む */;

class ApplicationException extends Exception {
public function __construct(int $code) {
$this->master = $errorMasters[$code];
parent::__construct($master['Message'], $code);
}
}

function checkPoint(int $point) : void {
if ($point < 0) {
throw new ApplicationException(1001);
}
}
業務例外クラスの形や使い方は1とほぼ同じ。上のマスタはWeb APIをイメージした例で、対応するエラーメッセージの他、ログレベルやHTTPステータスコード、それにAPI戻り値用に別のエラーコードを持たせている。
こういうマスタを加えることで、エラーを一元管理できるし、共通的な仕組みでエラーの種類に応じて条件を切り分けたりもできるしで、かなりよさげ。
ただ、デメリットも1と同じで、例外を投げてるとこだけ見ても、何のエラーだかパッと見分かり辛い。この点は改善してない。

6. 業務例外クラス+エラーコード+エラーコードマスタ+頻出業務エラーの例外クラス

基本5だけど一部で3も使うパターン。
ErrorCode,Message,LogLevel,HttpStatusCode,ResultCode
9999,"Fatal Error",Error,500,ERR9999
1000,"Invalid Id",Warn,404,ERR1000
1001,"Point is not enough",Debug,400,ERR2001
$errorMasters = /* CSVを連想配列で読み込む */;

class ApplicationException extends Exception {
public function __construct(int $code) {
$this->master = $errorMasters[$code];
parent::__construct($master['Message'], $code);
}
}

class InvalidIdException extends ApplicationException {
public function __construct() {
parent::__construct(1000);
}
}

function findById(int $id) : User {
$user = /* IDで検索する処理 */;
if (!$user) {
throw new InvalidIdException();
}
return $user;
}
function checkPoint(int $point) : void {
if ($point < 0) {
throw new ApplicationException(1001);
}
}
ようするに、基本は汎用的な業務例外+エラーコードで運用するけど、よく使うエラーコードだけは使い易いように例外クラス作りましょうということ。
これなら、エラーが分かりにくいという問題にも、例外クラスが増えすぎるという問題にも対処できると思われる。
しいてデメリットを上げるなら、catch (InvalidIdException $e) では new ApplicationException(1000) は補足できないから、中途半端に混在してると事故るかもという辺りですかね…?

1,2,3,5はお仕事で、4は趣味でやった事がある。6はまだない。
でも、悪くはないと思うので、次にエラー周り設計する機会があれば6でやってみようかなーと思案中。

おまけ)良いエラーコード

エラーコードはconstだろうが台帳やマスタがあろうがなかろうが、最終的には戻り値なりログなりどこかで生のコード値として登場してくる。
コード自体に意味を持たせるのはあまり好きではないのだが、とはいえ実際のところ、エラーコードを見ただけで内容が推測できた方が遥かに使い勝手がよい。
という事で、おまけとして、分かりやすいエラーコードについても少し考察してみる。

重要なエラーには分かりやすいコードを割り当てる

例えば、Fatal Errorだったら-1や9999とか、DB接続エラーは1、通信エラーは2で、一般のエラーは1001から採番するとか、そういうの。よく出てくるor重要なエラーコードはみんな最終的に覚えるので、初めから分かりやすい値にしといた方がよい。

カテゴリ分けする

例えば、DBのエラーは1000番台、ネットワークのエラーは2000番台みたいにするとか、または機能ごとにユーザー系は1000番台、商品系は2000番台にするとか、そういうの。エラーコードだけ見て、どの辺の問題か察せられると楽。
また、可能であればエラーコードを文字列にして ERR001, EDB001, ENW001 みたいにするとより分かりやすい。

なお「重要なエラーには分かりやすいコードを」は各カテゴリ内にも適用すべきで、例えばDB接続エラーを EDB001 にして、普通のDB系エラーは EDB010 以降とかするとより分かりやすい。

HTTPステータスコードと被らないようにする

エラーコードに400, 404, 500辺りを使うと地味にややこしくなるという話。開発者は404とか500とか聞くとHTTPエラーを連想するので、全然関係ないエラーに404とか500とか出ると一瞬混乱したり戸惑ったりする。別に大した害があるわけではないけど、あえて被らせる理由もないので、桁数を4桁とかにして回避しましょう。

なお、逆の発想として、エラーコード404にNot Found的なものを割り当てたりと、積極的に被らせるという手もある。これは覚えやすいのでありかも。


以上。結構長くなったけど、業務エラー例外が使いやすいかどうかは開発効率に地味に影響するので、開発を始める前にどんな設計がいいか一度ちゃんと検討してみましょう。
あと、まだまだ悩んでいるので、もっといい設計あるよという方は教えてください…m(__)m
スポンサーサイト

Tag: PHP Java .NET Node.js

0 Comments

Leave a comment