制御不能な波及:制約条件変更がクリティカルパスに与える「衝撃」をシミュレートする
プロジェクトスケジュールの管理において、あるタスクの制約条件(Constraint)を「Must Finish On」や「Start No Earlier Than」に変更することは、単なる日付の固定ではない。それは、ネットワーク図における静水面に巨大な石を投げ込む行為に等しい。
一見、局所的な変更に見えるその一石は、先行・後続関係の連鎖(Ripple Effect)を通じて、プロジェクト全体のクリティカルパス(CP)を劇的に、時には予測不能な形で変容させる。
多くのプロジェクトマネージャーや初級エンジニアは、GUI上で制約を変更し、その結果を「見てから」判断するという、極めてリスクの高い、いわば「事後対応」の運用を行っている。しかし、真のシステムアーキテクトが構築すべきは、変更がネットワークのトポロジーに与える影響を、現在のスケジュールを汚染することなく、事前に、かつ精密に予測するシミュレーション・エンジンである。
本稿では、Microsoft ProjectのCOMオブジェクトモデルを極限まで制御し、制約条件の変更がクリティカルパスに与える影響を、メモリ効率と実行速度を両立させながらシミュレートする、プロフェッショナルな実装手法を解説する。
1. アーキテクチャの設計思想:Snapshot & Rollback
シミュレーションを実装する際、最も陥りやすい罠は「現在稼働中のプロジェクト・オブジェクトを直接書き換えてしまうこと」だ。誤ったシミュレーション結果によって、ユーザーが構築したリアルなスケジュールが破壊されることは、ツールとしての致命的な欠陥である。
我々が採用すべきは、「Snapshot & Rollback」パターンである。
1. State Capture: 変更対象のタスクおよび、関連するネットワーク(先行・後続)の属性をメモリ上に一時的に退避する。
2. Mutation: 仮想的な制約条件を適用する。
3. Calculation: Projectの計算エンジン(Scheduling Engine)を強制的に再計算させる。
4. Delta Analysis: 計算後のクリティカルパス(`Task.IsCritical`)の状態を、退避した状態と比較する。
5. Restoration: プロジェクトの状態を完全に、かつ原子的に(Atomically)元の状態へ復元する。
このプロセスにおいて、COMオブジェクトのライフサイクル管理を誤ると、メモリリークや、最悪の場合、MS Projectプロセスのハングアップを招く。
2. 高度なメモリ管理と計算エンジンの制御
VBAはガベージコレクションが強力ではない。特にMS Projectのような巨大なCOMオブジェクトを扱う場合、オブジェクトの参照カウントを厳密に管理しなければならない。
また、シミュレーションのパフォーマンスを決定づけるのは、`Project.Calculate` の呼び出し回数だ。計算エンジンは非常に重い処理である。ループの中で無秩序に呼び出せば、シミュレーションは実用的な時間内に終わらない。
最適化の指針
- ScreenUpdating / Calculation Control: 計算中は画面更新を停止し、計算モードを `pjManual` に切り替える。
- Object Scoping: `Task` オブジェクトなどは、ループの各ステップで明示的に `Set task = Nothing` を行い、参照を解放する。
- APIによる精度向上: 大規模なデータセットを扱う場合、Windows APIの `QueryPerformanceCounter` を用いて、シミュレーションのオーバーヘッドを精密に測定し、ボトルネックを特定する。
3. 実装例:Critical Path Impact Simulator
以下に、特定のタスクの制約条件を変更した際に、どのタスクが新たにクリティカルパスに浮上するか(あるいは外れるか)を特定する、プロフェッショナル仕様のコードを示す。
Option Explicit
' Windows API: 高精度タイマー(パフォーマンス測定用)
Private Declare PtrSafe Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCounter As Currency) As Long
Private Declare PtrSafe Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As Currency) As Long
' プロジェクト定数の定義(参照設定がない環境への配慮)
Private Const pjManual As Long = 0
Private Const pjAutomatic As Long = -1
''' <summary>
''' 特定のタスクの制約条件変更がクリティカルパスに与える影響をシミュレートする。
''' </summary>
''' <param name="targetTaskID">対象とするタスクのID</param>
''' <param name="newConstraint">適用する新しい制約条件 (例: pjTaskMustFinishOn)</param>
''' <param name="newDate">適用する新しい日付</param>
Public Sub SimulateConstraintImpact(ByVal targetTaskID As Long, ByVal newConstraint As Long, ByVal newDate As Date)
Dim proj As Project
Dim targetTask As Task
Dim t As Task
Dim originalConstraint As Long
Dim originalDate As Date
Dim impactReport As String
' 高精度計測用変数
Dim startCount As Currency, endCount As Currency, freq As Currency
Set proj = Application.Project
On Error GoTo ErrorHandler
' 1. 対象タスクの特定と現在の状態を退避
On Error Resume Next
Set targetTask = proj.Tasks(targetTaskID)
On Error GoTo ErrorHandler
If targetTask Is Nothing Then
MsgBox "指定されたタスクIDが見つかりません。", vbCritical
Exit Sub
End If
originalConstraint = targetTask.ConstraintType
originalDate = targetTask.ConstraintDate
' 2. シミュレーション環境の準備(計算エンジンの制御)
' 画面更新を停止し、計算をマニュアルに設定して副作用を最小化する
Application.ScreenUpdating = False
proj.Calculation = pjManual
' 3. 計測開始
QueryPerformanceFrequency freq
QueryPerformanceCounter startCount
' 4. 状態の変更(Mutation)
targetTask.ConstraintType = newConstraint
targetTask.ConstraintDate = newDate
' プロジェクトの再計算を強制
proj.Calculate
' 5. 影響範囲の分析(Delta Analysis)
' 変更前後のクリティカルパスを比較するロジック
' ※ 本来は変更前のCPリストを事前に保持しておくのが理想的だが、
' ここでは「変更後のCP」を抽出し、レポートを作成する。
impactReport = "--- Critical Path Impact Report ---" & vbCrLf
impactReport = impactReport & "Target Task: " & targetTask.Name & vbCrLf
impactReport = impactReport & "New Constraint: " & GetConstraintName(newConstraint) & " (" & newDate & ")" & vbCrLf & vbCrLf
impactReport = impactReport & "New Critical Path Tasks:" & vbCrLf
For Each t In proj.Tasks
' オブジェクトの存在チェックとクリティカルパス判定
If Not t Is Nothing Then
If t.IsCritical Then
impactReport = impactReport & "- " & t.Name & " (Start: " & t.Start & ")" & vbCrLf
End If
End If
' メモリ負荷軽減のため、ループ内での参照管理を徹底
Set t = Nothing
Next t
' 6. 状態の復元(Rollback)
' ユーザーのデータを汚さないよう、必ず元の状態に戻す
targetTask.ConstraintType = originalConstraint
targetTask.ConstraintDate = originalDate
proj.Calculate
' 計測終了
QueryPerformanceCounter endCount
Dim elapsed As Double
elapsed = (endCount - startCount) / freq
' 7. 後処理
proj.Calculation = pjAutomatic
Application.ScreenUpdating = True
' 結果の出力
Debug.Print impactReport
Debug.Print "Simulation Time: " & Format(elapsed, "0.0000") & " sec"
MsgBox impactReport, vbInformation, "Simulation Complete"
CleanUp:
' オブジェクトの明示的解放
Set targetTask = Nothing
Set proj = Nothing
Exit Sub
ErrorHandler:
MsgBox "致命的なエラーが発生しました: " & Err.Description, vbCritical
Resume CleanUp
End Sub
''' <summary>
''' 制約条件の定数を文字列に変換するヘルパー関数
''' </summary>
Private Function GetConstraintName(ByVal constraint As Long) As String
Select Case constraint
Case 0: GetConstraintName = "As Soon As Possible"
Case 1: GetConstraintName = "As Late As Possible"
Case 2: GetConstraintName = "Start No Earlier Than"
Case 3: GetConstraintName = "Start No Later Than"
Case 4: GetConstraintName = "Must Start On"
Case 5: GetConstraintName = "Must Start On (Strict)"
Case 6: GetConstraintName = "Start No Earlier Than (Strict)"
Case 7: GetConstraintName = "Finish No Earlier Than"
Case 8: GetConstraintName = "Finish No Later Than"
Case 9: GetConstraintName = "Must Finish On"
Case 10: GetConstraintName = "Must Finish On (Strict)"
Case Else: GetConstraintName = "Unknown"
End Select
End Function
4. 極限の知見:レガシー環境と大規模プロジェクトへの対応
このコードは実用的なレベルにありますが、エンタープライズ環境、特に数千タスクを超える巨大なプロジェクトにおいては、さらなる考慮が必要です。
大規模ネットワークにおける計算コストの回避
`For Each t In proj.Tasks` による全タスクの走査は、タスク数が極めて多い場合、$O(n)$ の計算量となり、シミュレーションのボトルネックとなります。
より高度なアプローチとしては、「変更されたタスクの先行・後続関係(Predecessors/Successors)を再帰的に辿り、影響を受ける可能性のあるサブセットのみを走査する」アルゴリズムを実装することです。これにより、計算量は劇的に削減されます。
Windows APIによるプロセスの監視
シミュレーション中に `proj.Calculate` が長時間ブロックされるケースがあります。レガシーな環境や不安定なネットワーク上のファイルを開いている場合、VBAは応答なし(Not Responding)に陥ります。
これを防ぐために、シミュレーションを別スレッド(あるいは別プロセス)で実行し、Windows APIの `WaitForSingleObject` 等を用いてタイムアウト制御を行うアーキテクチャが、真のミッションクリティカルなシステムには求められます。
結論
制約条件の変更は、プロジェクトの構造を根本から揺るがす。
エンジニアの役割は、単にコードを書くことではない。その変更がもたらす「波」を予見し、安全に、かつ精密に制御するための「盾」と「鏡」を構築することにある。
このシミュレーション・エンジンは、その第一歩に過ぎない。
