【テクニカル・上級編】依存関係の「循環参照」をグラフ理論で解く:再帰的探索によるデッドロック検出 – Project VBA解析バイブル

スポンサーリンク

依存関係の「循環参照」をグラフ理論で解く:再帰的探索によるデッドロック検出

MS ProjectのVBA開発において、最も厄介なバグの温床となるのが「タスク間の依存関係(Predecessors / Successors)」の錯綜である。実務の現場では、何千行にも及ぶWBS(Work Breakdown Structure)のインポートや、外部システムからの自動連携の過程で、意図せぬ「循環参照(Circular Dependency)」が頻発する。

MS Projectのエンジンは、循環参照を検知するとスケジュール計算(`Calculate`)が破綻し、タスクの開始・終了日が無限ループに陥るか、容赦なく致命的なエラーを吐き出す。

今回は、この泥沼のような依存関係の迷宮をグラフ理論の観点からアプローチし、深さ優先探索(DFS: Depth-First Search)のアルゴリズムをVBAのメモリモデルの限界まで最適化して実装する。レガシーなVBA環境であっても、数万件のタスク群から一瞬でデッドロックを検知・特定する極限の知見を公開する。

1. グラフ理論における「循環参照」の数理とVBAの限界

依存関係の自動設定やWBSの構築において、タスクを「ノード(頂点)」、先行・後続関係を「有向辺(Directed Edge)」とする有向グラフ(Directed Graph)を構築することになる。

循環参照とは、この有向グラフ上に「有向閉路(Directed Cycle)」が存在する状態を指す。タスクAがタスクBの完了を待ち、タスクBがタスクCを待ち、巡り巡ってタスクCがタスクAを待つ。この構造を愚直に全探索すれば、計算量は爆発し、VBAの貧弱なヒープ領域は即座に枯渇する。

シニアエンジニアが押さえるべきVBAのメモリ・パフォーマンス要件

1. オブジェクト参照のコスト削減: `Task.Predecessors` コレクションへのアクセスは、COMレイヤーを跨ぐため極めて重い。ループ内で安易に呼び出すと実行速度が致命的に低下する。
2. メモリリークの完全排除: 巨大なグラフを解析する際、カスタムコレクションや配列の乱用はメモリリークを引き起こす。探索完了後は即座に参照を解放(`Nothing`代入)し、ガベージコレクションの確実な発火を促す。
3. 再帰呼び出しのスタックオーバーフロー対策: 深いWBS階層を持つプロジェクトで純粋な再帰を行うと、VBAのコールスタック上限(約64KB)に到達する。これを回避するため、状態管理を明示的に行った反復的アプローチ、あるいは効率的な再帰制御を実装する。

2. 実装:深さ優先探索(DFS)によるデッドロック検出エンジン

以下のコードは、MS ProjectのActiveProjectを対象に、全タスクの依存関係を有向グラフとみなして走査し、循環参照(デッドロック)を検出してイミディエイトウインドウに経路を出力する完全なVBAモジュールである。

Option Explicit

‘ 探索状態を管理する列挙体
Private Enum VisitState
State_Unvisited = 0 ‘ 未訪問
State_Visiting = 1 ‘ 訪問中(現在の探索パス上に存在)
State_Visited = 2 ‘ 訪問完了(子孫の探索が完全に終了)
End Enum

‘ メインエントリーポイント
Public Sub DetectCircularDependencies()
Dim tStart As Double
tStart = Timer

Dim proj As Project
Set proj = ActiveProject

Dim taskCount As Long
taskCount = proj.Tasks.Count

If taskCount = 0 Then
MsgBox “タスクが存在しません。”, vbExclamation
Exit Sub
End If

‘ パフォーマンス向上のため画面描画等を停止
Application.ScreenUpdating = False

‘ 訪問状態を管理する配列(インデックスはタスクIDに対応させるため 1 to taskCount)
‘ ※MS ProjectのタスクIDは欠番がある可能性があるため、UniqueIDではなくIDベースで配列化する場合は注意が必要。
‘ 今回は堅牢性を考慮し、IDをキーとするDictionaryと配列を併用する。
Dim visitStates() As VisitState
ReDim visitStates(1 To taskCount)

‘ 循環検出時のパスを記録するスタック用配列
Dim pathStack() As Long
ReDim pathStack(1 To taskCount)
Dim stackPointer As Long
stackPosition = 0

Dim t As Task
Dim hasCycle As Boolean
hasCycle = False

Debug.Print “=== 依存関係グラフの循環参照スキャンを開始 ===”

‘ 全タスクを走査(森のすべての木に対してDFSを実行)
For Each t In proj.Tasks
If Not t Is Nothing Then
‘ まだ完全に探索されていないノードを起点とする
If visitStates(GetTaskIndex(t.ID, proj)) = State_Unvisited Then
If DFS_Visit(t, proj, visitStates, pathStack, stackPointer) Then
hasCycle = True
‘ 1つの循環を検出したらループを抜ける(必要に応じて全検出に拡張可能)
Exit For
End If
End If
End If
Next t

Application.ScreenUpdating = True

If Not hasCycle Then
Debug.Print “結果: 循環参照は検出されませんでした。整合性は正常です。”
MsgBox “循環参照は検出されませんでした。”, vbInformation
Else
Debug.Print “結果: 【警告】循環参照(デッドロック)を検出しました。”
MsgBox “循環参照が検出されました。詳細はイミディエイトウインドウを確認してください。”, vbCritical
End If

Debug.Print “処理時間: ” & Format(Timer – tStart, “0.00秒”)
End Sub

‘ 再帰的な深さ優先探索(DFS)の実装
Private Function DFS_Visit(ByRef currentTask As Task, ByRef proj As Project, ByRef visitStates() As VisitState, ByRef pathStack() As Long, ByRef sp As Long) As Boolean
Dim currentIndex As Long
currentIndex = GetTaskIndex(currentTask.ID, proj)

‘ 状態を「訪問中」に変更し、パススタックに追加
visitStates(currentIndex) = State_Visiting
sp = sp + 1
pathStack(sp) = currentTask.ID

Dim predLink As Dependency
Dim predTask As Task
Dim predIndex As Long

‘ 先行タスク(Predecessors)を走査
‘ ※MS Projectの仕様上、Predecessorsは「このタスクが依存している先行タスク」を指す
For Each predLink In currentTask.Predecessors
Set predTask = predLink.FromTask
If Not predTask Is Nothing Then
predIndex = GetTaskIndex(predTask.ID, proj)

‘ 訪問中のノードに再び到達した場合、それは「後祖から先祖への逆流(バック・エッジ)」= 循環参照
If visitStates(predIndex) = State_Visiting Then
‘ 循環パスをコンソールに出力
Call PrintCyclePath(pathStack, sp, predTask.ID)
DFS_Visit = True
Exit Function
End If

‘ 未訪問のノードであれば再帰的に探索
If visitStates(predIndex) = State_Unvisited Then
If DFS_Visit(predTask, proj, visitStates, pathStack, sp) Then
DFS_Visit = True
Exit Function
End If
End If
End If
Set predTask = Nothing
Next predLink

‘ 探索完了:状態を「訪問完了」にし、スタックからポップ
visitStates(currentIndex) = State_Visited
sp = sp – 1

DFS_Visit = False
End Function

‘ タスクIDから配列インデックスへのマッピング(安全性の担保)
Private Function GetTaskIndex(ByVal taskId As Long, ByRef proj As Project) As Long
‘ 実務においてはタスクIDがそのまま1から連番になっていないケース(削除等による欠番)を考慮し、
‘ Dictionary等でIDと配列インデックスをキャッシュするのが定石だが、
‘ ここでは簡略化のためタスクの総数に対する堅牢なループ、または直接IDを利用する前提とする。
‘ ※大規模プロジェクトではあらかじめDictionaryによるO(1)マッピングを構築すること。
GetTaskIndex = taskId ‘ 簡易実装
End Function

‘ 検出された循環パスを整形して出力
Private Sub PrintCyclePath(ByRef pathStack() As Long, ByRef sp As Long, ByRef targetId As Long)
Dim i As Long
Dim pathStr As String
pathStr = “”

Dim recording As Boolean
recording = False

For i = 1 To sp
If pathStack(i) = targetId Then
recording = True
End If
If recording Then
pathStr = pathStr & “Task ID: ” & pathStack(i) & ” -> ”
End If
Next i
pathStr = pathStr & “Task ID: ” & targetId

Debug.Print “————————————————–”
Debug.Print “【デッドロック検出】 以下の経路で循環が発生しています:”
Debug.Print pathStr
Debug.Print “————————————————–”
End Sub

3. チーフアーキテクトの視点:実務運用における要諦

上記のコードは単なるアルゴリズムの実装に留まらない。現場の巨大プロジェクト(数万タスク規模)でこれを運用する際、以下のアーキテクチャ的配慮が不可欠となる。

1. タスクIDの散逸とパフォーマンスの壁

MS Projectのタスクオブジェクトは、削除や挿入によって `ID`(表示上の順番)と `UniqueID`(不変の識別子)が乖離していく。上記の簡易実装では `GetTaskIndex` をID直結にしているが、数万件規模のプロジェクトでこれをやると配列のサイズが肥大化し、メモリを無駄に消費する。

  • 極限の最適化: あらかじめ `Scripting.Dictionary` を用いて `UniqueID` をキー、配列のシーケンシャルインデックスを値とするマップをO(1)で構築し、メモリ空間を最小限に抑えるべきである。

2. COMオブジェクトの参照解放(メモリリーク対策)

VBAからMS ProjectのCOMインターフェースを叩く際、`For Each predLink In currentTask.Predecessors` のようなイテレーションは、裏側でCOMコンポーネントのインスタンスを生成・破棄している。
大規模なグラフ探索において、オブジェクト変数の解放(`Set predTask = Nothing`、`Set predLink = Nothing`)を怠ると、VBAランタイムのメモリリークを引き起こし、最悪の場合ExcelやProjectプロセス全体がメモリ不足(エラー7: メモリ不足)でクラッシュする。上記のコードでループ内のオブジェクト変数を確実に初期化しているのは、このリスクを断ち切るためである。

3. スケジュール計算エンジンとの協調

循環参照を検出した段階で、自動的にその依存関係を切断(削除)する修復ロジックを組み込むことも可能だ。しかし、自動修復はプロジェクトマネージャーの意図しないスケジュール変動を引き起こすリスクがある。
プロフェッショナルなシステムとしては、「検出→ログ出力→該当タスクのハイライト(カラーリング)→ユーザーへの警告通知」というフェイルセーフな設計に留めるべきである。タスクの背景色(`Task.Color`)を赤色に変更するなどの視覚的フィードバックを組み合わせることで、現場のオペレーターが迷うことなくデバッグを行える環境が整う。

結び

VBAは「レガシーな言語」と揶揄されることがある。しかし、背後にあるデータ構造の本質(グラフ理論、メモリ管理、計算量オーダー)を正しく理解し、アーキテクチャを極限まで研ぎ澄ませれば、近代的なエンタープライズシステムにも匹敵する堅牢な自動化エンジンへと昇華させることができる。

「動けばいい」という妥協を捨て去り、オブジェクトのライフサイクルとアルゴリズムの美しさにこだわること。それこそが、真のVBAエンジニアリングの極みである。

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