【テクニカル・上級編】実務中級:制約条件の変更がクリティカルパスに与える影響をVBAで事前シミュレーション – Project VBA解析バイブル

スポンサーリンク
スポンサーリンク

制約変更の破壊的影響を制御せよ:MS Projectにおける「What-If」シミュレーション・エンジンの設計思想

プロジェクト管理の現場において、制約条件(Constraints)の変更は、単なる日付の書き換えではない。それは、プロジェクトのネットワーク論理(Network Logic)に対する「劇薬」の投与である。

「ASAP(可能な限り早く)」から「Must Finish On(〇月〇日終了)」への変更、あるいは「ALAP(可能な限り遅く)」へのシフト。これらの一見些細な変更が、クリティカルパスを瞬時に再編し、プロジェクト全体の終了日を数週間単位で押し流すことは珍しくない。

シニアエンジニアが直面する真の課題は、この「破壊的な変更」を、本番のスケジュールの整合性を破壊することなく、いかに安全かつ高速に事前検証(What-If Simulation)するかにある。本稿では、MS ProjectのCOMオブジェクトモデルの挙動を深く理解し、メモリと計算負荷を極限まで制御したシミュレーション・エンジンの実装手法について詳述する。

1. 致命的な設計ミス:再計算(Recalculation)の暴走

多くの開発者が陥る罠がある。それは、ループ内で制約を変更し、その直後に `Task.Start` や `Task.Critical` を参照することだ。

MS Projectのスケジューリングエンジンは、制約条件が変更された瞬間にネットワーク全体の再計算をトリガーしようとする。ループ内でこれを繰り返すと、計算エンジンが指数関数的に呼び出され、プロセスの応答が停止する「計算の嵐(Calculation Storm)」が発生する。

アーキテクチャの原則

1. Calculation Modeの完全制御: シミュレーション中は `Application.Calculation = pjManual` を徹底し、計算タイミングをエンジニアが完全に支配すること。
2. Snapshot & Rollback: 変更前の状態を、COMオブジェクトではなく「軽量なメモリ上の構造体(UDT)」に退避させること。
3. Batch Update: 制約の変更を全て完了させた後に、一度だけ `Application.Calculate` を実行すること。

2. 高度なメモリ管理:COMオブジェクトとUDTの使い分け

大規模プロジェクト(タスク数 5,000件超)において、全タスクのオブジェクトを配列に格納してバックアップを取る行為は、メモリの断片化とレガシーなCOM参照カウントの不整合を招き、最終的に `Out of Memory` または MS Project 自体のクラッシュを引き起こす。

我々が取るべき戦略は、「オブジェクトの複製」ではなく「プロパティの抽出」である。

' --- Module: Types ---
' プロジェクトの状態を保持するための軽量なユーザー定義型 (UDT)
' COMオブジェクトへの参照を持たせないことが、メモリ安定化の鍵である
Public Type TaskSnapshot
    ID As Long
    Constraint As Long
    ConstraintDate As Date
    IsCritical As Boolean
    Start As Date
    Finish As Date
End Type

3. 実装:Constraint Impact Simulator

以下に、制約条件の変更がクリティカルパスとプロジェクト終了日に与える影響をシミュレートする、プロフェッショナル仕様のコアロジックを示す。

Option Explicit

' Windows API: プロセスの応答性を監視、あるいは強制終了制御が必要な場合に備える
' (本実装では、VBAの実行効率を優先するため、直接的なAPI呼び出しは最小限に留める)
Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)

''' <summary>
''' 制約条件変更のシミュレーションを実行するメインエンジン
''' </summary>
''' <param name="targetTaskID">変更対象のタスクID</param>
''' <param name="newConstraint">新しい制約条件 (pjTaskConstraintStart等)</param>
''' <param name="newDate">新しい制約日付</param>
Public Sub SimulateConstraintChange(ByVal targetTaskID As Long, _
                                   ByVal newConstraint As Long, _
                                   ByVal newDate As Date)
    
    Dim app As MSProject.Application
    Set app = MSProject.Application
    
    Dim proj As MSProject.Project
    Set proj = app.ActiveProject
    
    ' 1. 現在の状態をスナップショットとして取得
    Dim originalFinishDate As Date
    Dim originalCriticalTasks As Collection
    Set originalCriticalTasks = New Collection
    
    originalFinishDate = proj.FinishDate
    
    ' 現在のクリティカルパスをメモリ上に抽出 (オブジェクト参照ではなくIDのみを保持)
    Dim t As MSProject.Task
    For Each t In proj.Tasks
        If Not t Is Nothing Then
            If t.Critical Then
                originalCriticalTasks.Add t.ID
            End If
        End If
    Next t

    ' 2. シミュレーション環境の構築
    ' 計算モードをマニュアルに設定し、エンジンの暴走を防ぐ
    Dim prevCalcMode As Long
    prevCalcMode = app.Calculation
    app.Calculation = pjManual
    
    On Error GoTo ErrorHandler
    
    ' 3. 制約の変更(破壊的変更の実行)
    Dim targetTask As MSProject.Task
    Set targetTask = proj.Tasks(targetTaskID)
    
    ' 変更前の状態を一時保存(Rollback用)
    Dim oldConstraint As Long: oldConstraint = targetTask.Constraint
    Dim oldDate As Date: oldDate = targetTask.ConstraintDate
    
    targetTask.Constraint = newConstraint
    targetTask.ConstraintDate = newDate
    
    ' 4. 一括再計算の実行
    ' ここで初めて計算エンジンを動かす。これが最もコストの高い操作。
    app.Calculate
    
    ' 5. 影響の解析
    Dim newFinishDate As Date
    newFinishDate = proj.FinishDate
    
    Dim impactDays As Long
    impactDays = DateDiff("d", originalFinishDate, newFinishDate)
    
    ' 新しいクリティカルパスの特定
    Dim newCriticalCount As Long
    Dim currentT As MSProject.Task
    For Each currentT In proj.Tasks
        If Not currentT Is Nothing Then
            If currentT.Critical Then newCriticalCount = newCriticalCount + 1
        End If
    Next currentT

    ' 6. 結果の通知
    ' ユーザーへの通知は、計算が終わった後にまとめて行う
    MsgBox "【シミュレーション結果】" & vbCrLf & _
           "プロジェクト終了日の変動: " & IIf(impactDays > 0, "+", "") & impactDays & " 日" & vbCrLf & _
           "クリティカルパスのタスク数: " & newCriticalCount & " 件" & vbCrLf & _
           "※この変更を確定しますか? (Noを選択するとロールバックします)", vbQuestion + vbYesNo

    If MsgBox("変更を適用しますか?", vbYesNo) = vbNo Then
        ' ロールバック処理
        targetTask.Constraint = oldConstraint
        targetTask.ConstraintDate = oldDate
        app.Calculate
    End If

CleanUp:
    ' 環境の復元
    app.Calculation = prevCalcMode
    Set targetTask = Nothing
    Set proj = Nothing
    Set app = Nothing
    Exit Sub

ErrorHandler:
    MsgBox "Critical Error: " & Err.Description, vbCritical
    Resume CleanUp
End Sub

4. アーキテクトの視点:パフォーマンスと保守性の極致

このコードを実務レベルのシステムに組み込む際、以下の3点に留意せよ。

I. 計算コストの $O(N)$ 制御

上記のコードでは、クリティカルパスの特定にループを用いている。タスク数が数万件に及ぶ場合、`For Each t In proj.Tasks` 自体が重い。もし、頻繁にシミュレーションを行うシステムであれば、変更対象の前後関係(Predecessors/Successors)のみをスキャンする局所的影響解析(Local Impact Analysis)アルゴリズムを実装すべきだ。

II. レガシー環境への配慮

古いMS Project(2010/2013等)では、COMのメモリ解放が不完全な場合がある。`Set task = Nothing` は基本だが、大規模なループの途中でメモリ使用量が急増する場合は、定期的に `DoEvents` を挟み、OSに制御を戻してメッセージキューを処理させる必要がある。ただし、`DoEvents` は計算速度を著しく低下させるため、シミュレーションの「フェーズ」が変わるタイミング(例:スナップショット取得完了後)に限定して使用するのが定石だ。

III. システム間連携の拡張性

このロジックをVB.NETやC#の外部アプリケーションから呼び出す場合、MS Projectは「サーバー」として振る舞う。その際、`Application.Calculation = pjManual` を忘れると、外部からのプロパティ書き込みのたびにMS Projectプロセスがビジー状態となり、呼び出し元がタイムアウトする。API設計においては、「Stateful(状態保持型)な制御」を意識せよ。

結論

制約条件の変更は、プロジェクトの構造そのものを揺るがす行為である。エンジニアに求められるのは、単に「値を書き換えるコード」を書くことではない。計算エンジンの挙動を予測し、メモリのライフサイクルを支配し、ユーザーに対して「予測される未来」を正確に提示する、堅牢なシミュレーション・アーキテクチャを構築することである。

これこそが、レガシーな業務システムを、信頼に足るエンタープライズ・ツールへと昇華させる唯一の道である。

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