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

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

制約条件の「破壊的変更」を未然に防げ:Project VBAによるスケジュール影響シミュレーションの極意

プロジェクトマネジメントにおける「制約条件(Constraint)」の変更は、一見すると単なるプロパティの書き換えに過ぎない。しかし、MS Projectの計算エンジンというブラックボックスに対して、`Must Finish On` や `Start No Earlier Than` といった制約を投げ込む行為は、ネットワークロジック全体を激震させる「破壊的変更」だ。

多くのジュニアエンジニアは、ユーザーの要求通りに `Task.ConstraintType` を書き換えるコードを書く。だが、プロフェッショナルは違う。彼らは「その変更がプロジェクト全体の終了日に何日間の遅延をもたらすか」を、実際のデータを破壊することなく事前に予見する。

本記事では、MS Projectの計算エンジンを制御し、制約変更の影響を安全にシミュレートするための、堅牢な設計思想とプロダクションレベルのコードを伝授する。

1. なぜ「書き換え」を直接行ってはいけないのか

MS Projectのオブジェクトモデルにおいて、タスクの制約を変更した瞬間、計算エンジン(Calculation Engine)は連鎖的な再計算を開始する。

もし、シミュレーションのつもりで制約を変更し、その結果がプロジェクトにとって致命的な遅延をもたらすものだった場合、あるいは計算中にエラーが発生して処理が中断した場合、ユーザーのプロジェクトデータは「中途半端に書き換わった壊れた状態」になる。

これは業務ツールとして致命的な欠陥だ。我々が目指すべきは、「状態の保存(Snapshot)」「変更の適用(Mutation)」「影響の測定(Measurement)」「状態の復元(Rollback)」という、アトミック(不可分)なトランザクション処理である。

2. アーキテクチャ設計:Snapshot & Rollback パターン

シミュレーション機能を実装する際の設計指針は、以下のステップに集約される。

1. State Capture(状態捕捉): 対象タスクの現在の `ConstraintType` と `ConstraintDate` をメモリ上に退避する。
2. Calculation Control(計算制御): 再計算のタイミングを制御するため、計算モードを一時的に手動(Manual)に切り替える。
3. Mutation(変異): ターゲットとなる制約を適用する。
4. Force Recalculate(強制再計算): `Project.Calculate` を呼び出し、変更による波及効果をエンジンに計算させる。
5. Impact Analysis(影響分析): プロジェクト全体の終了日(`Project.Finish`)の変化を比較する。
6. Rollback(復元): ここが最も重要だ。 測定が終わったら、必ず手順1で保存した元の値に戻し、計算モードを自動に戻す。

3. プロダクション・コード:影響シミュレーターの実装

以下に、そのまま実務に投入可能なレベルの堅牢なコードを示す。単なる「動くコード」ではなく、エラーハンドリングと状態復元を徹底した設計にしている。

Option Explicit

' --- 列挙型による結果定義(保守性を高めるため) ---
Public Enum SimulationResultStatus
    srSuccess = 0
    srError = 1
    srTaskNotFound = 2
End Enum

' --- シミュレーション結果を格納する構造体 ---
Public Type SimulationResult
    Status As SimulationResultStatus
    OriginalFinish As Date
    SimulatedFinish As Date
    DelayDays As Long
    ErrorMessage As String
End Type

''' <summary>
''' 特定のタスクの制約変更がプロジェクト終了日に与える影響をシミュレーションする
''' </summary>
''' <param name="taskId">対象タスクのID</param>
''' <param name="newType">新しい制約タイプ (pjTaskConstraintType等)</param>
''' <param name="newDate">新しい制約日付</param>
Public Function SimulateConstraintImpact(ByVal taskId As Long, _
                                         ByVal newType As Long, _
                                         ByVal newDate As Date) As SimulationResult
    
    Dim res As SimulationResult
    Dim targetTask As Task
    Dim originalType As Long
    Dim originalDate As Date
    Dim originalProjectFinish As Date
    Dim currentCalcMode As Long
    
    ' 1. 初期化とエラーハンドリングの準備
    On Error GoTo ErrorHandler
    
    ' タスクの存在確認
    On Error Resume Next
    Set targetTask = ActiveProject.Tasks(taskId)
    On Error GoTo ErrorHandler
    
    If targetTask Is Nothing Then
        res.Status = srTaskNotFound
        Exit Function
    End If

    ' 2. Snapshot: 現在の状態をメモリに退避
    originalType = targetTask.ConstraintType
    originalDate = targetTask.ConstraintDate
    originalProjectFinish = ActiveProject.Finish
    currentCalcMode = ActiveProject.Calculation ' 現在の計算モードを保存
    
    ' 計算モードを「手動」に設定(意図しない連鎖計算を抑制)
    ActiveProject.Calculation = pjManual

    ' 3. Mutation: 制約の変更適用
    targetTask.ConstraintType = newType
    targetTask.ConstraintDate = newDate
    
    ' 4. Force Recalculate: エンジンに計算させる
    ActiveProject.Calculate
    
    ' 5. Impact Analysis: 影響の測定
    res.OriginalFinish = originalProjectFinish
    res.SimulatedFinish = ActiveProject.Finish
    res.DelayDays = DateDiff("d", originalProjectFinish, ActiveProject.Finish)
    res.Status = srSuccess

CleanUp:
    ' 6. Rollback: 状態を完全に元に戻す(重要!)
    ' エラーが発生しても、必ずここを通って元の状態へ復元させる
    On Error Resume Next
    If Not targetTask Is Nothing Then
        targetTask.ConstraintType = originalType
        targetTask.ConstraintDate = originalDate
    End If
    ActiveProject.Calculation = currentCalcMode
    ActiveProject.Calculate ' 復元後の整合性を確保
    Exit Function

ErrorHandler:
    res.Status = srError
    res.ErrorMessage = Err.Description
    Resume CleanUp
End Function

''' <summary>
''' 実行テスト用メインルーチン
''' </summary>
Sub TestSimulation()
    Dim simRes As SimulationResult
    Dim targetID As Long: targetID = 5 ' テスト用のタスクID
    
    ' 例: タスク5を「2023/12/31に必ず終了(Must Finish On)」に変更した場合のシミュレーション
    simRes = SimulateConstraintImpact(targetID, pjMustFinishOn, DateValue("2024/12/31"))
    
    If simRes.Status = srSuccess Then
        If simRes.DelayDays > 0 Then
            MsgBox "【警告】制約変更によりプロジェクトが " & simRes.DelayDays & " 日遅延します!" & vbCrLf & _
                   "現在: " & simRes.OriginalFinish & vbCrLf & _
                   "予測: " & simRes.SimulatedFinish, vbExclamation, "シミュレーション結果"
        Else
            MsgBox "制約変更の影響はありません。プロジェクト終了日に変更はありません。", vbInformation, "シミュレーション結果"
        End If
    Else
        MsgBox "シミュレーション中にエラーが発生しました: " & simRes.ErrorMessage, vbCritical
    End If
End Sub

4. エンジニアが陥る「設計の罠」と回避策

計算モードの制御を怠るな

`ActiveProject.Calculation = pjManual` を設定せずに制約を変更すると、プロパティを書き換えるたびに大規模な再計算が走り、コードのパフォーマンスが劇的に低下する。また、計算途中の不安定な状態で値を読み取ってしまうリスクがある。シミュレーション時は、「変更 → 一括計算 → 測定」というサイクルを厳守せよ。

データベース・外部連携時の整合性

もしこのシミュレーション結果を外部データベース(SQL ServerやAccess)に保存する場合、「シミュレーション結果の保存」と「プロジェクトの状態復元」は分離して考えるべきだ。
不完全な状態(Rollbackに失敗した状態)でDBにログを書き込もうとすると、システム全体が「不整合なデータ」を正解として認識してしまう。必ず `CleanUp` セクションで復元が完了したことを確認してから、次のロジックへ進むこと。

クリティカルパスの動的変化への考慮

本コードでは「終了日」に焦点を当てているが、実務では「クリティカルパスのタスク自体が変わる」ことも多い。より高度なシミュレーションを行うなら、`Task.Critical` プロパティの集合(Collection)を比較対象に加える設計へと拡張せよ。

結論

Project VBAを用いた自動化ツールにおいて、最も価値があるのは「単に作業を速くすること」ではない。「ユーザーが取り返しのつかないミスを犯す前に、論理的な根拠をもって警告を与えること」だ。

Snapshot、Mutation、Rollback。このトランザクションの原則を脳に刻み込め。それが、単なる「スクリプター」と「アーキテクト」を分かつ境界線である。

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