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

スポンサーリンク

やあ、プログラミングと業務自動化を愛する皆さん!Project VBAの世界へようこそ。
今日も元気にコードを書いていますか?

Microsoft Project(以下、MS Project)をVBAで制御できるようになると、数千行に及ぶ巨大なWBS(作業分解構成案)が一瞬で生成され、スケジュールが自動的に組み上がるという快感を味わえます。マクロの記録を卒業し、コードでプロジェクト全体を統制する領域に入った証拠ですね。

しかし、この自動化の旅路において、誰もが一度は衝突する「目に見えない巨大な壁」が存在します。
それが「依存関係の循環参照(デッドロック)」です。

「タスクAが終わらないとタスクBが始まらない。でも、タスクBが終わらないとタスクCが始まらず、タスクCが終わらないとタスクAが始まらない……」

このような論理的な矛盾が発生すると、MS Projectのスケジュール計算エンジンは悲鳴を上げ、マクロは途中で例外エラーを吐いて停止します。手動なら警告ダイアログが出ますが、VBAで大量のタスクを高速処理している最中にこれが起きると、原因の特定は困難を極めます。

そこで今回は、学術的な「グラフ理論」の知見を少しだけ借りて、再帰的探索(深さ優先探索:DFS)によって循環参照を100%事前に検知し、その悪夢のルートを可視化する究極のデバッグ手法を伝授します。

難しそうに聞こえるかもしれませんが、大丈夫。基礎から分かりやすく、紐解いていきますね。
ここをクリアすれば、Project VBAの基本、そしてプログラミングの本質的なアルゴリズム思考はバッチリですよ!

1. 依存関係と「有向グラフ」の思考法

まずは、私たちが普段MS Projectで組んでいるWBSと依存関係(先行タスク)を、コンピュータがどう見ているかを整理しましょう。

プログラミングの世界では、このような「点とそれをつなぐ線」の関係を「グラフ(Graph)」と呼びます。

  • 頂点(ノード):1つひとつの「タスク」
  • 矢印(有向エッジ):タスク間の「依存関係」(例:タスクA → タスクB)

正しいスケジュール構造は、矢印を辿っていっても絶対に元の場所に戻ってこない構造をしています。これを専門用語で DAG(Directed Acyclic Graph:有向非巡回グラフ) と呼びます。

【正常な構造(DAG)】
[タスクA] ───> [タスクB] ───> [タスクC] ───> [タスクD] (完了!)

【異常な構造(循環参照 / デッドロック)】
[タスクA] ───> [タスクB] ───> [タスクC]
^ │
└──────────────────────────────┘ (無限ループ!)

循環参照が発生している状態とは、グラフの中に「輪(サイクル)」ができてしまっている状態です。
マクロでタスクの依存関係を自動設定する際は、この「輪」が生まれないかをプログラム側で事前チェック(検知)してあげることが、極めて堅牢な自動化ツールを作る秘訣なのです。

2. アルゴリズムの真髄:深さ優先探索(DFS)と「3つの色」

では、コードを書く前に、どうやってプログラムでこの「輪」を見つけるのか、そのロジック(アルゴリズム)を解説します。

使う手法は「深さ優先探索(Depth-First Search: DFS)」「再帰処理(Recursion)」の組み合わせです。

それぞれのタスク(ノード)に、捜索状態を表す「3つの色(ステート)」を塗っていくイメージを持ってください。

1. 白(未訪問): まだ一度もチェックしていないタスク
2. 灰(訪問中): 現在、依存関係のルートを探索中のタスク(スタックに入っている状態)
3. 黒(訪問完了): そのタスクから先には循環が存在しないと確認できたタスク

循環を検知するルール

タスクを1つずつ辿って「灰色のタスク」を順番に塗っていきます。
探索を進める中で、「次に進もうとした先が、すでに『灰色』だった場合」……おめでとうございます(?)、循環参照を発見しました!

自分が今通過している最中のルート(灰色)に戻ってきたということは、そこに「輪」が存在することを意味するからです。

3. 実践!循環参照を検知・可視化するProject VBAコード

お待たせしました。実際の開発現場でそのまま使える、極限まで磨き上げたVBAコードをお見せします。

MS ProjectのVBAエディタ(Alt + F11)を開き、標準モジュールを作成して以下のコードを貼り付けてみてください。

Option Explicit

‘ ==============================================================================
‘ 【モジュール名】 Mod_CycleDetector
‘ 【概要】 MS Projectにおけるタスク依存関係の循環参照(デッドロック)を検出・可視化
‘ ==============================================================================

‘ タスクの探索状態を管理する列挙型
Private Enum TaskState
Unvisited = 0 ‘ 白:未訪問
Visiting = 1 ‘ 灰色:訪問中(現在の探索パス上に存在)
Visited = 2 ‘ 黒:訪問完了(安全確認済み)
End Enum

‘ 循環参照の検出結果を保持する構造体
Private Type CycleResult
HasCycle As Boolean
CyclePath As String
End Type

”’

”’ メイン処理:アクティブプロジェクト内の全タスクをスキャンし循環参照を検査します
”’

Public Sub DetectProjectCycles()
Dim prj As Project
Set prj = ActiveProject

If prj.Tasks.Count = 0 Then
MsgBox “プロジェクトにタスクが存在しません。”, vbInformation, “処理中止”
Exit Sub
End If

‘ 全タスクの状態を記録する辞書(Key: Task.UniqueID, Value: TaskState)
Dim nodeStates As Object
Set nodeStates = CreateObject(“Scripting.Dictionary”)

‘ 探索パスを記憶するコレクション
Dim currentPath As New Collection

Dim t As Task
Dim result As CycleResult
Dim cycleFound As Boolean: cycleFound = False

Debug.Print “————————————————–”
Debug.Print ” 依存関係の循環参照チェックを開始します… ”
Debug.Print “————————————————–”

‘ プロジェクト内の全タスクをループ(未訪問のものから探索開始)
For Each t In prj.Tasks
‘ 非表示・空行のタスクを除外
If Not (t Is Nothing) Then
Dim uID As Long
uID = t.UniqueID

‘ まだ訪問していないタスクであれば探索実行
If Not nodeStates.Exists(uID) OrElse nodeStates(uID) = TaskState.Unvisited Then
result = TraverseDFS(t, nodeStates, currentPath)

If result.HasCycle Then
cycleFound = True
Debug.Print “【警告】循環参照を検出しました!”
Debug.Print ” 検出ルート: ” & result.CyclePath
Debug.Print “————————————————–”
End If
End If
End If
Next t

If cycleFound Then
MsgBox “依存関係に論理的な矛盾(循環参照)が見つかりました。” & vbCrLf & _
“詳細はイミディエイトウィンドウを確認してください。”, vbCritical, “デッドロック検出”
Else
MsgBox “循環参照は検出されませんでした。依存関係は正常(DAG)です。”, vbInformation, “チェック完了”
End If

‘ ライフサイクル管理:オブジェクトの解放
Set nodeStates = Nothing
Set currentPath = Nothing
End Sub

”’

”’ 深さ優先探索(DFS)を行う再帰関数
”’

”’ 現在検査中のタスク ”’ 全タスクの状態辞書 ”’ 現在の探索ルート(スタック) Private Function TraverseDFS( _
ByRef currentTask As Task, _
ByRef states As Object, _
ByRef path As Collection _
) As CycleResult

Dim res As CycleResult
res.HasCycle = False
res.CyclePath = “”

Dim uID As Long
uID = currentTask.UniqueID

‘ 1. 現在のノードを「訪問中(灰色)」にセットし、パスに追加
states(uID) = TaskState.Visiting
path.Add currentTask.Name & ” (ID:” & currentTask.ID & “)”

‘ 2. 先行タスク(PredecessorTasks)を辿る
‘ ※ 後続タスク(SuccessorTasks)を辿る形にシフトすることも可能
Dim predTask As Task
For Each predTask In currentTask.PredecessorTasks
If Not (predTask Is Nothing) Then
Dim predUID As Long
predUID = predTask.UniqueID

‘ 先行タスクの状態を取得
Dim predState As TaskState
If states.Exists(predUID) Then
predState = states(predUID)
Else
predState = TaskState.Unvisited
End If

‘ — 循環検知の核心部 —
If predState = TaskState.Visiting Then
‘ 探索中のタスクに再び到達した = 循環が発生!
res.HasCycle = True
res.CyclePath = BuildCyclePathString(path, predTask.Name & ” (ID:” & predTask.ID & “)”)

‘ バックトラック処理を行って復帰
path.Remove path.Count
states(uID) = TaskState.Visited
TraverseDFS = res
Exit Function

ElseIf predState = TaskState.Unvisited Then
‘ 未訪問なら深さ優先で再帰呼び出し
res = TraverseDFS(predTask, states, path)
If res.HasCycle Then
‘ 戻り値で循環が報告されたらそのまま上位へ抜ける
path.Remove path.Count
states(uID) = TaskState.Visited
TraverseDFS = res
Exit Function
End If
End If
End If
Next predTask

‘ 3. 行き止まり、または全先行タスクの安全が確認できたら「訪問完了(黒)」へ移行
states(uID) = TaskState.Visited
‘ スタックから自分を取り除く(バックトラック)
path.Remove path.Count

TraverseDFS = res
End Function

”’

”’ 循環が検出された際のログ用文字列(A -> B -> C -> A)を生成します
”’

Private Function BuildCyclePathString(ByRef path As Collection, ByRef targetTaskName As String) As String
Dim strPath As String
Dim i As Long

For i = 1 To path.Count
strPath = strPath & “[” & path(i) & “] -> ”
Next i

strPath = strPath & “[” & targetTaskName & ” (始点に戻る)]”
BuildCyclePathString = strPath
End Function

4. コードの解説と知的な先輩からのワンポイントアドバイス

このコードは一見複雑に見えるかもしれませんが、プログラミングの本質が凝縮されています。ポイントを噛み砕いて解説しますね。

① `UniqueID` をキーにする理由

MS Projectでは、タスクの並び替えや挿入によって「`Task.ID`(行番号)」がコロコロ変わります。そのため、プログラム内でタスクを絶対的に特定するキーとしては、不変の識別子である `Task.UniqueID` を使うのが鉄則です。

② 再帰処理(Recursion)と「バックトラック」

`TraverseDFS` 関数の中で、自分自身である `TraverseDFS` を呼び出していますね。これが「再帰」です。

タスクの依存先をどこまでも深く潜っていき、行き止まりになったら `path.Remove path.Count` で一つ手前の分岐まで戻ります(これをバックトラックと呼びます)。
この「進んで、戻る」という挙動を、コレクション(`path`)をスタックとして使うことで綺麗に表現しているのです。

③ コレクションと Dictionary のメモリ管理

プロ仕様のVBAを書く上で極めて重要なのがメモリのライフサイクル管理です。
処理の最後で `Set nodeStates = Nothing` や `Set currentPath = Nothing` を行い、明示的にオブジェクトを破棄しています。こうした細やかな気配りが、大量のデータを扱ってもエクセルやMS Projectをクラッシュさせない「プロのコード」と「素人のコード」を分ける境目になります。

5. 初心者が陥りがちな「落とし穴」とパフォーマンス最適化

WBSをプログラミングで自動構築する際、知っておくべきハマりポイントが2つあります。

1. `Task.Predecessors` 文字列の直接操作は危険!

VBAから依存関係を設定する際、`Task.Predecessors = “1,2”` のように文字列で強引に代入する手法がよく紹介されています。
しかし、これを循環参照が発生する状態(例: タスク1の先行に「3」を入れる際、すでに3の先行に1がある場合)で行うと、MS Projectが即座に実行時エラーを発生させてマクロが強制終了します。

だからこそ、依存関係の文字列をセットする前に、今回紹介したようなロジックで「この矢印を追加してもグラフはDAGを保てるか?」を判定する「防御的プログラミング」が威力を発揮するのです。

2. 数千タスク規模での探索速度

全タスクに対して探索を行うと重くなるのでは?と心配されるかもしれません。
しかし、このアルゴリズム(DFS+3色管理)の計算量は $O(V + E)$($V$: タスク数, $E$: 依存関係の数)という極めて軽量なものです。一度「黒(訪問完了)」になったタスクは二度と深く探索しないため、数千行のWBSであっても一瞬でスキャンが完了します。

6. まとめ:グラフ理論を味方につけて、ワンランク上のエンジニアへ

お疲れ様でした!今回は少し高度な「グラフ理論」と「再帰探索」を使って、Project VBAにおける循環参照のデッドロックを未然に防ぐプロの手法を解説しました。

今回のポイントを復習しましょう。

1. タスクの依存関係は「有向グラフ」として捉える
2. 正常なスケジュールは「循環のないグラフ(DAG)」であるべき
3. 深さ優先探索(DFS)と「3つの状態(色)」で循環を瞬時に検知できる
4. `UniqueID` による確実なタスク同定と、オブジェクト解放の徹底

「マクロの記録」で作るコードから一歩踏み出し、こうしたアルゴリズム的思考をVBAに取り入れることができるようになれば、あなたはもう立派な高度自動化エンジニアです。

複雑に絡み合ったWBSも、構造を正しく見極めれば必ずロジックで解き明かすことができます。
ここをクリアすれば、Project VBAの基本はバッチリですよ!

自信を持って、さらに洗練されたコードを書いていってくださいね。応援しています!

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