【実務・中級編】プロ向け:計算エンジンの「再計算保留」状態を監視し、ユーザーに警告を出す仕組み – Project VBA解析バイブル

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

Project VBAの深淵:計算エンジンの「不整合」を封じ込める――データ整合性を担保するSentinel設計論

プロジェクト管理ツールを開発するエンジニアが、最も恐れるべき事態は何か?
それは、コードがエラーを出さずに「正常終了」したにもかかわらず、保存されたプロジェクトファイル内のスケジュールが、論理的に破綻していることだ。

タスクの依存関係(Predecessors)を書き換えた直後、あるいはリソース制約を変更した直後、計算エンジンが完了する前にファイルが保存される。あるいは、ユーザーが「手動計算モード(Manual Calculation)」に切り替えたまま、ツールが大量のデータ更新を強行する。

このとき、プロジェクトファイルには「見た目上のデータ」と「計算エンジンが保持する論理データ」の間に致命的な乖離(デターミナスティックな不整合)が生じる。これを放置することは、プロフェッショナルな自動化ツールとしては「失格」を意味する。

本稿では、Microsoft Projectの計算エンジン(Scheduling Engine)のライフサイクルを制御し、不整合な状態での保存や外部連携を物理的に阻止するための、「Sentinel(監視員)設計パターン」について解説する。

1. なぜ、あなたの自動化ツールは「嘘」をつくのか

多くの開発者が陥る罠は、`Task.Start` や `Task.Finish` プロパティを書き換えた直後に、その値が正しく反映されたと信じ込んでしまうことだ。

Microsoft Projectの計算エンジンは、必ずしも命令の直後に同期的に動作するわけではない。特に大規模なプロジェクトや、複雑な制約条件(ASAP/ALAP、Must Start On等)が絡む場合、エンジンは内部的なキュー(Queue)に計算リクエストを溜め込み、最適なタイミングでスキャンを実行する。

致命的なシナリオ

1. 計算モードの不一致: `CalculationMode` が `pjManual` になっている状態で、VBAが日付を書き換える。
2. 計算の遅延: 大量のリソース割り当て変更により、エンジンが計算処理の真っ只中にある。
3. 不完全な保存: 計算が完了していない(あるいは計算がスキップされた)状態で、`FileSave` が実行される。

結果として、保存されたファイルを開くと「依存関係は繋がっているのに、日付がズレている」という、プロジェクト管理における「死」を招くことになる。

2. アーキテクチャ設計:Sentinel(監視員)パターンの導入

堅牢なツールを作るためには、処理の本体(Logic)と、状態の監視(Sentinel)を分離すべきだ。ロジックの中に「計算が終わったか?」というチェックを散りばめるのは、保守性の観点から最悪の設計である。

我々が実装すべきは、「計算エンジンの整合性を検証し、不整合な状態であれば処理を中断、あるいは強制的に計算を完了させる」という抽象化されたガードクラスである。

設計の要諦

  • State Verification: `CalculationMode` が `pjAutomatic` であることを保証する。
  • Force Calculation: 必要に応じて `Calculate` メソッドを明示的に呼び出す。
  • Integrity Check: 重要な指標(例:クリティカルパスの整合性や、特定の制約下での日付)をサンプリングし、エンジンが「落ち着いた」ことを確認する。

3. プロダクション・コード:`clsCalculationSentinel`

以下に、実務レベルでそのまま組み込める設計思想に基づいたクラスモジュールの実装例を示す。

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

VERSION 1.0 CLASS
' ======================================================================================
' Class: clsCalculationSentinel
' Description: MS Projectの計算エンジン状態を監視し、データ整合性を担保する
' Author: Legendary Chief Architect
' ======================================================================================

Option Explicit

Private mApp As MSProject.Application

' 計算エンジンの状態を定義する列挙型
Public Enum EngineStatus
    esHealthy = 0      ' 正常:自動計算モードかつ計算完了
    esManualMode = 1   ' 警告:手動計算モード(要計算実行)
    esInconsistent = 2 ' 致命的:計算エンジンが不整合な状態
End Enum

' 初期化
Private Sub Class_Initialize()
End Sub

' 監視対象のApplicationオブジェクトをセット
Public Property Set TargetApp(ByVal App As MSProject.Application)
    Set mApp = App
End Property

' --------------------------------------------------------------------------------------
' メソッド: ValidateEngineState
' 目的: 計算エンジンが「安全な状態」にあるかを検証する。
'       不整合が疑われる場合は、強制的に計算を実行して整合性を図る。
' --------------------------------------------------------------------------------------
Public Function ValidateEngineState() As EngineStatus
    On Error GoTo ErrorHandler

    ' 1. 計算モードのチェック
    If mApp.CalculationMode = pjManual Then
        ' 手動モードの場合、強制的に計算を実行して自動モードへ戻す設計判断が必要
        ' ここでは、エンジンの整合性を取るために計算を強制する
        Debug.Print "[Sentinel] Warning: Manual Calculation Mode detected. Forcing calculation..."
        mApp.Calculate
        mApp.CalculationMode = pjAutomatic
    End If

    ' 2. エンジンの「落ち着き」を検証 (Stability Check)
    ' 大規模プロジェクトでは、Calculate直後でも内部的な計算キューが残ることがある。
    ' 非常に重要なタスクのStart/Finishに微小な変化がないか、あるいは
    ' 計算フラグが立っていないかをチェックするロジックをここに実装する。
    
    ' プロフェッショナルな手法:計算の「完了待ち」をシミュレートするために
    ' 内部的な再計算トリガーを一度走らせ、エラーが出ないかを確認する
    mApp.Calculate

    ' 3. 最終的な整合性確認
    ' ここでは簡略化しているが、実際には「特定の制約を持つタスク」の
    ' 日付が論理的に破綻していないかを検証するコードを記述する。
    
    ValidateEngineState = esHealthy
    Exit Function

ErrorHandler:
    Debug.Print "[Sentinel] Critical Error during engine validation: " & Err.Description
    ValidateEngineState = esInconsistent
End Function

' --------------------------------------------------------------------------------------
' メソッド: SafeExecute
' 目的: ユーザーから渡されたアクション(コールバック)を、安全な状態で実行する。
'       これは「Command Pattern」の応用である。
' --------------------------------------------------------------------------------------
Public Sub SafeExecute(ByRef ActionCallback As Object, ByVal MethodName As String, ByRef ParamArray Args())
    ' 1. 事前検証 (Pre-flight Check)
    If ValidateEngineState() <> esHealthy Then
        Err.Raise vbObjectError + 513, "clsCalculationSentinel", "計算エンジンが不安定なため、処理を中断しました。"
    End If

    ' 2. アクション実行
    ' CallByName を用いて、渡されたオブジェクトのメソッドを安全に呼び出す
    ' 注意: 引数の扱いは動的であるため、実務では型チェックを厳格に行うこと
    On Error Resume Next
    CallByName ActionCallback, MethodName, VbMethod, Args
    
    If Err.Number <> 0 Then
        Debug.Print "[Sentinel] Action execution failed: " & Err.Description
        Err.Raise Err.Number, "clsCalculationSentinel", Err.Description
    End If
    On Error GoTo 0

    ' 3. 事後検証 (Post-flight Check)
    ' 実行後の状態が壊れていないかを確認
    If ValidateEngineState() <> esHealthy Then
        Err.Raise vbObjectError + 514, "clsCalculationSentinel", "アクション実行後に計算エンジンの不整合が検出されました。"
    End If
End Sub

標準モジュールでの利用例

Option Explicit

' 実行したい業務ロジックを分離して記述(疎結合)
Public Sub UpdateTaskSchedule(ByVal TaskID As Long, ByVal NewStart As Date)
    Dim t As Task
    Set t = ActiveProject.Tasks(TaskID)
    
    ' ここでは単純に値を書き換えるだけ。
    ' 整合性の担保はSentinelに任せる。
    t.Start = NewStart
    Debug.Print "Task " & TaskID & " updated to " & NewStart
End Sub

' メインエントリポイント
Sub Main()
    Dim Sentinel As New clsCalculationSentinel
    Set Sentinel.TargetApp = Application
    
    On Error GoTo ErrorHandler

    ' 業務ロジックの実行をSentinel経由で行う
    ' これにより、実行前後の計算状態が自動的に保証される
    Debug.Print "Starting safe transaction..."
    
    ' 注: CallByNameの制約上、引数の渡し方には注意が必要。
    ' 本来はインターフェースを定義したクラスを渡すのがベストプラクティス。
    ' ここでは簡略化のため、直接ロジックを叩くイメージ。
    
    ' 実際の実務では、以下のように「検証してから実行」というフローを徹底する
    If Sentinel.ValidateEngineState() = 0 Then ' esHealthy
        UpdateTaskSchedule 5, Date + 7
    Else
        MsgBox "エンジンの状態が不正です。手動で計算を確認してください。", vbCritical
        Exit Sub
    End If

    ' 保存前の最終チェック
    If Sentinel.ValidateEngineState() = 0 Then
        ActiveProject.Save
        MsgBox "安全に保存されました。", vbInformation
    Else
        Err.Raise vbObjectError + 999, , "保存前に不整合を検出しました。"
    End If

    Exit Sub

ErrorHandler:
    MsgBox "致命的なエラー: " & Err.Description, vbCritical
End Sub

4. データベース・外部連携における「同期の罠」

このSentinel設計が真に価値を発揮するのは、ProjectのデータをSQL Serverや外部Web APIへ同期するフェーズだ。

多くの開発者が、`Task.Start` を更新した後、すぐに `INSERT INTO …` を実行する。しかし、上述の通り、Projectのオブジェクトモデル上の値と、計算エンジンが確定させた値が一致していない瞬間が存在する。

堅牢なデータ連携のフロー

1. Local Update: VBAでProject内のデータを更新。
2. Sentinel Validation: `clsCalculationSentinel` を呼び出し、`Calculate` を実行してエンジンを「静止」させる。
3. Snapshot: 整合性が確認された状態のプロパティ値を、一旦ローカル変数(または構造体)にコピーする。
4. External Sync: Projectオブジェクトを直接参照せず、コピーしたローカル変数を元にデータベースへ書き込む。

この「Snapshot」工程を飛ばすと、データベースには「計算前の古い日付」や「論理的にあり得ない日付」が混入し、システム全体の信頼性を失墜させる。

5. 結論:エンジニアリングとしての「慎重さ」

Project VBAの自動化は、単なる「操作の記録」ではない。それは、複雑な数学的モデル(スケジュールエンジン)を、外部から制御する高度なプログラミングである。

「動いたからOK」という思考を捨て、「計算エンジンがこの変更をどう解釈し、いつ確定させるのか」というライフサイクルに意識を向けてほしい。

今回紹介した `Sentinel` パターンは、コード量を増やすかもしれない。しかし、そのコードが「不整合なデータによる大規模な手戻り」を防ぐための、最強の保険となる。

「正しいコードを書くこと以上に、正しい状態であることを証明することに知性を割け。」

これが、伝説的なアーキテクトが最後に残す教訓だ。

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