[Architect’s Journal] Visio VBAにおける「沈黙の失敗」を打破せよ:コンテキストを保持する堅牢なエラーハンドリング設計
業務自動化の現場において、ツールが「動く」ことと「信頼に値する」ことの間には、深くて暗い川が流れている。
多くの開発者が、Visio VBAでページ操作やレイヤー操作の自動化を行う際、手っ取り早く `On Error Resume Next` を書き込む。しかし、これはプロフェッショナルの仕事ではない。それは、バグを「解決」しているのではなく、単に「隠蔽」しているだけだ。
複雑な図面、数百枚のページ、動的に生成されるレイヤー。これらを扱う大規模な自動化処理において、エラーが発生した瞬間に処理が停止してしまえば、ユーザーの作業を中断させる。かといって、エラーを無視して進めば、不完全な図面が出来上がり、後工程で致命的な問題を引き起こす。
我々アーキテクトに求められるのは、「エラーを検知し、詳細な文脈(コンテキスト)を記録し、かつ致命的な停止を避けつつ、処理を継続させる」という、高度なフォールトトレランス(耐故障性)を備えた設計である。
本稿では、Visio VBAにおける「スタックトレース風ログ出力」と「ページスキップ戦略」を実装するための、極限の知見を伝授する。
1. なぜ、標準的なエラーハンドリングでは不十分なのか
VBAの標準的な `Err.Number` と `Err.Description` だけでは、実務レベルのトラブルシューティングには到底足りない。なぜなら、Visioのオブジェクトモデルは階層的であり、以下の情報が欠落しているからだ。
- 「どのページ」の操作中に発生したのか?
- 「どのレイヤー」に対して操作を行おうとしていたのか?
- 「どのステップ(追加、削除、サイズ変更)」で失敗したのか?
例えば、「ページのリサイズに失敗しました(Error 0x800…)」というログだけ渡されても、エンジニアは数千枚のページを一つずつ手作業で確認することになる。これは、自動化ツールの存在意義を根底から覆す「非効率の極み」である。
2. 設計思想:コンテキスト・アウェア・ハンドリング
我々が実装すべきは、「Context-Aware Error Handling(文脈を理解するエラーハンドリング)」である。
エラーが発生した際、その時点の「現在の状態(State)」をログにパッケージ化して出力する。具体的には、以下の情報を一つのエラーレコードとして統合する設計をとる。
1. Timestamp: 発生時刻
2. Operation: 実行中だったメソッド名(例: `ResizePage`)
3. Target Identity: 対象のページ名、ページID、またはレイヤー名
4. Error Details: Errオブジェクトの数値とメッセージ
5. Stack Trace (Simulated): どの関数からどの関数が呼ばれていたかの軌跡
3. プロダクション・コードの実装例
以下に、ページ操作のバッチ処理において、失敗したページをスキップしつつ、詳細なログを残す堅牢な設計パターンを示す。
Option Explicit
' ===========================================================================
' Error Context Structure
' エラー発生時のコンテキストを保持するための定数
' ===========================================================================
Private Enum OpType
opPageAdd = 1
opPageDelete = 2
opPageResize = 3
opLayerConfig = 4
End Enum
' ===========================================================================
' Main Entry Point: ページ一括処理エンジン
' ===========================================================================
Public Sub ExecuteRobustPageAutomation()
Dim vsoDoc As Visio.Document
Set vsoDoc = ThisDocument
Dim pg As Visio.Page
Dim targetPageNames As Variant
Dim i As Long
' テスト用のターゲットリスト(実務ではDBやExcelから取得)
targetPageNames = Array("Page-1", "Invalid_Page", "Page-3", "Error_Trigger_Page")
Debug.Print "--- Automation Started: " & Now & " ---"
' ページループ処理
For i = LBound(targetPageNames) To UBound(targetPageNames)
' -------------------------------------------------------------------
' ページ単位のスコープでエラーハンドリングを行う。
' これにより、一つのページが失敗しても、次のページへのループを継続できる。
' -------------------------------------------------------------------
On Error GoTo PageErrorScope
' 1. ターゲットページの特定(存在しない場合の対策)
Set pg = FindPageByName(vsoDoc, CStr(targetPageNames(i)))
' 2. ページ操作の実行(コンテキストを渡す)
ProcessSinglePage pg, OpType.opPageResize
Debug.Print "Successfully processed: " & targetPageNames(i)
ContinueNextPage:
On Error GoTo 0 ' ハンドラをリセット
GoTo NextIteration
PageErrorScope:
' エラー発生時、詳細なコンテキストとともにログを出力
' ここではエラーを「記録」することに徹し、処理を中断させない
LogErrorDetailed targetPageNames(i), OpType.opPageResize, Err.Number, Err.Description
Resume ContinueNextPage
NextIteration:
Next i
Debug.Print "--- Automation Completed: " & Now & " ---"
MsgBox "処理が完了しました。詳細はログを確認してください。", vbInformation
End Sub
' ===========================================================================
' Core Logic: 個別ページの操作ロジック
' ===========================================================================
Private Sub ProcessSinglePage(ByRef targetPg As Visio.Page, ByVal currentOp As OpType)
' 引数がNothingの場合、即座にエラーを発生させて呼び出し元に制御を戻す
If targetPg Is Nothing Then Err.Raise vbObjectError + 513, "ProcessSinglePage", "Target page is Nothing."
' --- 擬似的な操作フロー ---
' 1. ページサイズの変更
' 実際にはここで行われる操作が複雑になる
Select Case currentOp
Case OpType.opPageResize
' 特定の条件下でエラーをシミュレート(テスト用)
If targetPg.Name = "Error_Trigger_Page" Then Err.Raise vbObjectError + 999, "ResizeLogic", "Simulated hardware failure."
targetPg.PageSheet.CellsU("PageWidth").FormulaU = "15 in"
targetPg.PageSheet.CellsU("PageHeight").FormulaU = "10 in"
Case OpType.opPageAdd
' 追加ロジック...
End Select
End Sub
' ===========================================================================
' Helper: ページ検索(堅牢な検索)
' ===========================================================================
Private Function FindPageByName(ByRef doc As Visio.Document, ByVal name As String) As Visio.Page
Dim pg As Visio.Page
For Each pg In doc.Pages
If pg.Name = name Then
Set FindPageByName = pg
Exit Function
End If
Next pg
' 見つからない場合はNothingを返し、呼び出し元でエラーハンドリングさせる
Set FindPageByName = Nothing
End Function
' ===========================================================================
' Logging Engine: 詳細ログ出力
' ===========================================================================
Private Sub LogErrorDetailed(ByVal contextName As String, ByVal op As OpType, ByVal errNum As Long, ByVal errDesc As String)
Dim logMsg As String
Dim opName As String
' 列挙型を文字列に変換
Select Case op
Case OpType.opPageAdd: opName = "PAGE_ADD"
Case OpType.opPageDelete: opName = "PAGE_DELETE"
Case OpType.opPageResize: opName = "PAGE_RESIZE"
Case OpType.opLayerConfig: opName = "LAYER_CONFIG"
Case Else: opName = "UNKNOWN_OP"
End Select
' スタックトレースを模した構造化ログの構築
' 実務ではこれをファイル出力やデータベースへのINSERTに置き換える
logMsg = String(50, "=") & vbCrLf & _
"[ERROR REPORT]" & vbCrLf & _
"Timestamp : " & Now & vbCrLf & _
"Target : " & contextName & vbCrLf & _
"Operation : " & opName & vbCrLf & _
"Error No : " & errNum & vbCrLf & _
"Description : " & errDesc & vbCrLf & _
String(50, "=")
' 1. イミディエイトウィンドウへの出力(開発用)
Debug.Print logMsg
' 2. 【実務アドバイス】ファイルへの書き出し
' AppendToFile "C:\Logs\VisioAutomation_Log.txt", logMsg
End Sub
4. アーキテクトの視点:実務における運用上の注意点
このコードをプロダクション環境に投入する際、以下の3点を必ず考慮してください。
① 外部連携(File/DB)の排他制御
ログをテキストファイルに書き出す場合、複数のインスタンスが同時に走ると「ファイルが他のプロセスで使用されています」というエラーで、ログ出力自体が失敗するという本末転倒な事態が起こります。
- 対策: ログ出力専用のクラスを作成し、ファイルを開く際は `Append` モードを使用すること。あるいは、ログを一旦メモリ上のコレクションに蓄積し、全処理終了後に一括書き出しを行う設計にしてください。
② 状態の整合性(State Integrity)
ページのリサイズ中にエラーが発生した場合、そのページは「中途半端なサイズ」になっている可能性があります。
- 対策: エラーが発生したページに対して、ロールバック(元のサイズに戻す)を行うか、あるいは「異常なページ」としてフラグを立て、後続のプロセスから隔離するロジックを検討してください。
③ パフォーマンスの重み
`On Error GoTo` は、エラーが発生しない限りオーバーヘッドは極めて小さいですが、エラーが発生した瞬間にスタックのジャンプが発生します。
- 対策: ループの「外側」ではなく、必ず「ループの内側(個別のオブジェクト操作単位)」にエラーハンドリングを配置してください。これにより、エラーの局所化(Isolation)が可能になります。
結論
優れた自動化エンジニアは、コードの「成功ルート」を書くことよりも、「失敗ルート」をいかに美しく、かつ詳細に設計することに情熱を注ぎます。
`On Error Resume Next` という安易な逃げ道を捨て、コンテキストを伴うログ設計を導入すること。それが、あなたの作成するツールを「単なるスクリプト」から「プロフェッショナルな業務アプリケーション」へと昇華させる唯一の道です。
