WBSの断絶を防げ:タスク階層移動時における依存関係の整合性自動修復アーキテクチャ
VBAによる大規模なプロジェクト管理・WBS(Work Breakdown Structure)自動化において、最もエンジニアを絶望させる瞬間はどこか。それは、ユーザーがUI上で気軽に行った「タスクの昇格(インデント上げ)/降格(インデント下げ)」によって、背後に張り巡らされた数千件もの依存関係(先行・後続タスク)のグラフ構造が音を立てて崩壊し、クリティカルパスの計算が狂い、プロジェクト全体が致命的な論理破綻を起こした瞬間である。
GUIの操作は軽快であっても、その裏側にあるデータ構造の整合性担保は、VBAエンジニアが自らの手で極限まで厳密に実装しなければならない。Excelのセル操作や不完全なオブジェクト参照に依存したコードは、大規模データの前には無力と化す。
今回は、Project VBA(およびExcel VBAベースの独自WBSエンジン)における、タスクの階層変更時の依存関係の整合性チェックと自動修復(Self-Healing Dependency Graph)の極限の知見を公開する。
—
1. 階層構造変更における「論理破綻」の本質
WBSにおけるタスクの階層化は、単なる「見た目のインデント」ではない。それは「親タスクは子タスクの進捗・工期の集約地点である」という強力な因果律の定義である。
ここにタスクの依存関係(Predecessors / Successors)が絡むとき、以下の致命的な矛盾が発生する。
1. カプセル化違反の矛盾:
親タスクAに依存していた外部タスクXが、子タスクBが「降格」したことによって親子関係のスコープ外に置かれ、依存関係のアンカーが宙ぶらりんになる。
2. 自己参照・循環参照(Circular Dependency):
タスクの昇格により、元・子タスクが元・親タスクと同一階層、あるいは上位階層に移動した結果、子から親へ、親から子への無限ループ依存が生成される。
3. サマリータスクへの直接依存:
PMBOKの原則として、サマリータスク(子を持つ親タスク)に直接依存関係を設定することは御法度である。階層変更の結果、サマリータスクに依存関係が取り残された場合、スケジュールエンジンの計算がデッドロックする。
これらの問題に対し、イベントドリブンで介入し、トランザクション的かつ自動的に修復するエンジンを構築する。
—
2. アーキテクチャ設計:メモリ最適化とオブジェクトライフサイクル
数万行のWBSデータを扱う現場において、`Range`オブジェクトの都度参照や、不適切なVariant型の多用は、ガベージコレクションのオーバーヘッドを激増させ、パフォーマンスを致命的に劣化させる。
極限のパフォーマンスを発揮するため、以下の設計思想をコードに落とし込む。
- 二次元配列への一括ロード: セルへのアクセスを最小限にし、メモリ上の配列(`Variant`型)上でグラフ探索と検証を行う。
- 強固なオブジェクトの明示的解放: COMオブジェクトを操作する際は、スコープ終了時に確実に`Set xxx = Nothing`を実行し、Excelのメモリリークを根絶する。
- トランザクション的アプローチ: 検証に失敗した場合、メモリ上の変更をコミットせず、データ不整合を未然に防ぐ。
—
3. 実装コード:依存関係の整合性自動修復エンジン
以下に、タスクの昇格・降格イベントをフックし、依存関係の整合性を検証・自動修復するコアクラスモジュールの実装を示す。
クラスモジュール名: `WBSDependencyManager`
Option Explicit
‘ =========================================================================
‘ クラス名: WBSDependencyManager
‘ 概要: WBSの階層変更時における依存関係の整合性検証および自動修復エンジン
‘ =========================================================================
‘ データ構造定義(配列のインデックス定数)
Private Const COL_ID As Long = 1
Private Const COL_NAME As Long = 2
Private Const COL_LEVEL As Long = 3
Private Const COL_PRED As Long = 4 ‘ 先行タスクID (カンマ区切り)
Private Const COL_SUCC As Long = 5 ‘ 後続タスクID
Private m_WorkSheet As Worksheet
‘ 初期化
Public Sub Initialize(ByRef targetWs As Worksheet)
Set m_WorkSheet = targetWs
End Sub
‘ ————————————————————————-
‘ メインメソッド: タスク階層変更時の整合性チェックと自動修復
‘ ————————————————————————-
Public Function ValidateAndRepairHierarchy(ByVal targetRow As Long, ByVal isPromotion As Boolean) As Boolean
Dim rawData As Variant
Dim lastRow As Long
On Error GoTo ErrorHandler
‘ 1. データのメモリ上への一括ロード(パフォーマンスの極限最適化)
lastRow = m_WorkSheet.Cells(m_WorkSheet.Rows.Count, COL_ID).End(xlUp).Row
If lastRow < 2 Then Exit Function
rawData = m_WorkSheet.Range(m_WorkSheet.Cells(2, 1), m_WorkSheet.Cells(lastRow, 5)).Value
' 2. 該当タスクの階層レベルを仮変更
Dim currentLevel As Long
Dim targetID As String
targetID = CStr(rawData(targetRow - 1, COL_ID))
currentLevel = rawData(targetRow - 1, COL_LEVEL)
If isPromotion Then
If currentLevel > 1 Then currentLevel = currentLevel – 1
Else
currentLevel = currentLevel + 1
End If
rawData(targetRow – 1, COL_LEVEL) = currentLevel
‘ 3. 循環参照およびサマリータスク制約の検証と修復
Call RepairCircularDependencies(rawData, targetRow – 1)
Call RepairSummaryTaskDependencies(rawData, targetRow – 1)
‘ 4. 修復結果をシートへ一括書き戻し
m_WorkSheet.Range(m_WorkSheet.Cells(2, 1), m_WorkSheet.Cells(lastRow, 5)).Value = rawData
ValidateAndRepairHierarchy = True
Exit Function
ErrorHandler:
‘ 致命的エラー時のログ出力やロールバック処理をここに記述
ValidateAndRepairHierarchy = False
MsgBox “依存関係の修復中にエラーが発生しました: ” & Err.Description, vbCritical
End Function
‘ ————————————————————————-
‘ 循環参照の検知と修復
‘ ————————————————————————-
Private Sub RepairCircularDependencies(ByRef data As Variant, ByVal rowIndex As Long)
Dim targetID As String
Dim predStr As String
Dim preds() As String
Dim i As Long, j As Long
targetID = CStr(data(rowIndex, COL_ID))
predStr = CStr(data(rowIndex, COL_PRED))
If predStr = “” Or predStr = “0” Then Exit Sub
preds = Split(predStr, “,”)
Dim cleanedPreds As String: cleanedPreds = “”
For i = LBound(preds) To UBound(preds)
Dim pID As String
pID = Trim(preds(i))
‘ 自己参照の排除
If pID <> targetID Then
‘ 階層構造上の論理矛盾(子から親への不正な依存など)をチェック
If Not HasHierarchyConflict(data, targetID, pID) Then
If cleanedPreds = “” Then
cleanedPreds = pID
Else
cleanedPreds = cleanedPreds & “,” & pID
End If
End If
End If
Next i
data(rowIndex, COL_PRED) = cleanedPreds
End Sub
‘ ————————————————————————-
‘ サマリータスク(子を持つタスク)への依存関係の自動格上げ修復
‘ ————————————————————————-
Private Sub RepairSummaryTaskDependencies(ByRef data As Variant, ByVal rowIndex As Long)
Dim targetID As String
Dim targetLevel As Long
Dim i As Long
targetID = CStr(data(rowIndex, COL_ID))
targetLevel = data(rowIndex, COL_LEVEL)
‘ 自身が「親(サマリー)」になった場合、他のタスクから直接依存されてはならない
‘ -> 依存を末端の子タスクに委譲するなどの高度な修復ロジック
Dim isSummary As Boolean
isSummary = False
If rowIndex < UBound(data, 1) Then
If data(rowIndex + 1, COL_LEVEL) > targetLevel Then
isSummary = True
End If
End If
If isSummary Then
‘ サマリータスクに対する先行・後続関係のパージまたは子タスクへの移譲
For i = LBound(data, 1) To UBound(data, 1)
‘ 他のタスクの先行タスクにこのサマリーIDが含まれている場合、最初の末端子タスクに付け替える
Dim pred As String
pred = CStr(data(i, COL_PRED))
If InStr(1, pred, targetID) > 0 Then
‘ 簡易的にサマリーへの直接依存を削除(実務では子タスクへのルーティングを推奨)
data(i, COL_PRED) = Replace(pred, targetID, “”)
End If
Next i
End If
End Sub
‘ ————————————————————————-
‘ 階層上の論理矛盾判定
‘ ————————————————————————-
Private Function HasHierarchyConflict(ByRef data As Variant, ByVal myID As String, ByVal predID As String) As Boolean
‘ 簡易的な循環・矛盾パスの検知スタブ
‘ 実運用では有向グラフのトポロジカルソートアルゴリズムをここに実装する
HasHierarchyConflict = False
End Function
‘ クラス終了時の明示的解放
Private Sub Class_Terminate()
Set m_WorkSheet = Nothing
End Sub
—
4. レガシー環境・システム間連携における実践的注意点
1. Excel / Project (MSProject) 間の非同期遅延対策:
COMオブジェクトを通じてMicrosoft ProjectとExcel間を連携させる場合、`Application.Calculate` やタスクの再計算キューが完全に消化される前に次の処理が走ると、依存関係IDの不一致(Ghost ID)が発生する。トランザクションの前後には必ず明示的な再計算と同期を挟むこと。
2. メモリの明示的解放の徹底:
VBAは参照カウンタ方式のガベージコレクションを採用しているため、巨大な配列やワークシートオブジェクトをローカル変数として保持し続けたままプロシージャを抜けると、Excelのプロセス内にメモリ断片が残る。`Class_Terminate`を活用したカプセル化された解放フローを徹底せよ。
総括
WBSの階層変更に伴う依存関係の自動修復は、単なる「エラー回避の小細工」ではない。それはプロジェクト管理システムの信頼性を担保する最後の防壁である。
オブジェクトのライフサイクルを完全に制御し、メモリ上で高速にグラフ構造を再構築するこのアプリケーション・アーキテクチャこそが、現場の混乱を未然に防ぐ唯一の解である。
