とあるコンサルタントのつぶやき

MCS の某コンサルタントがまったり語るテクノロジのお話です。

.NETの例外処理 Part.1

.NETの例外処理 Part.1

  • Comments 9

さて次は何を書こうかなぁと思ってましたが、前回のエントリのウケが比較的よかった様子なので、もうちょっと初心者向けのエントリを続けてみようと思ったり。ということで、今回は .NET の例外処理について書いてみたいと思います。

なぜにいまさら例外処理……? と思われる方も多いと思うのですが、理由はただ一点。現場レベルのソースコード読んでると、未だに例外処理がめちゃくちゃなアプリがホントに多いのです。

私が昔、Java を初めて触ったときに感動した機構の一つが例外処理(とそれに関連するスタックトレース)で、例外処理を正しく書くだけで、アプリの内部動作の大部分はわかるし、障害解析も圧倒的にラクになる。しかしその一方で、例外処理についてわかりやすくまとめられた書籍が少ないために、なかなか理解するのが大変なところでもあるんですよね。自分の書いた開発技術大全 vol.3 だけではどうもまだ分かりにくいようなので、少し加筆しながらここで説明しよう、と思った次第です。そんなわけで、今日は比較的初心者向けのトピックですが、気楽にお付き合いいただけると嬉しいかもです。

# といいつつ、たぶんそんなに気楽に読めるエントリでもないかもしれません。
# 例外処理は、実はアプリケーションデザインにもかかわる部分があり、例えば BC や DAC の
# メソッドの引数・戻り値の設計にも影響を与えてくるからです。なので、一度じっくり腰を据えて学習
# してみてください。一度きちんと勉強すれば済む話ですので^^。

まず、そもそも .NET アプリケーションにおける「例外」は、どのような状況を表現するために用意されている機能なのか、というところから解説していくことにします。

[.NET アプリケーションにおける例外と業務エラーの違い]

一般的に、業務アプリケーションを開発する場合には、業務フローチャートを作成し、その流れを C# や VB のコードによって実装します。

image

通常、業務処理の終了(=各メソッドの終了)は、正常終了と業務エラーのパターンに大別されますが、例外はこうした正常終了や業務エラーを表現するために利用してはいけません。例外はその名の通り、例外的な状況、すなわち業務フローチャートからはみ出してしまったという、想定外の事象を表現するために利用します。

image

具体的には、データベース接続エラーやネットワークエラー、メモリ不足などが発生した場合には、この状況を例外によって表現する、というのが .NET における大原則、と理解してください。

しかし、この例だけだと分かりにくいと思いますので、もうちょっと具体的な例を取り上げて説明してみましょう。例えば、重複ユーザ ID の利用を認めないような、新規顧客登録業務を考えてみます。

image

このような業務アプリにおいて、どのようなケースが業務エラーに相当し、どのようなケースが例外に相当するのかを考えてみます。ボタンを押したあとの応答は、以下の 3 つのパターンに分類できますが、.NET アプリケーションでは、③のパターンのみを例外とし、②のケースを例外として表現してはならない、というのがポイントになります。

image

この説明だけだとまだ業務エラーと例外の違いがわかりにくいので、もう少し突っ込んで説明をしてみます。

今、このアプリケーションで③に相当するケース、すなわちデータベース接続エラーやネットワークエラーが発生したとします。通常、これらのエラーはエンドユーザに対して通知するべき内容ではありません。というか、「データベースに接続できませんでした」、とかいわれても、エンドユーザ側としてはどないせいっちゅーねん、状態になりますよね?^^ おそらくこのような場合には、エンドユーザに対しては「ごめんなさい、またしばらくしたらアクセスしてください」「ヘルプデスクに電話してください」などのメッセージを通知するようにし、エラーの詳細情報は隠ぺいすることになるでしょう。

ところが、ID 重複エラーのようなものは、エンドユーザ側で対処のしようのある状況です。このような場合には、エンドユーザに対して適切なガイダンスメッセージを表示し、再入力などを求めることになるでしょう。

image

このように考えてみると分かりやすいと思いますが、

  • .NET における「例外」とは、エンドユーザに対して通知すべきではない、「システム的・アプリ的な異常事態」が発生した場合に利用するもの。(上図の下側のような画面を出さなければならないケースは例外、ということです。)
  • それ以外の場合、すなわちエンドユーザに対して再試行を促すようなメッセージを表示することになるケースを業務エラーと呼ぶ。業務エラーでは、例外を利用してはいけない

というのが .NET 開発における原則、ということになります。

[2 種類の業務エラー]

なお、エンドユーザに対して再試行を促すようなメッセージを表示する業務エラーというのも、大別すると以下の 2 種類にわけることができます。すなわち、

  • 生年月日テキストボックスから入力された文字列が、適切な日付でなかった場合にはエラーメッセージを表示する。(有効な日付であるか否か、また未来の日付でないか否か)
    → これは UI 部のみで完遂することが可能な入力チェックであり、単体入力チェックと呼ばれる。
    → 単体入力チェックは、UI (*.aspx)上でチェックをしてしまう。具体的には、ASP.NET 検証コントロールなどを利用してチェックを行う。
    → このため、ビジネスロジッククラス(BC)側は、単体入力チェックエラーとなっているようなデータ(例えば"2079年15月41日"などといったあり得ないデータ)を受け取ることはありません。
  • 登録しようと思った顧客 ID が、他のユーザによってすでに使われていて重複していたためにエラーメッセージを表示する。
    → これは UI 部のみでは完遂できないチェックであり、複合チェックなどと呼ばれる。(※ 正式な呼び方はたぶんないと思いますので、仮にこのように呼ぶことにしておきます。)
    → このようなデータについては、ビジネスロジッククラス(BC)が受け取り、エラーであると判定された場合には、UI 部に戻り値として通知し、エラーメッセージを表示しなければならない。
    → つまり、UI 部のみではエラー処理を完遂できません。

となります。このことからわかるように、実は「何が業務エラーなのか」は、コンポーネント種別によってもかわってきます。上記の例でいうと、ビジネスロジッククラス(BC)部では、

  • 単体入力チェックは UI 部で完遂しているはずなので、このパターンの業務エラーは考えなくてよい。
  • BC 部では、データベース上のデータとの突き合わせなどを必要とする業務エラーについてのみ考えればよい。

となります。

[具体的なシグネチャ設計例]

ではここまでの話を元に、具体的に、このアプリケーションにおいてビジネスロジッククラス(BC)のメソッドシグネチャ(入力パラメータとメソッド戻り値)をどのような形にすればよいのかを考えてみましょう。

image

ビジネスロジッククラスでは、メソッド入力値として、希望する ID や名前、メールアドレス、生年月日などを受け取って処理を行いますが、その終了パターンは、① 成功、② 顧客 ID 重複、の 2 通りあります。これらを戻り値として表現できなければなりません。そこで、enum 値(列挙型)を利用すると、以下のようにメソッド定義をすることができます。

public class CustomerBizLogic {
 
  public RegistCustomerResult ResistCustomer(string id, string name, string mail, 
                        DateTime birthday) {
    // ...
  }
 
  public enum RegistCustomerResult {
    Success,
    DuplicateCustomerIDError
  }
}

ここで、メソッド戻り値としては、データベース接続エラーやネットワークエラー、あるいはメモリ不足などのケースは全く出てこないことに気を付けてください。こうした状況が発生した場合には例外が発生し、呼び出し元に例外として通知されるため、メソッド戻り値としては表現する必要がないからです。

今度は、このビジネスロジッククラスを利用する UI 部(*.aspx ファイル)の実装がどのようになるのかを考えてみます。業務エラーとは、エンドユーザに適切に通知する必要のあるエラーケースなので、正しくケース分類して、後処理を書かなければなりません。よって、UI 部のコードは次のようになります。

protected void btnRegist_Click(object sender, EventArgs e) {
    if (Page.IsValid == false) return;
    CustomerBizLogic biz = new CustomerBizLogic();
    CustomerBizLogic.RegistCustomerResult result = biz.ResistCustomer(tbxId.Text, 
        tbxName.Text, tbxMail.Text, DateTime.Parse(tbxBirthday.Text));
    switch (result) {
        case CustomerBizLogic.RegistCustomerResult.Success:
            lblResult.Text = "正しく顧客登録を行いました。";
            break;
        case CustomerBizLogic.RegistCustomerResult.DuplicateCustomerIDError:
            lblResult.Text = "指定された ID はすでに利用されています。";
            break;
    }
}
 

基本的に UI 部では、BC からのメソッド戻り値を switch 文によって寄り分けて、その後の処理を書く、と考えると分かりやすいでしょう。(=try-catch などが出てくることはない、ということになります。詳しくは後述。)

[業務エラーが複雑なパターンとなる場合]

なお、上述の例では業務エラーが比較的単純なパターンのみを想定したため、メソッド戻り値としては「どの業務エラーになったのか」だけを表現すれば十分(=enum 型を使えば十分)でした。しかし、場合によってはサーバ側から、各業務エラーごとに異なる付帯情報を返したいこともあります。(例えば、顧客 ID が重複した場合には、サーバ側からおすすめ ID 情報をくっつけて返す、など) このような場合には、メソッド戻り値として構造体を利用するようなこともあります。

ちなみに、ビジネスロジッククラスのメソッド戻り値を使った業務エラーの表現方法は、おおまかに以下の 3 パターンにわけることができます。

  • null型
  • enum型
  • 構造体クラス型

もう少し具体的に書くと、以下の通りです。

① null 型

正常終了の場合には、通常の戻り値(オブジェクトインスタンス)を返し、業務エラーの場合には、null値を返す、という方式。

public AuthorsDataSet GetDataByAuId(string au_id);
 → 正常終了:オブジェクトインスタンスを返す
 → 業務エラー(著者IDが見当たらない):nullを返す
 
public decimal? GetPriceByTitleId(string title_id);
 → 正常終了:価格データを返す
 → 業務エラー(書籍IDが見当たらない):nullを返す

※ この方式、個人的にはあんまり好きではありません。というのも、"null" の意味付けが一定しなくなり、後からこのメソッドを読んだときに、null が何を意味するのかが曖昧になりやすいからです。

※ また、上記の例では、著者 ID が見つからなかった場合には、null を返す方式と、中身の行数がゼロ行の AuthorsDataSet インスタンスを返す方式が考えられます。実装上は後者の方がラク。どちらを使うにせよ、どの方式を採用しているのかは明確に仕様として管理しておかないと、実装ブレや後からのメンテナンス時のトラブルの元になりますので要注意。

② enum 型

成功/失敗などの情報のみを返せばよい場合には、各ケースを列挙型(enum)で表現して返します。(業務エラーのパターンが一つしかない場合には、bool型を利用しても OK。)

public OrderResult OrderBook(string customerId, …);
 (OrderResultは列挙型)
 → 正常終了:OrderResult.Successを返す
 → 業務エラー(書籍の在庫がない):
      OrderResult.NoStockを返す
 → 業務エラー(納品が間に合わない):
      OrderResult.CannotDeliveryUntilRequiredDate
 → …

個人的には、コードの可読性が高まるので非常に好きな方式。呼び出し元でも switch 構文で後処理がきれいに書けるというメリットがあります。

なお、絶対にやってはいけないのは、数値コードを使って戻り値を表現する方法。例えば上記の例の場合、int 型を戻り値にしておいて、0 だったら正常終了、1 だったら在庫不足、2 だったら納期が間に合わない、などとするのは NG。これをしてしまうと、数値コード対応表がないと、アプリケーションプログラムの意味がわからなくなってしまうためです。アプリケーションプログラミングの大原則の一つに「Self-Descriptive(自己記述的)」(アプリケーションコード自体が、その意味を表現できていなければならない)という原則がありますが、その原則を守るためには、数値コードは使うべきではありません。

③ 構造体クラス型

正常終了/業務エラーなどの各ケースにおいて何かしらの付随情報を返す必要がある場合には、付随情報などをラッピングしたクラスを作成し、これを用いて戻り値を表現します。

public UpdateResult UpdateAuthorInfo(AuthorsDataSet ads)
{
    ... 
}
 
[Serializable]
public class UpdateResult
{
    public bool IsSuccess;            // 正常終了 or 業務エラー
    public UpdateErrorResultEnum BizError;    // 業務エラーの理由
    public int NewestCacheVersionNumber;        // 業務エラー①の付帯情報
    public AuthorsDataSet NewestInfoOnServer;    // 業務エラー③の付帯情報
}
 
public enum UpdateErrorResultEnum
{
    StateDataIsStale,
    DuplicatePhoneNumber,
    OptimisticCurrencyViolation
}

[例外の処理方法]

さて、ここまで業務エラーの設計方法と取り扱い方について説明してきました。ポイントとしては、業務エラーはメソッドシグネチャの一部として設計しなければならない、ということでしたが、では例外についてばとのように扱えばよいのでしょうか?

まず、大原則を覚えてください。

よほどのことがない限り、アプリケーションで try-catch を書いてはいけません。

もう一度繰り返します。

よほどのことがない限り、アプリケーションで try-catch を書いてはいけません。 

もう一度(ry、すみませんしつこいですね^^。でもこれ、めちゃめちゃ重要なのです。

C# や VB を学習すると、割と早い段階で try-catch 構文による例外処理方法を学習すると思います。が、実際のアプリケーションコードでは、特殊な事情(後で説明します)がない限りは、try-catch を記述してはいけないのです。では try-catch を書かずにどうやって例外の後処理をすればよいのか....というと、通常、集約例外ハンドラと呼ばれる機能を利用します。

まず、例外処理(try-catch)や集約例外ハンドラを一切記述しなかった場合にどのような挙動をするのかを考えてみます。例えば、UI / ビジネスロジッククラス(BC) / データアクセスクラス(DAC)の論理 3 階層型構造で作成したアプリケーションを取り上げてみましょう。

image

この場合、業務エラーと例外は、次のように処理されます。

  • 業務エラーが発生した場合
    この場合は、例外を使わず、戻り値を使って上位コンポーネントに業務エラー発生を通知します。そして、Web ページではこの戻り値をガイダンスメッセージとして画面に表示し、ユーザに通知します。
  • 例外が発生した場合
    この場合、例外はどんどん上位の呼び出し元に通知されていきます。基本的に try-catch は記述してはならないので、例外は Web ページまで通知されますが、ここでも try-catch されていないため、例外は最終的には、実行ランタイムである ASP.NET ランタイムが捕捉します。すると、ASP.NET は、下図のような黄色い画面を表示���ます。

image

ちなみにランタイムが捕捉した場合の挙動は、アプリケーション種別ごとに異なります。

  • ASP.NET ランタイムの場合 → 上図のような黄色い画面を出す。
  • Windows フォームやコンソールアプリケーションの場合 → 下図のようなダイアログを出す。

image

このような画面は、開発時にはデバッグがすぐにできるので便利でしょうが、運用時には不適切です(運用時には、「しばらく時間をおいてやりなおしてください」とか「ヘルプデスクに電話してください」といったメッセージを表示する必要があるため)。このため、ASP.NET ランタイムや Windows フォームなどでは、これらの画面を差し替えるための機能を備えています。詳細はまた後で詳しく解説しますので、ここでは概要だけ説明しますと。

① ASP.NET ランタイムの場合

未処理例外が発生した場合には、global.asax ファイル内の Application_Error() メソッドが自動で呼び出されます。また、web.config ファイルの <customErrors> セクションを利用すると、エラー時に表示する画面を差し替えることができます。

<configuration>
  <system.web>
    <customErrors defaultRedirect="error.htm" mode="RemoteOnly" />
  </system.web>
</configuration>

② Windows フォームの場合

未処理例外が発生した場合には、 Application.ThreadException イベントハンドラが自動で呼び出されます。よって、ここにエラーメッセージ通知を行うコードを記述しておくと good。最も簡単なサンプルコードを示すと以下のようになります。

namespace WindowsFormsApplication1
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
 
        static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
        {
            MessageBox.Show("致命的な例外が発生しました。\nヘルプデスクに電話してください。");
            Application.Exit();
        }
    }
}

③ コンソールアプリケーションの場合

未処理例外が発生した場合には、AppDomain.CurrentDomain.UnhandledException イベントハンドラが自動で呼び出されます。よって、ここにエラーメッセージ通知を行うコードを書きます。

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
        }
 
        static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            MessageBox.Show("致命的な例外が発生しました。\nヘルプデスクに電話してください。");
            Environment.Exit(-1);
        }
    }
}

よって、これらの処理をカスタマイズしておけば、異常事態に対する後処理を個別に作り込む必要はない、ということになります。簡単にいえば、try-catch による例外の後処理を個別に作り込む必要性はない、ということです。

# 実際、私もいくつかの開発現場で、アプリケーションコード中に書かれた try-catch 文を片っぱしから除去してもらったことがあります。それぐらい、初学者は「なんとなく try-catch」を書いてしまうことが多いんですよね。この点は初学者が陥りがちな典型的なミスの一つなので注意してください。

[業務フローチャートと throw / try-catch の関係]

さて、ここまで try-catch 文は書くな、と解説してきたのですが、実際の業務アプリケーションの場合には、try-catch 文や throw 文を記述することもあります。どのような場合にこれらを記述する必要があるのかというと、それは業務フローチャートの流れの調整を行いたい場合です。もう一度、最初の方に掲載した図を再掲します。

image

この絵からわかるように、例外とは業務フローチャートの想定から外れた場合を表現する目的で利用します。が、場合によっては以下のようなケースもあるはずです。

  • 間違って業務フローチャートから出ちゃったんだけど、元に戻って処理を続けたい。
  • 自ら業務フローチャートの外に出て、自爆したい。

このような場合には、try-catch 命令、あるいは throw 命令を利用します。

image

上記の説明だけだとわかりにくいと思いますので、具体例を取り上げてみましょう。

① try-catch 命令を利用して、業務フローチャートに戻るケース

例えば、前述の顧客新規登録業務における、ビジネスロジッククラス(BC)の実装を考えてみましょう。Visual Studio 2008 の開発では、データアクセスクラス(DAC)の実装にテーブルアダプタを利用しますが、テーブルアダプタを介して INSERT 命令を発行すると、重複顧客がいる場合には、INSERT 処理が PK 制約違反により失敗します。

image

このとき、DAC から BC へは INSERT 命令失敗により、SqlException 例外が通知されますが、このケースは BC の立場からすると、例外として取り扱うべき事象ではなく、業務エラーとして取り扱うべき事象です。このため、BC 上では、try-catch 命令によりこの例外を捕捉し、通常の戻り値に変換して UI に返すことになります。

image

※ 以下のコードにはまだ問題があるので、続きの説明を読んでください。
 
public RegistCustomerResult ResistCustomer(string id, string name, string mail, 
                        DateTime birthday)
{
    // テーブルアダプタを利用
    CustomersTableAdapter ta = new CustomersTableAdapter();
    try
    {
        // INSERT 命令を実施
        ta.InsertCustomer(id, name, mail, birthday);
    }
    catch (SqlException sqle)
    {
        return RegistCustomerResult.DuplicateCustomerIDError;
    }
    return RegistCustomerResult.Success;
}
 

しかし、ここで留意しなければならないことがあります。それは、SqlException 例外は、PK 制約違反以外のケースでも起こりうる、ということです。下表は、SQL Server 内部で起こる代表的なエラーと、それに対する例外の発生有無の概要表なのですが、これからわかるように、SqlException 例外は、ディスク枯渇やテーブルデータ破損といった、致命的なトラブルによっても発生します。このような事象は業務エラーでは当然ありませんが、上記のコードではすべて「重複顧客登録業務エラー」として取り扱われてしまいます。(=本来業務エラーとすべきではない SqlException まで業務エラーに変換してしまっている)

image

このような場合に役立つのが、catch ブロック内に記述する「throw」(引数なし)という命令です。この命令を使うと、catch したあとでありながら、catch しなかったことにできます

public RegistCustomerResult ResistCustomer(string id, string name, string mail, 
                        DateTime birthday)
{
    // テーブルアダプタを利用
    CustomersTableAdapter ta = new CustomersTableAdapter();
    try
    {
        // INSERT 命令を実施
        ta.InsertCustomer(id, name, mail, birthday);
    }
    catch (SqlException sqle)
    {
        if (sqle.Number == 2627) { // PK 制約違反だった場合には後処理を行う
            return RegistCustomerResult.DuplicateCustomerIDError;
        }
        else {
            throw; // それ以外だった場合には catch しなかったことにする
        }
    }
    return RegistCustomerResult.Success;
}
 

以上の話からわかるように、try-catch 命令を書く場合には、業務エラーなどに変換したくない例外を、間違って捕捉しないようにしなければなりません。具体的には、以下のようなコードは絶対に書いてはいけません

  • 複数行のコードをまとめて try-catch で囲む。
  • 一般例外(Exception クラス)を catch する。
  • catch したあと何もしない。
static void Main(string[] args)
{
    try // 間違い① 複数行を try-catch で一気に囲む
    {
        int a = 0;
        int b = 0;
        int c = a + b;
        Console.WriteLine(c);
        // ...
    }
    catch (Exception e) // 間違い② あらゆる例外を捕捉してしまう
    {
        // 間違い③ 補足したあと何もしない
    }
}

このようなコードを書いてはいけない理由は、try-catch する目的を考えてみていただければ明らかでしょう。

  • 複数行のコードをまとめて try-catch で囲む。
    → 捕捉して業務エラーに変更したい例外以外の例外が、別の行から発生し、それを捕捉してしまう恐れがある。
  • 一般例外(Exception クラス)を catch する。
    → 捕捉したい例外以外の例外をも捕捉してしまう恐れがある。
  • catch したあと何もしない。
    → 例外を「なかったこと」にしてしまう。

よって、例外を try-catch する場合には、以下の大原則を守らなければなりません。

  • try-catch ブロックは、例外が発生しうる『1 行』のみを囲む。
  • 一般例外(Exception クラス)ではなく、特定の例外(SqlException など)のみを捕捉する。
  • catch した後には、必ず後処理(業務エラーへの変換など)を記述する。

※ リソースの確実な解放のために記述する try-finally の記述ではこれらに反するコードを記述することがありますが、これはそもそも目的が異なるためです。これについてはまた別の機会に。

② throw 命令を使って自爆するケース

では次に、throw 命令について説明します。throw 命令とは、アプリケーション内部から例外を発生させる処理ですが、これは自ら業務フローチャートの外に出て、アプリケーションを止めようとする自爆処理であると言えます。

例えば以下のような例を考えてみます。先ほどの新規顧客登録業務において、ビジネスロジッククラスの部分で、UI (*.aspx)から引き渡された電子メールアドレスの文字列が、○○@○○.○○ といった電子メールアドレスのフォーマットになっていなかった場合を考えてみます。

image

通常、ブラウザから入力される電子メールアドレスは、まず UI (*.aspx)上に貼り付けられた検証コントロールによってチェックします。このため、アプリケーションが適切に実装されている限り、ビジネスロジッククラス(BC)が、フォーマットのおかしい電子メールアドレスを UI 部から受け取ることはあり得ません。もしそのような事態が発生したとすると、それは以下のような状況が想定されます。

  • UI 実装者が作業ミスあるいは作業を怠って、検証コントロールを正しく使わなかった。
  • アプリケーションになんらかの脆弱性があり、悪意のあるユーザがこれを使って、無理やり不正なパラメータを投入してきた。

これらはいずれもヤバい状況ですが、このようなヤバい状況であるにもかかわらずそのままアプリケーション処理を続行すると、どんな事態が発生するのか分からず、極めて危険です。このように、アプリケーション内部で異常事態であることが判明した場合には、throw 命令を使うことにより、アプリケーションを停止させます

例えば、先のこちらのコードに対して、「異常事態の判定コード」を追加してみましょう。

public RegistCustomerResult ResistCustomer(string id, string name, string mail, 
                        DateTime birthday)
{
    // テーブルアダプタを利用
    CustomersTableAdapter ta = new CustomersTableAdapter();
    try
    {
        // INSERT 命令を実施
        ta.InsertCustomer(id, name, mail, birthday);
    }
    catch (SqlException sqle)
    {
        if (sqle.Number == 2627) { // PK 制約違反だった場合には後処理を行う
            return RegistCustomerResult.DuplicateCustomerIDError;
        }
        else {
            throw; // それ以外だった場合には catch しなかったことにする
        }
    }
    return RegistCustomerResult.Success;
}
 

このコードの場合には、以下のような「異常事態」の検知コードを追加することができます。

  • BC 部で入力値に単体入力エラーが発見された場合には、明らかにおかしい。
  • INSERT 処理を行ったはずなのに更新結果行数が 0 行の場合には、明らかにおかしい。

よって、このような異常事態チェックコードを前出のコードに追加すると以下のようになります。

   1: public RegistCustomerResult ResistCustomer(string id, string name, string mail, 
   2:                         DateTime birthday)
   3: {
   4:     if (Regex.IsMatch(id, "^[A-Z]{2}[0-9]{4}$") == false) throw new ArgumentException("id");
   5:     if (Regex.IsMatch(name, "^[\u0020-\u007e]{1,20}$" == false) throw new ArgumentException("name");
   6:     if (birthday >= DateTime.Now) throw new ArgumentException("birthday");
   7:  
   8:     // テーブルアダプタを利用
   9:     CustomersTableAdapter ta = new CustomersTableAdapter();
  10:     try
  11:     {
  12:         // INSERT 命令を実施
  13:         int affectedRows = ta.InsertCustomer(id, name, mail, birthday);
  14:         if (affectedRows != 1) throw new ApplicationException("INSERT 処理に失敗しました。");
  15:     }
  16:     catch (SqlException sqle)
  17:     {
  18:         if (sqle.Number == 2627) {
  19:             return RegistCustomerResult.DuplicateCustomerIDError;
  20:         }
  21:         else {
  22:             throw;
  23:         }
  24:     }
  25:     return RegistCustomerResult.Success;
  26: }
  27:  

このような異常事態検知コードについては、以下の点に注意してください。

  • このような異常状態検知コードがなくても、アプリケーションは正しく動作する。
    そもそもこのような事態は「起こってはいけない」「起こるはずのない」ものです。このため、これらのコードはなくても本来は問題ありません。ですが、これを入れておくことにより、「万が一」の事態に備えることができる、ということになります。
  • このような異常事態検知コードをどこまで書くかは、ケースバイケースで考える。
    上記では、単体入力エラーや、更新結果行数チェックコードを追加しましたが、このような異常事態は「考えれば考えるほど」もっとたくさん出てきてしまいます。が、あまりやりすぎても作業効率が悪くなるばかり、ですので、現実にはどこかで線引きする必要があります。通常は、単体入力エラーに関する異常状態チェック、DB 更新といった重要処理に関する異常状態チェックのみ行えば十分でしょう。
  • 異常事態が検知されたときに throw する例外は、極端にいえば何でもよい。
    異常事態発生時に throw する例外を何にするか、ということに関しては様々な議論があり、ApplicationException にすべきとか、その派生クラスを定義して throw すべきとか、いろんなことが書かれています。が、ぶっちゃけトークをすれば & 極端なことを言えばどの例外クラスを使っても OK。というのは、ここで throw した例外は、誰かが try-catch するわけではなく、ランタイムに拾わせてアプリケーションを停止させる目的で使うものだからです。とはいえ、あまりにも実態と異なる例外クラスを使うのも不適切なので、通常は ArgumentException 例外(入力パラメータに不正が見つかった場合)や ApplicationException 例外(アプリケーションの一般例外)を使います。(ApplicationException 例外を使うな、という資料も結構見かけるのですが、実際問題としては、上記のような理由により、そんなに神経質になる必要性はないです。)
  • 異常事態が検知されたときに throw する例外には、詳細な情報を入れておく。
    ここで発生された例外は、最終的に集約例外ハンドラにひっかかり、イベントログなどにロギングされます。この情報は、事後的な障害解析に使われるため、ここにどれだけの詳細情報を含めておけるのかが実は非常に重要になります。例外ログの読み方は今後解説していきますが、例えば、どういった理由・どういった実際の値でトラブルが発生したのかの情報を、例外内部に情報として含めておくとよいです。
  • これらの例外発生コードは、カバレッジ上、通すことが難しいコードになる。
    単体機能テストや結合機能テストを行う際、よく「コードカバレッジを 100% にしろ」といった無茶なことが言われますが、ここに書いたような異常事態検知コードというのは、テスト上、非常に通しにくいパスになります。しかしコードカバレッジを 100% にするために、これらの通しにくいパスを無理矢理通したり、あるいは(削っても正常なときには支障がないからという理由で)これらのコードを削ってしまう、というのは、あまり賢いアプローチとはいえません。通常、こうした異常事態検知コードの妥当性は、目視によるコードレビューによって判断し、単体機能テストや結合機能テストでコードカバレッジ 100% を目指す、という非効率的なアプローチはしない方が適切です。

[まとめ]

というわけで例外処理についてひたすら解説してきましたが、ここまでのキーポイントをまとめると以下のようになります。

  • 例外は業務エラーには利用せず、異常事態(アプリケーション/システムエラー)の表現にのみ利用する。
  • 例外が発生した際の後処理は、ランタイムの後処理機能に任せるようにし、基本的には try-catch を書かない。
  • try-catch や throw 命令は、フローチャートの流れを調整したい場合に利用する。

image

上記 3 つの原則を守るだけで、アプリケーションの障害解析は圧倒的にラクになるので、まずはこれらの原則をしっかり守ったコーディングを行ってください。次回は引き続き、ASP.NET ランタイムの持つ 3 つの例外処理機能の使い方などを解説していきます。(と思いましたが、ちょっと回り道するかもしれません....。)

  • .NETはまるっきり素人(というか、今も使っていない)なのですが、興味深く読ませていただきました。

    「私だったら…」って思ったのが一点。

    「業務エラーはメソッドシグネチャの一部として設計しなければならない」ということでしたが、私なら、業務エラー用のExceptionを定義して、それをthrowして上位に拾わせることでそのあとの処理を分岐させるように作るかなと思いました。

    そうすると、その後の「よほどのことがない限り、アプリケーションで try-catch を書いてはいけません。」にもあてはまらなくなっちゃいますが。

    ご意見を頂けると嬉しいです。

  • コメント書いた後で読み返してみて「BCのところでそれをやるのか」と気づきました。そうすれば、UIは同じことになりますね。

    失礼しました。

  • > y_ko_no さん

    > 「業務エラーはメソッドシグネチャの一部として設計しなければならない」

    > ということでしたが、私なら、業務エラー用のExceptionを定義して、それを

    > throwして上位に拾わせることでそのあとの処理を分岐させるように作るかなと

    > 思いました。

    はい、そういう考え方があることも理解していますが、この点については一つ

    重要なポイントがあります。いずれ書くつもりでいるのですが、先回りして

    キーポイントだけ書くと、次のようになります。

    まず、業務エラー用の Exception というのは、Java の言語構文には存在するの

    ですが、C# や VB などの CLR 系言語には存在しないものです。

    Java では、例外が二種類あり、検査例外(Checked Exception)と、実行時例外

    (Runtime Exception)というのがあるのですが、CLR 系には Java の検査例外の

    仕組みが存在しません。

    Java の検査例外には、以下の 2 つの特徴があります。

    ・メソッドシグネチャとして throws 句を書かないとダメ。

    ・呼び出し側で try-catch を書かないとダメ。

    この 2 つの特徴は、要するに「必ず処理ルートとして考慮しなくちゃいけない

    ケース」ということを意味しています。端的に言えば、業務エラーを表現する

    機能として、検査例外というものが用意されている、ということなんですよね。

    ところが CLR 系言語にはこの検査例外の仕組みが備わっていません。

    Java の実行時例外や CLR の例外は、「throws 書かなくてOK」、「呼び出し側で

    try-catch 書かなくて OK」、つまり端的に言えば「インタフェース約款として

    規約しない例外」ということになるのですね。

    業務エラーというのは、インタフェース約款として規定すべきもの。

    だから、Java の場合には検査例外として、CLR の場合には(検査例外に相当する

    仕組みがないので)戻り値の一部として定義する、ということになります。

    ちなみにこの点に関して言うと、私は Java の言語仕様の方が好きです。

    検査例外を使って業務エラーを表現した方が、コードとして業務エラーを綺麗に

    表現できるからです。しかし、一方で検査例外と業務エラーの関係を正しく理解

    していない Java プログラマも非常に多いのが実情で、Java 自体の例外設計にも

    この点に関する混乱が見られるほどです。(有名どころで言うと、EJB 1.0 では

    EJB の例外が検査例外である RemoteException で定義されていたのですが、

    これが 1.1 からは EJBException という実行時例外に変更され��、とか。)

    Java の世界でも、検査例外を言語構文として作ったことは失敗だったんじゃ?

    という議論があると聞いたことがあるのですが、正しく使わないと、検査例外は

    かえって危険なのは確かです。実際、Java の場合は SqlException や Remote

    Exception などがかたっぱしから検査例外として定義されているのですが、

    検査例外→実行時例外への変換などを意識できていないプログラマがコードを

    書くと、結構とんでもないことになります。

    この辺も、いずれちゃんとエントリとしてまとめるつもりなので、よかったら

    また覗きにきてみてください。

  • はじめまして、よこけんと申します。

    業務エラーに関する箇所についてコメントさせて頂きます。

    「業務エラーは戻り値で表現する」とのことですが、他にも例外を使わない表現方法はたくさんあると思います。

    戻り値ではなくプロパティ (例えば、発生した業務エラーを格納する Errors プロパティ) で表現することもできますし、事前検証用のメソッドを用意しておくこともできます。業務エラーを UI 操作と直結させることもできます。

    どのようにするかは、アーキテクチャやレイヤによって異なると思います。

    戻り値に限定している理由は何かあるのでしょうか?

    ここからは特に下位レイヤの話なのですが、処理結果を戻り値で返す場合、後から列挙値が追加されたら呼び出し元全てに修正を入れなければならず、修正漏れが発生する可能性があります。

    「例外を投げる + 事前検証メソッドの提供」という形態ならば、呼び出し元の修正漏れの検出が容易になります。事前検証できるので、こちらも業務エラーに例外を使わずに済むと思います。(この場合は、例外には InvalidOperationException 系が妥当ですね。)

    前述の通りアーキテクチャにもよりますが、下位のレイヤは上位から処理を委譲されてくるので、こちらの形態の方が良い場合もあると思います。

    ID の重複などは実行時にしか (厳密な) 検出ができませんが、同様の理由で、戻り値ではなく例外を使うのもアリだと思います。(この場合は InvalidOperationException のような汎用な例外を投げるのではなく、具体的な例外を投げますが。)

    # これも下位のレイヤの話です。上位では戻り値や Errors プロパティに変換できます。

    下位レイヤで例外を使っても、上位レイヤで「業務エラーというのは、インタフェース約款として規定すべきもの」を満たすことができるので、下位レイヤでは ID の重複を例外で通知しても何ら問題ないことになる (※) と思います。

    # これまたアーキテクチャ次第ではあると思いますが。

    ※ 検査例外は必須ではなくなり、例外は C# や VB の XML ドキュメントコメントで明示すれば充分となります。

    文章がわかりづらかったらすみません。要は「.NET では業務エラーは戻り値」、「上位レイヤも下位レイヤも同様」 (こちらは明記されてはいませんが) という二点についての意見です。

  • よこけんさん、コメントありがとうございます。

    > どのようにするかは、アーキテクチャやレイヤによって異なると思います。

    > 戻り値に限定している理由は何かあるのでしょうか?

    はい、アーキテクチャ次第なので、この通り「でなければならない」という類のものではないと思います。「絶対に」という表現を使ってはいますが、これはその方が(大半の人には)分かりやすいと思ったからで、よこけんさんぐらい深く考察されている方であれば、例外やプロパティ表現を使っても構わないと思います。

    この辺については次回のエントリにきっちり書くつもりでいるのですが、私が業務エラーに例外を使うべきではない、としている大きな理由は以下の 2 つです。

    ① 業務エラーは、メソッドシグネチャ(インタフェース規約)の一部として表現すべき。

    ② Java の検査例外のように、メソッドシグネチャの一部として例外を表現したり取り扱ったりする機能が、.NET にはない。

    問題は②の点で、②を例外(実行時例外)に倒すか戻り値に倒すかは、これはアーキテクチャ次第です。私は、①の点に加えて、

    ・業務フローチャートとアプリケーションコードは 1:1 に対応しているべき。(特に BC)

    ・業務フローチャートの関心領域は、明示的にシグネチャとして表現されるべき。

    という考え方を持っており、このために、シグネチャ表現できない例外を使うことを嫌っています。

    ただ、いくつかツッコミを入れると、

    > 発生した業務エラーを格納する Errors プロパティ

    これは、BC はステートレスであるべきというコンセプトから避けています。(UI 部のダイアログの設計とかではアリかもしれませんが。)

    > ここからは特に下位レイヤの話なのですが、処理結果を戻り値で返す場合、後から列挙値が追加されたら呼び出し元全てに修正を入れなければならず、修正漏れが発生する可能性があります。

    こちらは上位レイヤの実装コード次第だと思います。まず、下位レイヤの I/F 仕様が変更された場合には上位レイヤは絶対に見直しが必要です(I/F 約款がくずれたことを意味するので)。そして(修正漏れを避けるためにというわけではないのですが)、上位レイヤでは、予め下位レイヤが想定外の列挙値を返してくるようなら自爆コードを書かなければなりません。

    protected void btnRegist_Click(object sender, EventArgs e) {

       if (Page.IsValid == false) return;

       CustomerBizLogic biz = new CustomerBizLogic();

       CustomerBizLogic.RegistCustomerResult result = biz.ResistCustomer(tbxId.Text,

    tbxName.Text, tbxMail.Text, DateTime.Parse(tbxBirthday.Text));

       switch (result) {

           case CustomerBizLogic.RegistCustomerResult.Success:

               lblResult.Text = "正しく顧客登録を行いました。";

               break;

           case CustomerBizLogic.RegistCustomerResult.DuplicateCustomerIDError:

               lblResult.Text = "指定された ID はすでに利用されています。";

               break;

           default:

               throw ApplicationException("想定外の結果が返ってきました。" + result.ToString());

       }

    }

    # というか、こんなめんどい話になるのは端的にいえば「.NET に検査例外がないから」なのですが;。

    # 検査例外があればこんなことしなくてもコンパイルエラーになりますからね....

    検査例外が万能とは言いませんが(これは次回のエントリにて)、検査例外相当のケースをどのように取り扱うべきかは設計ポリシー(アーキテクチャ)次第です。私は、比較的汎用性が高く、一貫性を取りやすいアーキテクチャを好むので上記のような指針を使っていますが、よこけんさんのように適切な理由があれば、他の設計指針を使ってもよいと思っています。

  • よこけんさんの Web サイトの方、今、拝見させていただきました。なかなか熱い議論があるようで^^。ちょっと追加で捕捉させてください。

    私のエントリでは話を単純化する目的で書いていないのですが、業務エラーと例外に関するもう一つ重要なトピックとして、「何を業務エラーと見なすのか」(何をフローチャート上の想定ケースとするのか)というポイントがあります。

    例えば、(ちょっと TryParse は忘れるという前提で)int.Parse() メソッドを考えてみると、

    ・「入力文字列が不正フォーマットである」という事態は十二分に考えられる。だから「考えられるかどうか」という意味でいえば十分に想定範囲内。また、多くの場合には業務エラー的に扱う必要がある。

    ・けれども、いつでもそうとは限らない。確実に数字であることが保障されている文字をパースしたい場合、むしろ「入力文字列が不正フォーマットである」ケースを業務エラーとしてライブラリ設計されるとうっとおしい。

    という状況があり、結果として、

    ・「入力文字列が不正フォーマットである」という事態は例外として扱う。

    ・もしこれが業務エラーに相当するのであれば、try-catch によるフロー修正を行う。

    という形になっているのだと私は理解しています。(実際、.NET の BCL はそうやって設計されているので、業務アプリの実装がスマートになる)

    実際、BCL みたいなものは、利用者側(=アプリケーション側)の都合によって、何が業務エラーで何がシステムエラーなのかが大きく変わるために、BCL 側の適当な判断で「これは業務エラー『のはず』」とか設計されると、ものすごく面倒になることがしばしばあります。(この典型例が Java の検査例外の SQLException と RemoteException) だから、BCL みたいなものは、「とりあえずことごとくシステムエラー扱いにして API 設計する」としてもらった方が、利用者側からすると便利になるんですよね。

    でも、これと同じ議論は業務アプリには当てはまらないと思います。例えば業務アプリの UI, BC, DAC を考えてみると、

    ・UI, BC, DAC の実装者は非常に近いところにいる。

    ・BCL と違い、I/F 設計の汎用性を考える必要性が薄い。

    ・むしろメソッド約款を明確化し、かちっと作る方がよい。(=呼び出し側の都合で、あるケースが業務エラーかシステムエラーかの判断がぶれるという事態は少ないし、それは業務アプリ設計のブレを意味する)

    ということがあります。なので、BCL と同様の議論に基づいて「業務エラー表現に『とりあえず』例外を使っておき、ハンドルするか否か(=そのケースを業務エラーとみなすかシステムエラーとみなすのか)は呼び出し元に任せる」という考え方は当てはまらないと考えます。

    となると本質的な問題は、「業務エラーケース(=呼び出し元との間で「約款」として定められるべきケース)をどうやって表現するのが最も適切なのか?」 という議論であると思います。が、結局のところ、その議論に対する万人が納得しうる正解は「Java の検査例外に相当する仕組みがあること」なので、この話って誰もが納得する一意の答えは得られないのではないか、と……残念ながら;。

  • ご返答ありがとうございます。

    > はい、アーキテクチャ次第なので、この通り「でなければならない」という類のものではないと思います。

    > 「絶対に」という表現を使ってはいますが、これはその方が(大半の人には)分かりやすいと思ったからで、

    なるほど、そういうことでしたか、納得です。

    > > 発生した業務エラーを格納する Errors プロパティ

    > これは、BC はステートレスであるべきというコンセプトから避けています。

    こんな感じでインスタンスを毎回生成しても NG ですか?

    protected void btnRegist_Click(object sender, EventArgs e) {

     if (Page.IsValid == false) return;

     CustomerRegisterLogic biz = new CustomerRegisterLogic();

     biz.Id = tbxId.Text;

     biz.Name = tbxName.Text;

     biz.Mail = tbxMail.Text;

     biz.Birthday = DateTime.Parse(tbxBirthday.Text);

     biz.Execute();

     switch (biz.Error) {

       case CustomerRegisterLogicErrors.None:

         lblResult.Text = "正しく顧客登録を行いました。";

         break;

       case CustomerRegisterLogicError.DuplicateCustomerIDError:

         lblResult.Text = "指定された ID はすでに利用されています。";

         break;

       default:

         throw ApplicationException("想定外の結果が返ってきました。" + biz.Error.ToString());

     }

    }

    > 上位レイヤでは、予め下位レイヤが想定外の列挙値を返してくるようなら自爆コードを書かなければなりません。

    確かにその通りなんですが、使う側にその責任が移動してしまうことにちょっとだけ抵抗があるんですよね…。(無視できるレベルですけど^^;)

    > よこけんさんの Web サイトの方、今、拝見させていただきました。なかなか熱い議論があるようで^^。

    わざわざブログをご覧になって下さったとは、ありがとうございます。

    以前は「業務エラーは例外で表現すべき」と考えていたのですが (http://csharper.blog57.fc2.com/blog-entry-99.html)、最近は違う考え (http://csharper.blog57.fc2.com/blog-entry-236.html) になって、更に今回のとあるコンサルタントさん (名前は伏せておいた方がいいんですよね?^^;) とのやり取りでまた色々学んだこと・考えさせられることがあり…という具合です (苦笑)

    > でも、これと同じ議論は業務アプリには当てはまらないと思います。例えば業務アプリの UI, BC, DAC を考えてみると、

    > ・UI, BC, DAC の実装者は非常に近いところにいる。

    > ・BCL と違い、I/F 設計の汎用性を考える必要性が薄い。

    > ・むしろメソッド約款を明確化し、かちっと作る方がよい。(=呼び出し側の都合で、あるケースが業務エラーかシステムエラーかの判断がぶれるという事態は少ないし、それは業務アプリ設計のブレを意味する)

    > ということがあります。なので、BCL と同様の議論に基づいて「業務エラー表現に『とりあえず』例外を使っておき、ハンドルするか否か(=そのケースを業務エラーとみなすかシステムエラーとみなすのか)は呼び出し元に任せる」という考え方は当てはまらないと考えます。

    確かに汎用性はあまり考えなくても良いとは思いますが、今まで業務エラーとして扱っていなかったものを後から業務エラーとして扱うことになった時に、全体の見直しが必要となってしまうのは一つのデメリットかなと思います。(もちろん、「だからダメ」なんて主張をする気は毛頭ありません。その時の状況に応じてメリットとデメリットを天秤にかける、ですね。)

    ちなみに、僕が好んでいるアーキテクチャでは、アプリケーションとしての動作を上位レイヤで担当し、下位レイヤではほとんど意識しないようにしています。この場合、下位レイヤが (アプリケーションの動作の一種である) 業務エラーを意識する必要がありません。

    僕は、例外は本来、「メソッドが本来の目的を果たせなかったことを通知するためのもの」だと思っています。下位レイヤではこの思想に純粋に従うだけです。事前検証が可能ならば事前検証メソッド (や状態プロパティなんか) を用意します。事前検証を用意したら例外は InvalidOperationException 等を、用意しないのならば厳密な型の例外を投げます。

    下位レイヤは基本的に、「例外は投げるが事前検証で回避すべきである」という考え方です。

    > となると本質的な問題は、「業務エラーケース(=呼び出し元との間で「約款」として定められるべきケース)をどうやって表現するのが最も適切なのか?」という議論であると思います。が、結局のところ、その議論に対する万人が納得しうる正解は「Java の検査例外に相当する仕組みがあること」なので、この話って誰もが納得する一意の答えは得られないのではないか、と……残念ながら;。

    そうですね、誰もが納得する一意の答えは得られないですね。

    ただ、Java の検査例外があっても、答えは唯一にはならないんじゃないかなぁと思います。僕が下位レイヤに事前検証メソッド (や状態プロパティなんか) を積極的に用意するのは、使いやすさ (処理の実行はしないけど検証したい、状態を知りたい、って時もあると思います) や可読性の観点からですので、僕の場合は検査例外があったとしても「全て検査例外で」とはならない気がします。(実際に使ってみるとまた違う答えになるかもですが…w)

  • よこけんさん、書き込みありがとうございます。つらつらと。

    > こんな感じでインスタンスを毎回生成しても NG ですか?

    はい、というかこの設計は(例外や業務エラー云々を抜きにして)避けるべきだと私は考えます。この設計の場合、ビジネスロジッククラスはステートフルになりますが、よこけんさんの例でいうと、

    ・.Execute() メソッド

    ・.Error プロパティ

    については、内部的に「不正な順番でアクセスされた場合に対する対処コード」を記述しなければならなくなります。(例:.Execute() の前に .Error を呼び出された場合には InvalidOperationException を投げるとか。) それはめんどいので、ステートレス設計にしてしまおう、と。(極端なことを言うと、ビジネスロジッククラスについては「全部 static なメソッドで書く」ぐらいの勢いでもよい、と私は思っています。)

    > 僕が下位レイヤに事前検証メソッド (や状態プロパティなんか) を積極的に用意するのは、使いやすさ (処理の実行はしないけど検証したい、状態を知りたい、って時もあると思います) や可読性の観点からですので、僕の場合は検査例外があったとしても「全て検査例外で」とはならない気がします。(実際に使ってみるとまた違う答えになるかもですが…w)

    ちなみに個人的には、事前検証メソッドや状態プロパティなどの確認コードを用意することは、どちらかというと避けます。(メソッドの数などがむやみに増えるため) たぶん正解は、

    ・言語仕様として、検査例外の仕組みを導入する。(← .NET では×、Java では◎)

    ・汎用性の高いライブラリ(クラスライブラリ)については、検査例外を使わず、すべて実行時例外で表現する。(← .NET では◎、Javaでは×)

    ・業務アプリレベルでは、業務エラーの表現に、検査例外(またはそれに相当する仕組み)を利用する。

    です。ポイントは、「callerとcalleeが近くにいない場合には、何を業務エラー、何をアプリエラーとすべきかを、callee側の一方的な判断で決められない」ということで、Java のライブラリが多少使いにくいのはその辺に原因があります。すべてのメリットをカバーした言語やライブラリが存在しないことが悩みの種ですねぇ....。

    と、それはともかく。

    > とあるコンサルタントさん (名前は伏せておいた方がいいんですよね?^^;)

    まあすでにバレバレですが(笑)、nakama さん、とか nakama っち、とか呼んでもらえればおっけーですw。

  • というわけで、前回まで 3 回(+α)に渡って .NET の例外処理の適切な書き方について解説してきましたが、ここまでの解説にもまして重要なのは、 表示またはロギングされた例外情報を、正確に読み取れるようになること。

Page 1 of 1 (9 items)
Leave a Comment
  • Please add 6 and 1 and type the answer here:
  • Post