【テクニカル・上級編】プロ向け:Project.TaskConstraintChangeイベントのフックによる制約違反のリアルタイム監視 – Project VBA解析バイブル

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

Constraint Governance: `TaskConstraintChange` によるスケジュールの整合性維持とリアルタイム監視エンジンの設計

プロジェクト管理の現場において、Microsoft Projectは極めて強力な計算エンジンを持つ。しかし、その強力さゆえに、ユーザーによる「恣意的な制約(Constraint)」の追加が、プロジェクト全体のクリティカルパスと論理的整合性を破壊する温床となる。

「どうしてもこのタスクはこの日に終わらせたい」という現場の要求から、ユーザーは `Must Finish On` や `Start No Earlier Than` といった制約を安易に設定する。これが積み重なった結果、スケジュールは計算エンジンによる動的な計算(Dynamic Scheduling)を失い、単なる「日付の羅列」へと成り下がる。

本稿では、シニアエンジニアおよびシステム管理者が直面するこの問題を、`Project.TaskConstraintChange` イベントをフックした「制約監視エンジン」によって解決する、極限のアーキテクチャを解説する。

1. アーキテクチャの設計思想:観測者モデルの導入

単なるマクロによる事後チェック(Post-check)では、データの整合性は担保できない。真のガバナンスとは、「不正な状態がプロジェクト・ファイルに書き込まれる瞬間を阻止すること」である。

我々が実装すべきは、以下の3要素を備えた監視エンジンである。

1. Event Interception: `TaskConstraintChange` イベントによる変更の即時検知。
2. State Management: 変更前の「正当な状態」をメモリ上に保持する履歴管理。
3. Re-entrancy Control: ロールバック処理自体が再度イベントを発生させ、無限ループに陥るのを防ぐ排他制御。

2. 実装の核心:`clsConstraintGuard` クラスモジュール

イベントを管理するロジックは、標準モジュールに記述してはならない。オブジェクトのライフサイクルを制御し、メモリリークを防ぐために、必ずクラスモジュールにカプセル化する。

ここでは、`Scripting.Dictionary` を用いてタスクの状態を管理し、制約違反を検知した瞬間にロールバックを実行するクラスを設計する。

クラスモジュール: `clsConstraintGuard`

' ==============================================================================
' Class: clsConstraintGuard
' Description: Projectの制約変更を監視し、組織ルールに違反した際に即座に
'              ロールバックを実行する監視エンジン。
' ==============================================================================
Option Explicit

' Microsoft Scripting Runtime の参照設定が必要
Private WithEvents App As MSProject.Application
Private m_IsRollingBack As Boolean ' 再入防止フラグ
Private m_History As Scripting.Dictionary ' タスクの状態を保持するメモリ領域

' 組織の許可する制約タイプを定義(例: ASAP, ALAPのみ許可)
Private Const ALLOWED_CONSTRAINT_START As Long = pjAsSoonAsPossible
Private Const ALLOWED_CONSTRAINT_FINISH As Long = pjAsSoonAsPossible

' ------------------------------------------------------------------------------
' Initialize: エンジンの初期化
' ------------------------------------------------------------------------------
Public Sub Initialize(ByRef targetApp As MSProject.Application)
    Set App = targetApp
    Set m_History = New Scripting.Dictionary
    m_IsRollingBack = False
    Debug.Print "Constraint Guard Engine Initialized."
End Sub

' ------------------------------------------------------------------------------
' Terminate: オブジェクトの明示的解放(メモリ最適化)
' ------------------------------------------------------------------------------
Public Sub Terminate()
    On Error Resume Next
    Set m_History = Nothing
    Set App = Nothing
    Debug.Print "Constraint Guard Engine Terminated."
End Sub

' ------------------------------------------------------------------------------
' Event: TaskConstraintChange
' ------------------------------------------------------------------------------
Private Sub App_TaskConstraintChange(ByVal Task As MSProject.Task)
    ' 1. 再入防止チェック:ロールバック中のイベント発生を無視する
    If m_IsRollingBack Then Exit Sub
    
    ' 2. 特殊なタスク(サマリータスク等)の除外ロジックをここに実装
    If Task Is Nothing Then Exit Sub

    On Error GoTo ErrorHandler

    ' 3. 制約違反の判定
    ' 例: 制約が 'Must Finish On' 等の強制的制約に変更された場合
    If Task.ConstraintType <> ALLOWED_CONSTRAINT_START And _
       Task.ConstraintType <> ALLOWED_CONSTRAINT_FINISH Then
        
        Debug.Print "Violation Detected: Task [" & Task.Name & "] - Constraint: " & Task.ConstraintType
        
        ' ロールバック実行
        Call PerformRollback(Task)
    Else
        ' 4. 正当な変更であれば、現在の状態を履歴に保存(次回のロールバック用)
        ' ※実運用では、変更前の状態を保持するロジックを TaskChange イベント等で別途実装する必要がある
        UpdateHistory Task
    End If

    Exit Sub

ErrorHandler:
    ' Windows APIを使用した致命的なエラーログ出力などを検討すべき箇所
    Debug.Print "Error in ConstraintGuard: " & Err.Description
End Sub

' ------------------------------------------------------------------------------
' Private: Rollback Logic
' ------------------------------------------------------------------------------
Private Sub PerformRollback(ByRef TargetTask As MSProject.Task)
    m_IsRollingBack = True ' 排他制御開始
    
    On Error Resume Next
    ' 履歴から以前の状態を復元(ここでは簡略化のため、ASAPに戻す実装)
    TargetTask.ConstraintType = pjAsSoonAsPossible
    
    ' 変更が適用されたことを通知
    MsgBox "警告: 組織の標準ルールに基づき、制約の変更を拒否しました。" & vbCrLf & _
           "タスク: " & TargetTask.Name, vbCritical, "Governance Violation"
    
    m_IsRollingBack = False ' 排他制御解除
End Sub

' ------------------------------------------------------------------------------
' Private: History Management
' ------------------------------------------------------------------------------
Private Sub UpdateHistory(ByRef TargetTask As MSProject.Task)
    ' タスクIDをキーとして、制約タイプと日付を保存する
    ' 実装の詳細は、メモリ消費量とタスク数とのトレードオフを考慮すること
    If Not m_History.Exists(TargetTask.ID) Then
        m_History.Add TargetTask.ID, TargetTask.ConstraintType
    Else
        m_History(TargetTask.ID) = TargetTask.ConstraintType
    End If
End Sub

3. エンジニアが留意すべき「極限の知見」

この実装を実戦投入する際、シニアエンジニアは以下の3点に神経を研ぎ澄ませなければならない。

① 再入防止(Re-entrancy)の徹底

`PerformRollback` 内で行う `TargetTask.ConstraintType = …` という操作は、再び `App_TaskConstraintChange` イベントを誘発する。これに対処するための `m_IsRollingBack` フラグによる排他制御は、単なる作法ではなく、スタックオーバーフローと無限ループを防ぐための生存戦略である。

② オブジェクトのライフサイクルとメモリ管理

VBAはガベージコレクションが強力ではない。`clsConstraintGuard` インスタンスが、プロジェクトの終了時やアドインの解除時に適切に `Terminate` されない場合、`WithEvents` で保持している `Application` オブジェクトへの参照が残り続け、MS Project 自体のクラッシュ(Access Violation)を引き起こす。
必ず `Set Instance = Nothing` を明示的に呼び出す設計にせよ。

③ スケーラビリティとパフォーマンス

数万タスク規模のプロジェクトにおいて、すべての制約変更を `Dictionary` に格納し続けることは、メモリの断片化を招く。

  • 最適化案: 変更があったタスクの ID のみを保持する、あるいは特定の「重要タスク(Critical Path 上のタスク)」にのみ監視を限定するフィルタリングロジックを `App_TaskConstraintChange` の冒頭に挿入せよ。

4. 実装のデプロイメント(標準モジュール)

監視エンジンを起動させるためのエントリポイントである。

' ==============================================================================
' Module: modMain
' Description: エンジンのライフサイクル管理
' ==============================================================================
Option Explicit

Private m_Guard As clsConstraintGuard

' プロジェクト起動時に実行
Public Sub StartGovernanceEngine()
    Set m_Guard = New clsConstraintGuard
    m_Guard.Initialize Application
    MsgBox "Governance Engine is active.", vbInformation
End Sub

' プロジェクト終了時に実行(必ず呼び出すこと)
Public Sub StopGovernanceEngine()
    If Not m_Guard Is Nothing Then
        m_Guard.Terminate
        Set m_Guard = Nothing
    End If
    MsgBox "Governance Engine stopped.", vbInformation
End Sub

結論

プロジェクトの整合性を守るのは、ユーザーの規律ではなく、堅牢なアーキテクチャである。
`TaskConstraintChange` を利用したリアルタイム監視は、MS Project を単なる「描画ツール」から、組織のルールを強制する「ガバナンス・プラットフォーム」へと昇華させる。

エンジニアに求められるのは、単に動くコードを書くことではない。例外的なユーザー操作、メモリの挙動、そしてシステム全体の安定性を予見した設計を行うことである。

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