【実務・中級編】前提条件の循環参照を検知してログ出力するデバッグツール – Project VBA解析バイブル

スポンサーリンク

Project VBAを掌握する極限の知見
第4回:タスク・マトリクスの深淵 – グラフ理論で断つ「循環参照」の悪夢

開発プロジェクトの現場において、WBS(Work Breakdown Structure)やタスク間の依存関係管理をExcel(Project VBA)上で構築した経験はないだろうか。
「先行タスクが完了しなければ、後続タスクに着手できない」。一見、美しく直感的な依存関係のチェーンも、プロジェクトの規模が膨らみ、担当者が複雑に入り組んだプレデセッサ(前提条件)を設定し始めた瞬間、それは静かに崩壊する。

「タスクAの完了がBを促し、BがCを呼び、最終的にCがAの開始を要求する」

――そう、循環参照(Circular Dependency)の誕生だ。

この呪われた無限ループは、Projectのスケジュールエンジンを沈黙させ、最悪の場合、アプリケーションをハングアップに追い込む。今回は、この泥沼のような依存関係の迷宮にグラフ理論のメスを入れ、VBAのメモリ上で一刀両断する「循環参照検知デバッグツール」の極限実装を伝授する。

なぜ、愚直なループ探索は破綻するのか?

多くの初学者が陥る罠が、「タスクのリストを上から順に舐め、プレデセッサを再帰的に辿る」というアプローチだ。
この設計には、以下の致命的な欠陥(アンチパターン)が存在する。

1. スタックオーバーフローの恐怖:VBAの再帰呼び出し深度には限界がある。巨大なWBS構造でこれをやると、あっさり「プロシージャの呼び出し回数が多すぎます(エラー 28)」で沈没する。
2. O(N^2)以上の計算量爆発:キャッシュを持たずに同じノードを何度も評価する愚直な探索は、タスクが数千件を超えた途端、Excelを固まらせる。
3. エラーハンドリングの欠落:存在しないタスクIDが参照されている場合の「参照切れ」を考慮していないため、デバッグツール自体がバグの温床になる。

我々が求めるべきは、数学的裏付けのある堅牢なアルゴリズムだ。ここでは「有向グラフのトポロジカルソート(Kahnのアルゴリズム変形)」および「深さ優先探索(DFS)による白色・灰色・黒色coloring(三色問題)」の概念をVBAに移植する。

アーキテクチャ設計:堅牢なデータ構造の定義

循環参照を検知するためには、Excelシートのセルを行ったり来たりする低速なアプローチを捨て、メモリ上(Scripting.Dictionary)に「隣接リスト(Adjacency List)」を構築する必要がある。

  • ノード(Node):各タスクID
  • 有向エッジ(Directed Edge):タスクA $\to$ タスクB(AはBの先行タスクである=BはAに依存している)

この構造に対し、DFS(深さ優先探索)のバックトラック時に「現在探索中のパス上に存在するノード(灰色)」に再度遭遇した瞬間、それが循環参照の証拠(バック・エッジ)となる。

プロダクションコード:循環参照検知デバッグエンジン

以下のコードは、実務の現場でそのままコピペして即座に検証可能な、モジュール化されたプロダクションコードである。エラーハンドリング、メモリ管理、そして詳細なイミディエイトログ出力を完備している。

Option Explicit

‘ =================================================================================
‘ módulo名: MdlCycleDetector
‘ テーマ: グラフ理論に基づくタスク依存関係の循環参照検知エンジン
‘ 依存関係: Microsoft Scripting Runtime (早期バインディング推奨)
‘ =================================================================================

Public Sub RunCircularDependencyCheck()
Dim ws As Worksheet
On Error GoTo ErrorHandler

‘ 対象シートの取得(環境に合わせて変更してください)
Set ws = ActiveSheet

Dim startTime As Double
startTime = Timer

Debug.Print “————————————————–”
Debug.Print “[INFO] 依存関係のグラフ構築を開始します…”

‘ 1. 依存関係グラフ(隣接リスト)の構築
Dim graph As Scripting.Dictionary
Set graph = BuildAdjacencyList(ws)

If graph.Count = 0 Then
Debug.Print “[WARN] 検証対象のタスクが見つかりませんでした。”
Exit Sub
End If

Debug.Print “[INFO] グラフ構築完了。ノード数: ” & graph.Count & ” 処理時間: ” & Format(Timer – startTime, “0.00秒”)
Debug.Print “[INFO] 循環参照の探索(DFS)を実行中…”

‘ 2. 循環参照の検知実行
Call DetectCycles(graph)

Debug.Print “[INFO] 検証プロセスが正常に終了しました。”
Debug.Print “————————————————–”
Exit Sub

ErrorHandler:
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical, “デバッグツール異常終了”
Debug.Print “[ERROR] Fatal Error: ” & Err.Description
End Sub

‘ ——————————————————————————–
‘ @Summary: シート上のタスク表から隣接リスト(グラフ)を構築する
‘ @Param : ws – 対象ワークシート
‘ @Return : Scripting.Dictionary (Key: TaskID, Value: Collection of Successors)
‘ ——————————————————————————–
Private Function BuildAdjacencyList(ws As Worksheet) As Scripting.Dictionary
Dim dictGraph As New Scripting.Dictionary
Dim lastRow As Long
Dim i As Long

‘ 仮定: A列=タスクID, B列=タスク名, C列=先行タスクID(カンマ区切り)
‘ ※実際のスキーマに合わせて適宜列番号を変更してください
lastRow = ws.Cells(ws.Rows.Count, “A”).End(xlUp).Row
If lastRow < 2 Then Set BuildAdjacencyList = dictGraph: Exit Function Dim taskID As String Dim predecessorsStr As String Dim predArray() As String Dim p As Long ' 第1パス: 全てのノードを辞書に登録 For i = 2 To lastRow taskID = Trim(CStr(ws.Cells(i, 1).Value)) If taskID <> “” Then
If Not dictGraph.Exists(taskID) Then
dictGraph.Add taskID, New Collection
End If
End If
Next i

‘ 第2パス: エッジ(依存関係)の張り付け
‘ 注意: 「Aの先行がBである」ということは、Bが完了しないとAが始まらない。
‘ グラフ理論で循環を見る場合、B -> A の有向エッジとして構築する。
For i = 2 To lastRow
taskID = Trim(CStr(ws.Cells(i, 1).Value))
predecessorsStr = Trim(CStr(ws.Cells(i, 3).Value)) ‘ C列: 先行タスク

If taskID <> “” And predecessorsStr <> “” Then
predArray = Split(predecessorsStr, “,”)
For p = LBound(predArray) To UBound(predArray)
Dim predID As String
predID = Trim(predArray(p))

If predID <> “” Then
‘ 先行タスクが存在する場合のみエッジを追加
If dictGraph.Exists(predID) Then
‘ predID -> taskID の方向でエッジを追加
dictGraph(predID).Add taskID
Else
Debug.Print “[WARN] 参照切れ検出: タスク [” & taskID & “] の先行タスク [” & predID & “] が存在しません。”
End If
End If
Next p
End If
Next i

Set BuildAdjacencyList = dictGraph
End Function

‘ ——————————————————————————–
‘ @Summary: 深さ優先探索(DFS)と三色coloringによる循環参照検知
‘ ——————————————————————————–
Private Sub DetectCycles(graph As Scripting.Dictionary)
‘ 状態管理用辞書
‘ 0 (または未登録) = 未訪問 (White)
‘ 1 = 訪問中 / 現在のパス上に存在 (Gray) -> これに再会したら循環!
‘ 2 = 訪問完了 / 派生する全ノードの探索終了 (Black)
Dim visitingState As New Scripting.Dictionary

Dim key As Variant
Dim cycleFound As Boolean
cycleFound = False

For Each key In graph.Keys
If visitingState.Item(key) <> 2 Then
Dim pathStack As New Collection
If DFSVisit(CStr(key), graph, visitingState, pathStack) Then
cycleFound = True
End If
End If
Next key

If Not cycleFound Then
Debug.Print “[SUCCESS] 循環参照は検出されませんでした。WBSの構造は健全です。”
End If
End Sub

‘ ——————————————————————————–
‘ @Summary: DFSの再帰本体(バックトラックによるパス追跡付き)
‘ ——————————————————————————–
Private Function DFSVisit(ByVal node As String, _
ByRef graph As Scripting.Dictionary, _
ByRef visitingState As Scripting.Dictionary, _
ByRef pathStack As Collection) As Boolean

‘ 現在のノードを「訪問中(Gray: 1)」に設定し、パスに追加
visitingState.Item(node) = 1
pathStack.Add node, node ‘ キー付きで追加して重複・存在チェックを高速化

Dim successors As Collection
Set successors = graph(node)

Dim succ As Variant
Dim foundCycle As Boolean
foundCycle = False

For Each succ In successors
Dim state As Integer
state = 0
If visitingState.Exists(succ) Then state = visitingState.Item(succ)

If state = 1 Then
‘ 【循環検出】Gray状態のノードにヒットした!
foundCycle = True
Debug.Print “==========================================”
Debug.Print “[CRITICAL] 循環参照(デッドロック)を検出しました!”
Debug.Print “——————————————”

‘ 循環パスの視覚化
Dim msg As String
msg = “パス: ”
Dim item As Variant
Dim printFlag As Boolean
printFlag = False

For Each item in pathStack
If item = succ Then printFlag = True
If printFlag Then msg = msg & item & ” -> ”
Next item
msg = msg & succ
Debug.Print msg
Debug.Print “==========================================”

ElseIf state = 0 Then
‘ 未訪問なら再帰的に潜る
If DFSVisit(CStr(succ), graph, visitingState, pathStack) Then
foundCycle = True
End If
End If
‘ state = 2 (Black) の場合は既に安全確認済みなのでスキップ
Next succ

‘ 探索完了したので「訪問完了(Black: 2)」に昇格し、パスからポップ
visitingState.Item(node) = 2
pathStack.Remove node

DFSVisit = foundCycle
End Function

現場で活きる!プロフェッショナルの実装ノート

このコードをデプロイするにあたり、アーキテクトとしていくつか補足しておかなければならない「現場の知見」がある。

1. 早期バインディング(Early Binding)の徹底

`Scripting.Dictionary` を使用するためには、VBAエディタのツール参照設定で 「Microsoft Scripting Runtime」 にチェックを入れる必要がある。
遅延バインディング(`CreateObject(“Scripting.Dictionary”)`)は記述の手間は減るが、ポリモーフィズムのオーバーヘッドが発生し、数万件のノードを扱う大規模WBSでは致命的なパフォーマンス低下を招く。プロたるもの、参照設定の徹底またはクラスモジュールによる自前ハッシュマップの実装を選ぶべきだ。

2. シートI/Oの排除

初心者がやりがちな「判定のたびにセルの値を `Cells(row, col)` で取得する」という実装は、Excel VBAにおいて最大のボトルネック(COM境界を跨ぐコスト)である。
今回のコードでは、グラフ構築時のわずか2回の走査(第1パス・第2パス)以外でシートにアクセスしない。全てのグラフ走査はメモリ上のDictionary内で完結するため、実行速度は秒速数万ノードを叩き出す。

3. 実務への組み込み方

このデバッグツールは、単体でマクロとして実行するだけでなく、WBSデータのインポート処理の直前や、ユーザーが「計画確定」ボタンを押した際のバリデーション・フックとして組み込むのが最も効果的である。
循環参照が1つでも検知された場合は、処理をアボート(中断)させ、イミディエイトウィンドウのログをユーザーフレンドリーなメッセージボックスに整形して返してやれば、ヘルプデスクへの問い合わせ件数を劇的に減らすことができる。

総括

VBAは、玩具ではない。正しく設計されたグラフ理論のアルゴリズムを実装すれば、Excelは単なる表計算ソフトから、高度なプロジェクト・オーケストレーション・エンジンへと変貌を遂げる。

「動けばいい」という妥協が生む技術的負債は、いつの日かプロジェクトそのものを窒息させる。
この循環参照検知エンジンをあなたの開発環境に組み込み、美しく、そして鉄壁のロジックを持つ自動化システムを構築してほしい。

タイトルとURLをコピーしました