The Sentinel Pattern: TaskConstraintChangeを用いた制約条件の強制整合性維持とロールバック機構の設計
プロジェクト・マネジメント・ソフトウェアにおける「スケジュールの整合性」とは、単なる日付の羅列ではない。それは、CPM(Critical Path Method)エンジンが計算する、タスク間の依存関係、リソース制約、そして時間的論理の結晶である。
しかし、現場の人間は往々にして、この数学的秩序を破壊する。
「どうしてもこのタスクを今日始めたいんだ」という、一時の感情に基づいた制約条件(Constraint)の強制的変更。それが `Must Start On` や `Start No Earlier Than` といった制約を、本来の ASAP/ALAP の論理から逸脱させる。たった一つの制約変更が、プロジェクト全体のクリティカルパスを歪め、計算エンジンの信頼性を失墜させる。
我々アーキテクトの責務は、システムの「自由」を制限することではない。システムの「整合性」を、プログラムの深淵から守り抜くことにある。
本稿では、MS Project VBAにおける `TaskConstraintChange` イベントをフックし、ユーザーによる不適切な制約変更をリアルタイムで検知、即座にロールバック(復元)する「監視エージェント(Sentinel)」の実装手法を詳説する。
1. アーキテクチャの核心:イベント・リエンツリー問題の回避
エンジニアが陥る最大の罠は、「イベントハンドラ内でのプロパティ変更による無限再帰(Recursive Loop)」である。
`TaskConstraintChange` イベントを検知し、その制約を元に戻そうとコード内で `Task.Constraint = …` を実行した瞬間、再び `TaskConstraintChange` イベントが発行される。これを適切に制御しなければ、スタックオーバーフローを引き起こし、MS Project自体がクラッシュする。
これを回避するためには、「再入防止フラグ(Re-entry Guard)」の導入が不可欠である。これは単なる `Boolean` 変数ではない。システムの実行状態(State)を管理する、守護者としてのスイッチである。
2. 実装:ConstraintGuard クラスモジュール
この監視ロジックは、標準モジュールに記述してはならない。オブジェクトのライフサイクルを制御し、イベントを保持し続けるために、必ずクラスモジュールとしてカプセル化する。
以下に、プロフェッショナルな実装例を示す。
Class Module: `clsConstraintGuard`
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "clsConstraintGuard"
Attribute VB_Exposed = True
' ==============================================================================
' Project VBA Constraint Monitoring Agent (Sentinel Pattern)
'
' 目的: 特定のルールに基づかない制約条件の変更を検知し、即座にロールバックする。
' 注意: イベント再帰防止ロジックを実装済み。
' ==============================================================================
Option Explicit
' MS Project Applicationオブジェクトのイベントを捕捉するための宣言
Private WithEvents App As MSProject.Application
' 再入防止フラグ(Re-entry Guard)
Private m_isProcessing As Boolean
' 監視対象とする制約条件の定義(例: ASAPのみを許可)
' 実際の実装では、タスクの属性やグループに応じて動的に変更する設計を推奨
Private Const ALLOWED_CONSTRAINT As MSProject.TaskConstraint = pjAsSoonAsPossible
' 監視対象のタスクリスト(メモリ最適化のため、IDのみを保持することを推奨)
Private m_restrictedTaskIDs As Collection
''' <summary>
''' 監視エージェントの初期化
''' </summary>
''' <param name="targetApp">監視対象のMS Project Applicationインスタンス</param>
Public Sub Initialize(ByRef targetApp As MSProject.Application)
Set App = targetApp
Set m_restrictedTaskIDs = New Collection
m_isProcessing = False
Debug.Print "[Sentinel] Monitoring Agent Initialized."
End Sub
''' <summary>
''' 監視対象タスクの追加
''' </summary>
Public Sub AddRestrictedTask(ByVal taskID As Long)
On Error Resume Next
m_restrictedTaskIDs.Add taskID, CStr(taskID)
On Error GoTo 0
End Sub
''' <summary>
''' 制約変更イベントのハンドラ
''' </summary>
Private Sub App_TaskConstraintChange(ByVal Task As MSProject.Task)
' 1. 再入防止チェック: 自身のロールバック処理によるイベント発火を無視する
If m_isProcessing Then Exit Sub
' 2. 監視対象タスクであるかの判定
If Not IsRestricted(Task.ID) Then Exit Sub
' 3. ロールバック処理の開始
On Error GoTo ErrorHandler
m_isProcessing = True ' フラグを立てる(ここから再帰は無視される)
' 4. 整合性チェック
' ユーザーが許可されていない制約に変更した場合
If Task.Constraint <> ALLOWED_CONSTRAINT Then
' ログ記録(実運用では外部DBやファイルへの非同期ログ出力を検討)
Debug.Print "[Sentinel] ALERT: Unauthorized constraint change detected on Task: " & Task.Name
Debug.Print " Old Constraint: " & Task.Constraint & " -> New: " & Task.Constraint
' 5. 強制ロールバック
' プロジェクトの整合性を保つため、即座に規定の値へ書き戻す
Task.Constraint = ALLOWED_CONSTRAINT
' ユーザーへの警告(UIスレッドをブロックしない程度の軽微な通知)
' MsgBox はユーザーの作業を阻害するため、実運用ではステータスバー利用を推奨
App.StatusBar = "Sentinel: Unauthorized constraint change reverted on task: " & Task.Name
End If
CleanUp:
m_isProcessing = False ' フラグを戻す
Exit Sub
ErrorHandler:
Debug.Print "[Sentinel] CRITICAL ERROR: " & Err.Description
Resume CleanUp
End Sub
' --- Private Helper Methods ---
''' <summary>
''' 指定されたタスクIDが監視対象リストに含まれているか判定
''' </summary>
Private Function IsRestricted(ByVal taskID As Long) As Boolean
On Error Resume Next
Dim dummy As Variant
dummy = m_restrictedTaskIDs.Item(CStr(taskID))
IsRestricted = (Err.Number = 0)
On Error GoTo 0
End Function
''' <summary>
''' オブジェクトの明示的な解放(メモリ管理の極致)
''' </summary>
Public Sub Terminate()
On Error Resume Next
Set m_restrictedTaskIDs = Nothing
Set App = Nothing
Debug.Print "[Sentinel] Monitoring Agent Terminated."
End Sub
3. 実装におけるエンジニアリング・ディテール
3.1 メモリ管理とオブジェクトのライフサイクル
VBAはCOM(Component Object Model)上で動作している。`clsConstraintGuard` インスタンスをグローバル変数として保持する場合、プロジェクト終了時に適切に `Terminate` メソッドを呼び出し、`Set App = Nothing` を実行しなければならない。これを怠ると、MS Projectのプロセスがバックグラウンドでゾンビ化し、メモリリークの原因となる。
3.2 パフォーマンスの重み
`TaskConstraintChange` は、ユーザーがドラッグ&ドロップでスケジュールを動かしている間、非常に高頻度で発生する可能性がある。
監視対象(`m_restrictedTaskIDs`)の検索において、`Collection` を使用し、Keyに `Task.ID` を指定することで、検索コストを $O(1)$ に抑えている。大規模プロジェクト(数万タスク)において、この差は致命的なラグとなって現れる。
3.3 堅牢なエラーハンドリング
`On Error GoTo ErrorHandler` は、単なるお作法ではない。特にロールバック処理中にエラーが発生した場合、`m_isProcessing` フラグが `True` のまま残ってしまうと、エージェントは「永久に沈黙」する。エラーハンドラ内で必ずフラグを `False` に戻す(`CleanUp` セクション)設計は、可用性を担保するための絶対条件である。
4. 運用への統合:標準モジュールからの起動
実際にこのエージェントを稼働させるには、プロジェクト起動時にインスタンスを生成する。
' Standard Module
Option Explicit
Private Sentinel As clsConstraintGuard
''' <summary>
''' システム起動時の初期化ルーチン
''' </summary>
Public Sub SystemStartup()
Set Sentinel = New clsConstraintGuard
' MS ProjectのApplicationインスタンスを渡す
Sentinel.Initialize MSProject.Application
' 特定のタスク(例:マイルストーン)を監視対象に登録
' 本来はDBや設定ファイルから読み込む設計が望ましい
Sentinel.AddRestrictedTask 1
Sentinel.AddRestrictedTask 5
Debug.Print "System Sentinel is now active."
End Sub
''' <summary>
''' システム終了時のクリーンアップ
''' </summary>
Public Sub SystemShutdown()
If Not Sentinel Is Nothing Then
Sentinel.Terminate
Set Sentinel = Nothing
End If
End Sub
結言
スケジュール管理システムにおける「制約」とは、プロジェクトの物理的な法則である。
ユーザーに自由を与えることは重要だが、その自由がシステムの論理的整合性を破壊することを許してはならない。
`TaskConstraintChange` を利用したこの監視エージェントは、単なるコードの断片ではない。それは、不確実な人間活動の中に、計算可能な秩序をもたらすための「法」の具現化である。シニアエンジニアとして、我々は常にこの「秩序の番人」としての視点を忘れてはならない。
