【テクニカル・上級編】プロ向け:依存関係と制約条件の矛盾をVBAで検知する「整合性チェック」エンジン – Project VBA解析バイブル

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

整合性チェックエンジンの設計思想:MS Projectにおける依存関係と制約条件の論理的矛盾を排除する

プロジェクト・マネジメントにおける「スケジュール」とは、単なる日付の羅列ではない。それは、リソース、制約、そして論理的な依存関係が織りなす、高度に決定論的な計算モデルである。

多くのPM(プロジェクトマネージャー)は、Microsoft Projectの「制約条件(Constraint)」を安易に使用する。`Must Start On` や `Start No Earlier Than` といった制約は、一見するとスケジュールを制御する強力な武器に見えるが、その実態は、プロジェクトの「論理的整合性」を破壊する毒となり得る。先行タスクの終了日と、タスク自身に課された制約日が衝突したとき、Projectの計算エンジンは警告を出すが、大規模なプロジェクトにおいては、その矛盾がクリティカルパスを歪め、プロジェクト全体を崩壊させる。

本稿では、単なる「ツール」の作り方を解説するのではない。MS ProjectのCOMオブジェクトモデルの背後にある挙動を理解し、依存関係と制約条件の矛盾を、計算機科学的なアプローチで検出し、修正案を提示する「整合性チェック・エンジン」の設計と実装について、その極限の知見を共有する。

1. 矛盾の正体:制約条件 vs 依存関係のデッドロック

MS Projectにおいて、タスクの開始日は以下の二つの力の均衡によって決定される。

1. 依存関係による計算(Predecessor Logic): 先行タスクの終了日(または進捗)に基づいた、論理的な「最短可能日」。
2. 制約条件による拘束(Constraint Logic): ユーザーが手動で設定した「固定的な日付」。

矛盾が発生するのは、「依存関係が要求する最短開始日」が「制約条件で許可された開始日」よりも後になる場合である。このとき、スケジュールは「論理的な整合性」を失い、ユーザーが意図しない形でタスクが押し出されるか、あるいは不自然な「ラグ(遅延)」が発生する。

シニアエンジニアが構築すべきなのは、Projectの計算エンジンに頼り切るのではなく、独自の計算レイヤーを構築し、Projectの内部状態を監査(Audit)するエンジンである。

2. アーキテクチャ設計:メモリ管理とパフォーマンスの最適化

大規模プロジェクト(数千タスク規模)を扱う場合、VBAの標準的なループ処理では致命的なパフォーマンス低下を招く。COMオブジェクトへのアクセスは、メモリ空間を跨ぐオーバーヘッドが極めて大きいからだ。

高速化のための戦略

  • オブジェクト・キャッシュの回避: `ActiveProject.Tasks` をループ内で何度も参照するのは愚策である。一度、必要なプロパティ(ID, Start, Finish, ConstraintType, ConstraintDate, Predecessors)をVariant型の配列に一括で吸い出し、メモリ上の配列に対してロジックを展開すること。
  • Dictionaryオブジェクトの活用: タスク間の依存関係を高速に解決するため、`Scripting.Dictionary` を用いてタスクIDとオブジェクト(あるいは配列のインデックス)を O(1) で引けるように設計する。
  • 明示的なメモリ解放: クラスモジュールを多用する場合、参照カウントの管理を怠ると、ExcelやProjectのプロセスがメモリに残存する。`Set obj = Nothing` は基本だが、循環参照を防ぐ設計が不可欠である。

3. 実装:整合性チェック・エンジン

以下に、矛盾を検出し、修正案を提示するエンジンのコアロジックを示す。ここでは、保守性と拡張性を考慮し、ロジックをクラスモジュールに分離する設計思想に基づいている。

Module: `modIntegrityEngine`

Option Explicit

' -------------------------------------------------------------------------
' 整合性チェックエンジン・メインモジュール
' -------------------------------------------------------------------------

Public Sub RunIntegrityCheck()
    Dim engine As New clsIntegrityEngine
    Dim conflicts As Collection
    Dim conflict As clsConflict
    
    On Error GoTo ErrorHandler
    
    ' エンジンの初期化と実行
    Set engine = New clsIntegrityEngine
    Set conflicts = engine.Analyze(ActiveProject)
    
    ' 結果の出力
    If conflicts.Count = 0 Then
        MsgBox "整合性チェック完了:矛盾は見つかりませんでした。", vbInformation
    Else
        ' 矛盾がある場合、詳細をレポート(ここではイミディエイトウィンドウと簡易表示)
        Debug.Print "--- Integrity Check Report: " & conflicts.Count & " conflicts found ---"
        For Each conflict In conflicts
            Debug.Print "Task ID: " & conflict.TaskID & " | Name: " & conflict.TaskName
            Debug.Print "  [Conflict] Constraint Date: " & conflict.ConstraintDate & _
                        " vs Calculated Earliest Start: " & conflict.CalculatedStart
            Debug.Print "  [Suggestion] " & conflict.Suggestion
            Debug.Print "-------------------------------------------------------"
        Next conflict
        
        MsgBox conflicts.Count & " 件の矛盾が検出されました。詳細はイミディエイトウィンドウを確認してください。", vbExclamation
    End If

CleanUp:
    Set engine = Nothing
    Set conflicts = Nothing
    Exit Sub

ErrorHandler:
    MsgBox "致命的なエラーが発生しました: " & Err.Description, vbCritical
    Resume CleanUp
End Sub

Class Module: `clsIntegrityEngine`

Option Explicit

' -------------------------------------------------------------------------
' clsIntegrityEngine: 整合性分析のコアロジック
' -------------------------------------------------------------------------

Private Type TaskData
    ID As Long
    Name As String
    ConstraintType As Long
    ConstraintDate As Date
    CalculatedStart As Date
    HasConstraint As Boolean
End Type

' 矛盾情報を格納するためのコレクション用クラス(別途 clsConflict を作成)
' ここでは簡略化のため、内部構造として扱う

Public Function Analyze(ByVal proj As MSProject.Project) As Collection
    Dim conflicts As New Collection
    Dim t As MSProject.Task
    Dim taskMap As Object ' Scripting.Dictionary
    Set taskMap = CreateObject("Scripting.Dictionary")
    
    ' 1. 高速化のため、タスクデータをメモリ(Dictionary)にキャッシュ
    ' COMオブジェクトへのアクセス回数を最小限に抑える
    For Each t In proj.Tasks
        If Not t Is Nothing Then
            Dim data As TaskData
            data.ID = t.ID
            data.Name = t.Name
            data.ConstraintType = t.ConstraintType
            data.ConstraintDate = t.ConstraintDate
            
            ' 制約があるかどうかを判定 (pjTaskConstraintAsSoonAsPossible は 0)
            data.HasConstraint = (t.ConstraintType <> 0)
            
            ' 計算上の開始日(依存関係を考慮した後の値)を取得
            ' ※Projectの再計算が終わっていることが前提
            data.CalculatedStart = t.Start
            
            taskMap.Add t.ID, data
        End If
    Next t

    ' 2. 矛盾の検知ロジック
    ' 各タスクについて、制約条件と実態の乖離を検証
    Dim key As Variant
    For Each key In taskMap.Keys
        Dim currentTask As TaskData
        currentTask = taskMap(key)
        
        If currentTask.HasConstraint Then
            ' ケースA: Must Start On / Must Finish On 等の強力な制約
            ' 先行タスクとの依存関係により、制約日より後に開始せざるを得ない場合
            
            ' ここでは、Projectが計算したStart dateが、ConstraintDateより
            ' 後ろにズレている(制約が無視されている、あるいは論理的に不可能な)場合を検知
            
            ' 注意:Projectの挙動として、制約を破ることはできないため、
            ' 「制約によって発生している不自然なラグ」を検出するのがプロの仕事である。
            
            ' 簡易的なロジック:制約日と実際の開始日の差分を評価
            ' 本来、Must Start On なら Start == ConstraintDate であるべき
            If currentTask.ConstraintType = 1 Then ' pjTaskConstraintMustStartOn
                If Abs(currentTask.ConstraintDate - currentTask.CalculatedStart) > 0.1 Then
                    ' 矛盾発見:制約日と計算上の開始日が一致していない
                    Dim c As New clsConflict
                    c.TaskID = currentTask.ID
                    c.TaskName = currentTask.Name
                    c.ConstraintDate = currentTask.ConstraintDate
                    c.CalculatedStart = currentTask.CalculatedStart
                    c.Suggestion = "制約条件を 'As Soon As Possible' に変更するか、先行タスクの終了日を調整してください。"
                    conflicts.Add c
                End If
            End If
        End If
    Next key

    Set Analyze = conflicts
End Function

*(注: `clsConflict` クラスは、プロパティを持つ単純なデータ保持用クラスとして定義されているものとする)*

4. 極限の知見:Windows APIによるシステムレベルの監視

さらに高度なシステム、例えば「プロジェクト管理サーバー」の一部としてVBAを動作させる場合、メモリリークやゾンビプロセスの発生は許されない。

MS ProjectのCOMインターフェースは、不適切なエラーハンドリングによって、ExcelやProjectのメモリ空間に「未解放の参照」を残しやすい。これを防ぐために、Windows APIの `GetProcessMemoryInfo` を呼び出し、実行中のプロセスのプライベートワーキングセットを監視する機能を組み込むことは、エンタープライズ級の自動化ツールにおける「作法」である。

また、大規模なバッチ処理を行う際は、`Declare Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)` を用いて、CPUの過負荷を制御し、OSのスケジューラに制御を戻す配慮も、レガシー環境を安定させるための知見である。

5. 結言

整合性チェックエンジンとは、単にエラーを見つけるためのものではない。それは、プロジェクトの「論理的な完全性」を保証するための、防波堤である。

シニアエンジニアとして、我々はツールが提示する結果を鵜呑みにしてはならない。ツールが「計算した結果」の裏側にある、制約と依存関係の力学を読み解き、それをコードによって検証する。そのプロセスこそが、不確実なプロジェクト管理という戦場において、確実な航路を示す唯一の手段となるのだ。

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