kakudooo docs

エラー処理と設計

関数の設計、実装を行う場合にエラー処理について考慮を迫られることが多いです。 具体的には以下のようなことです。

このドキュメントでは、業務アプリケーションの開発における上記のような関心事について、自分なりの指針をまとめておきたいと思います。

エラーの体系

まずはエラーの体系について確認しておきます。エラーの体系については以下、資料の内容を参考、引用させていただきながら確認していくことにします。

エラーチェックの体系的な分類方法

エラー処理の必要性

業務アプリケーションは、主に参照系と更新系の処理に分類されます。特にユーザー入力の伴う更新系でエラー処理を考慮する必要があります。というのも、ユーザー入力起因のエラーや脆弱性が発生しやすく、また、ユーザビリティの観点からユーザーへ適切なフィードバックを返す必要があるためです。 参照系の処理でもユーザー入力を伴う機能(検索など)についても同じく、上記のような観点でエラー処理について考慮することになります。以降、エラーは入力の伴うエラー(入力エラー)のこととして扱うことにします。

エラーの分類

まず、入力エラーを体系的に分類すると、業務エラーとシステムエラーに分けられ、業務エラーは更に単体入力エラーと突き合わせエラーに分けられます。

エラーの分類①

エラーの分類②

業務エラー

ユーザ入力値の問題で、処理が完遂できない場合のエラーです。設計や実装にあたっては以下を考慮します。

単体入力エラー

ユーザ入力値「のみ」で正誤判定ができるエラーです。 さらに、フィールド単位の入力エラー(個々のフィールドのデータだけで正誤判定できるエラー)と、インスタンス単位の入力エラー(複数のフィールドを同時に確認しないと正誤判定できないエラー)とに分類することができます。 UI 層のみでチェックすることも可能です。(信頼境界の端点で再チェックするとより丁寧)

突き合わせエラー

ユーザ入力値のみでは正誤判定できず、データベース上のデータなど、他のデータとの「突き合わせ」を行わないと正誤判定ができないエラーです。 バックエンド層でチェックが必要(データの突き合わせが必要なので、バックエンドで処理することが求められる)です。

システムエラー

システムトラブルで、処理が完遂できない場合のエラーです。基本的にはランタイムエラーとなり処理が中断されます。多くのフレームワークでは例外によるエラー通知をトップレベルの層まで通過させることで、フレームワークがよしなに例外をハンドリングしてくれるため、それを活用します。 例えば、Rails だとトップレベルまで通過した例外については 500 のステータスコードでレスポンスを作成してくれます。

例外とエラー

例外について、Code Complete の説明を借りると以下のように説明できます。

エラーや例外イベントを呼び出し元のコードに渡すことがができる特別な手段である。

つまり、例外とはある関数のコードが処理を継続することができないが、どうすればよいかわからない状態になった場合に、呼び出し元の関数にその判断を委ねる手段(実装)のことです。呼び出し元のコードは例外処理用の構文(try catch など )でその例外を補足して扱いを判断することが可能です。

各言語に搭載されている例外処理機能の便利さによって、エラーを例外で表現することが一般的であることからエラー=例外という認識が一般的ではありますが、エラーを必ず例外で表現しなければないというわけであり、エラーと例外の扱いについては明確な区別がないことは前提にしつつも、インターフェースとしてのエラーと実装としての例外を区別するとよいのではないかと考えています。

例外の分類

例外とは関数のコードが処理を継続することができなくなった際に呼び出し元の関数にその処理の判断を委ねる手段であることを前述しましたが、例外をさらに技術的例外とビジネス的例外の 2 つに分類することで呼び出し元の判断をより明確にすることができます。 プログラマが知るべき 97 のことより

技術的例外

のような、技術的な問題によりプログラムの実行が中断されるような例外です。クライアント側のプログラマのミスであったり、開発者の対処すべき障害や不具合であるため、ランタイムやフレームワーク、ライブラリのトップレベルの例外処理機構に対処を委ねます。

ビジネス的例外

業務ロジックの判断によりプログラムの実行が中断されるような例外です。この例外を発生させるプログラムコードのクライアント(呼び出し元)で対処してもらいます。

エラー処理と設計

ここまで、エラーの体系とその実装方法である例外について確認してきました。これらを踏まえて、業務アプリケーション開発における関数のエラー処理の設計について指針を書いておくことにします。

自分としては、対象の関数のクライアント(呼び出し元)がアプリケーションの処理の終端である or ないに設計が依存すると考えています。対象の関数がどの層に実装されるか?、どの層から呼び出されるのか?を意識するとよいでしょう。

関数のクライアント(呼び出し元)がアプリケーションの処理の終端である

業務エラーはユーザー入力に基づく、単体入力エラーと突き合わせエラーのことでした。これらの処理はクライアント(呼び出し元)がアプリケーションにおける処理の終端である場合が多いため、エラーオブジェクトを関数の戻り値として表現し、クライアントはエラーオブジェクトについての処理をハンドリングするように設計します。 入力値にかかわらず処理自体は継続する必要があるため、契約による設計の文脈では保護型の関数と言えます。

関数のクライアント(呼び出し元)がアプリケーションの処理の終端でない

システムエラーは想定外のシステムトラブルで処理が完遂できない場合のエラーでした。これらの処理はドメインオブジェクトや技術的な関心事を扱う共通関数やインフラ層で発生することが多く、クライアント(呼び出し元)がアプリケーションの終端でない場合が多いため、エラーを例外として表現し、クライアント(呼び出し元)は

ように設計します。 異常な入力値に対して、エラーの扱いをクライアントに強制することになるため、契約による設計の文脈では要求型の関数と言えます。

参考