【VB.NET極意】業務エラーを制する者がシステムを制す:カスタム例外設計の深淵
開発現場でよく見かける光景がある。すべての例外を`Catch ex As Exception`で一網打尽にし、`MessageBox.Show(ex.Message)`で画面に垂れ流すコード。そして、ログには「エラーが発生しました」という無機質な文字列だけが残る。
これでは、実務に耐えうる堅牢な業務アプリケーションとは言えない。
システムが予期せぬクラッシュを起こした「システムエラー(バグ・通信断・DB障害)」と、ユーザーの入力不備や残高不足といった「業務上のルール違反(業務エラー)」は、明確に分離されなければならない。
今回は、VB.NET中級者から一歩抜け出し、大規模な業務システムや自動化ツールを支える「カスタム例外(Custom Exception)」の設計と実装の極意を伝授しよう。
—
1. なぜカスタム例外が必要なのか?
標準の`Exception`クラスだけでは、業務ロジックの意図を上位レイヤー(UI層やログ出力層)へ正確に伝達できない。
例えば、「口座残高不足」という事象を考えてみる。
これを単なる `Exception` や `ApplicationException`(※現在は非推奨)として投げると、呼び出し元ではメッセージの文字列を「部分一致」で判定するような脆弱なコードを書く羽目になる。
‘ 【アンチパターン】文字列依存の判定。メッセージ仕様変更で即死する。
If ex.Message.Contains(“残高が不足”) Then
‘ 残高不足のリカバリ処理
End If
カスタム例外を導入すれば、「型」による厳密なハンドリングが可能になる。さらに、エラーコードや追加情報(不足金額や対象口座IDなど)をプロパティとして型安全に保持できるため、保守性が劇的に向上する。
—
2. 堅牢なカスタム例外クラスの設計原則
.NETの世界でカスタム例外を作る際、守るべき鉄則が3つある。
1. `Exception` クラスを継承する(またはその派生)。
2. クラス名の末尾は必ず `Exception` で終わらせる(コーディング規約の絶対遵守)。
3. シリアライズを考慮したコンストラクタを完備する(特にCOM InteropやWCF、remoting等を意識する場合)。
実務でそのまま使える、業務エラー用カスタム例外の決定版コードを見てほしい。
—
3. 【プロダクションコード】業務エラー例外の実装
以下のコードは、エラーコード (`ErrorCode`) と、業務的な追加データ (`ErrorContext`) を保持できる汎用的な業務例外クラスの模範解答だ。
Imports System
Imports System.Runtime.Serialization
Namespace Business.Exceptions
”’
”’
Public Class BusinessLogicException
Inherits Exception
‘ 業務エラーコード(例: “ERR_FIN_001″)
Public ReadOnly Property ErrorCode As String
‘ 追加のコンテキスト情報(必要に応じて画面表示やログに活用)
Public ReadOnly Property ErrorContext As Dictionary(Of String, String)
‘ デフォルトコンストラクタ
Public Sub New()
MyBase.New(“業務処理実行中にエラーが発生しました。”)
Me.ErrorCode = “ERR_BIZ_GENERAL”
Me.ErrorContext = New Dictionary(Of String, String)()
End Sub
‘ メッセージを受け取るコンストラクタ
Public Sub New(message As String)
MyBase.New(message)
Me.ErrorCode = “ERR_BIZ_GENERAL”
Me.ErrorContext = New Dictionary(Of String, String)()
End Sub
‘ メッセージとエラーコードを受け取るコンストラクタ(実務で最も多用)
Public Sub New(message As String, errorCode As String)
MyBase.New(message)
Me.ErrorCode = errorCode
Me.ErrorContext = New Dictionary(Of String, String)()
End Sub
‘ メッセージ、エラーコード、内部例外を受け取るコンストラクタ
Public Sub New(message As String, errorCode As String, innerException As Exception)
MyBase.New(message, innerException)
Me.ErrorCode = errorCode
Me.ErrorContext = New Dictionary(Of String, String)()
End Sub
‘ 構造化データ(コンテキスト)を付与できるコンストラクタ
Public Sub New(message As String, errorCode As String, context As Dictionary(Of String, String))
MyBase.New(message)
Me.ErrorCode = errorCode
Me.ErrorContext = If(context, New Dictionary(Of String, String)())
End Sub
‘ シリアライズ用コンストラクタ(.NETの例外設計ガイドライン必須要件)
Protected Sub New(info As SerializationInfo, context As StreamingContext)
MyBase.New(info, context)
Me.ErrorCode = info.GetString(“ErrorCode”)
‘ 簡易的なディクショナリ復元(実運用では型安全なデシリアライズを推奨)
‘ ここでは簡略化のため省略
End Sub
‘ シリアライズ処理のオーバーライド
Public Overrides Sub GetObjectData(info As SerializationInfo, context As StreamingContext)
MyBase.GetObjectData(info, context)
info.AddValue(“ErrorCode”, Me.ErrorCode)
End Sub
End Class
End Namespace
—
4. 実戦投入:サービス層でのスローとUI層でのキャッチ
では、このカスタム例外を実際の業務ロジック(例:銀行口座の出金処理)でどう使うか。アーキテクチャの上下関係を意識した実装例を示す。
サービス層(ビジネスロジック)でのスロー
Public Class AccountService
Public Sub Withdraw(accountId As String, amount As Decimal, currentBalance As Decimal)
‘ 残高不足のチェック
If currentBalance < amount Then
' 追加情報をディクショナリで持たせることで、上位層でのリッチなハンドリングが可能に
Dim extraInfo As New Dictionary(Of String, String) From {
{"AccountId", accountId},
{"Required", amount.ToString("C")},
{"Balance", currentBalance.ToString("C")}
}
' カスタム例外をスロー
Throw New BusinessLogicException(
"口座残高が不足しているため、出金処理を完了できません。",
"ERR_FIN_INSUFFICIENT_FUNDS",
extraInfo
)
End If
' 通常の出金処理...
End Sub
End Class
UI層(またはエントリポイント)でのスマートなハンドリング
Public Class MainForm
Private Sub btnWithdraw_Click(sender As Object, e As EventArgs) Handles btnWithdraw.Click
Dim service As New AccountService()
Try
‘ 業務処理の実行
service.Withdraw(“ACC-1005″, 50000D, 10000D)
Catch ex As BusinessLogicException
‘ 【業務エラーのキャッチ】
‘ ユーザーに対しては優しく、かつ正確なコードと共に通知
Dim detailMsg As String = $”[エラーコード: {ex.ErrorCode}]” & vbCrLf & ex.Message
‘ コンテキスト情報の活用(例:不足額を表示するなど)
If ex.ErrorContext.ContainsKey(“Required”) Then
detailMsg &= vbCrLf & $”不足金額: {ex.ErrorContext(“Required”)}”
End If
MessageBox.Show(detailMsg, “業務エラー”, MessageBoxButtons.OK, MessageBoxIcon.Warning)
‘ 業務ログにはコードと詳細を出力
Logger.Warn($”業務エラー発生: {ex.ErrorCode} – {ex.Message}”)
Catch ex As Exception
‘ 【システムエラー(予期せぬバグ等)のキャッチ】
Logger.Error(“予期せぬシステムエラーが発生しました。”, ex)
MessageBox.Show(“システム内部でエラーが発生しました。管理者に連絡してください。”, “致命的エラー”, MessageBoxButtons.OK, MessageBoxIcon.Error)
End Try
End Sub
End Class
—
5. プロジェクトアーキテクトからの提言
カスタム例外を導入する最大のメリットは、「エラーハンドリングの責任を適切なレイヤーに割り振れること」にある。
- UI層は、例外の「型」を見て、メッセージボックスの色を変えたり(業務エラーなら黄色の警告、システムエラーなら赤の致命的)、リトライボタンを表示したりする制御に専念できる。
- データアクセス層(DB・ファイル連携)は、接続切れや一意制約違反などを低レベルの例外として捉え、必要に応じて `BusinessLogicException` にラップ(包み直してスロー)して上位に伝えることで、DBの実装詳細を上位に隠蔽できる。
「ただ動くコード」から「保守しやすく、拡張性のあるコード」へ。
今日のこの設計をあなたのプロジェクトに組み込めば、エラーログの解析スピードとアプリケーションの信頼性は劇的に跳ね上がるはずだ。
