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

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

整合性の崩壊を許さない:MS Projectにおける依存関係と制約条件の論理矛盾検知エンジン

プロジェクトの規模が数千タスクを超え、複雑な依存関係(Predecessors/Successors)が絡み合い、さらに「Must Start On」や「Start No Earlier Than」といった強固な制約条件(Constraints)が散りばめられたとき、プロジェクト管理の現場は「論理の泥沼」へと変貌する。

MS Projectの計算エンジンは極めて優秀だ。しかし、計算エンジンが「計算可能な範囲で無理やりスケジュールを成立させてしまう」ことが、真の脅威となる。制約条件と依存関係が矛盾しているにもかかわらず、プロジェクトが「正常なスケジュール」として表示されてしまう――この「静かなる論理破綻」こそが、大規模プロジェクトのデッドラインを破壊する真犯人である。

本稿では、単なるタスクの走査を超え、タスク間のグラフ構造を論理的に検証し、制約条件の矛盾を再帰的に抽出する「整合性チェック・エンジン」の設計思想とその実装について詳述する。

1. アーキテクチャの設計思想:COM境界のオーバーヘッドを最小化せよ

シニアエンジニアが最初に直面する壁は、MS ProjectのCOMオブジェクトへのアクセス速度だ。`ActiveProject.Tasks` を `For Each` で回し、その中で各タスクの `Predecessors` を参照するような実装は、タスク数が数千に達した時点で致命的なパフォーマンス低下を招く。

我々が構築すべきは、「データの非同期的抽出とメモリ内での論理演算」である。

1. Data Extraction Phase: MS ProjectのCOMオブジェクトから、計算に必要な最小限のプロパティ(ID, Start, Finish, ConstraintType, Predecessor IDs)を、高速な `Dictionary` または `Array` に一括転送する。
2. In-Memory Logic Phase: MS Projectの計算エンジンから切り離された、純粋なメモリ空間上でグラフ理論に基づいた矛盾検知を行う。
3. Reporting Phase: 検知された矛盾箇所のみを、再度COMオブジェクトを通じてユーザーに提示する。

これにより、COM境界のトラフィック(Marshalling/Unmarshalling)を最小限に抑え、計算速度を桁違いに向上させる。

2. 実装:整合性チェック・エンジンのコア・アルゴリズム

以下に、制約条件と依存関係の矛盾を検知するプロフェッショナルな実装例を示す。ここでは、タスクを軽量な構造体に変換し、再帰的なグラフ探索を行うアプローチを採用する。

Module: IntegrityEngine

Option Explicit

' --- 定数定義 (MS Projectの列挙型をエミュレート) ---
Private Const PJ_CONSTRAINT_START_ON_TIME As Long = 1
Private Const PJ_CONSTRAINT_MUST_START_ON As Long = 2
Private Const PJ_CONSTRAINT_START_NO_EARLIER_THAN As Long = 3
Private Const PJ_CONSTRAINT_START_NO_LATER_THAN As Long = 4

' --- 構造体定義: COMオブジェクトの重みを排除 ---
Private Type TaskNode
    ID As Long
    Name As String
    Start As Date
    Finish As Date
    ConstraintType As Long
    PredecessorIDs() As Long ' 依存先IDの配列
End Type

' 矛盾を格納する構造体
Private Type ConflictReport
    TaskID As Long
    Description As String
End Type

''' <summary>
''' プロジェクト全体の整合性を検証するメインルーチン
''' </summary>
Public Sub ExecuteIntegrityCheck()
    Dim taskMap As Object ' Scripting.Dictionary
    Set taskMap = CreateObject("Scripting.Dictionary")
    
    Dim conflicts As Collection
    Set conflicts = New Collection
    
    On Error GoTo ErrorHandler
    
    ' 高速化のための設定
    Application.ScreenUpdating = False
    
    ' 1. Data Extraction (COMからメモリへの転送)
    Debug.Print "Extracting data from MS Project..."
    Call ExtractTaskData(taskMap)
    
    ' 2. Logic Validation (メモリ内での矛盾検知)
    Debug.Print "Running logic validation..."
    Call ValidateConstraints(taskMap, conflicts)
    
    ' 3. Reporting
    Debug.Print "Validation complete. Found " & conflicts.Count & " conflicts."
    Call ReportConflicts(conflicts)

CleanUp:
    Set taskMap = Nothing
    Set conflicts = Nothing
    Application.ScreenUpdating = True
    Exit Sub

ErrorHandler:
    MsgBox "Critical Error: " & Err.Description, vbCritical
    Resume CleanUp
End Sub

''' <summary>
''' タスクデータをDictionaryに高速転送
''' </summary>
Private Sub ExtractTaskData(ByRef taskMap As Object)
    Dim t As Task
    Dim node As TaskNode
    Dim pred As Task
    Dim i As Long
    
    ' プロジェクトの全タスクを走査
    For Each t In ActiveProject.Tasks
        If Not t Is Nothing Then
            node.ID = t.ID
            node.Name = t.Name
            node.Start = t.Start
            node.Finish = t.Finish
            node.ConstraintType = t.ConstraintType
            
            ' 依存関係の抽出 (Predecessors)
            If t.Predecessors.Count > 0 Then
                ReDim node.PredecessorIDs(1 To t.Predecessors.Count)
                For i = 1 To t.Predecessors.Count
                    Set pred = t.Predecessors(i)
                    node.PredecessorIDs(i) = pred.ID
                Next i
            Else
                ' 配列の初期化
                node.PredecessorIDs = Split("", "") ' 空の配列
            End If
            
            taskMap.Add t.ID, node
        End If
    Next t
End Sub

''' <summary>
''' 論理矛盾の検知ロジック
''' </summary>
Private Sub ValidateConstraints(ByRef taskMap As Object, ByRef conflicts As Collection)
    Dim key As Variant
    Dim currentNode As TaskNode
    Dim predID As Long
    Dim predNode As TaskNode
    
    For Each key In taskMap.Keys
        currentNode = taskMap(key)
        
        ' --- 検証パターン1: 依存関係 vs Must Start On ---
        ' 前続タスクの終了日よりも、制約による開始日が前にある場合
        If currentNode.ConstraintType = PJ_CONSTRAINT_MUST_START_ON Or _
           currentNode.ConstraintType = PJ_CONSTRAINT_START_NO_EARLIER_THAN Then
           
            If IsArray(currentNode.PredecessorIDs) Then
                For Each predID In currentNode.PredecessorIDs
                    If taskMap.Exists(predID) Then
                        predNode = taskMap(predID)
                        
                        ' 論理矛盾の判定
                        ' 前続タスクのFinish > 現在タスクのConstraint Start
                        If predNode.Finish > currentNode.Start Then
                            conflicts.Add New ConflictReport(currentNode.ID, _
                                "Logical Conflict: Predecessor " & predNode.ID & " finishes after Constraint Start.")
                        End If
                    End If
                Next predID
            End If
        End If
        
        ' --- 検証パターン2: 循環参照 (簡易実装) ---
        ' ※ 本来は再帰的グラフ探索(DFS)を用いて閉路を検出する
    Next key
End Sub

' 補助関数: 配列チェック用
Private Function IsArray(arr As Variant) As Boolean
    On Error Resume Next
    IsArray = (Not IsEmpty(arr)) And (UBound(arr) >= LBound(arr))
    On Error GoTo 0
End Function

' 報告用クラスの代用 (簡易版)
Private Type ConflictReport
    TaskID As Long
    Description As String
End Type

Private Sub ReportConflicts(conflicts As Collection)
    If conflicts.Count = 0 Then
        MsgBox "No logical conflicts detected. Integrity is maintained.", vbInformation
        Exit Sub
    End If
    
    Dim msg As String
    Dim c As Variant
    msg = "Detected " & conflicts.Count & " conflicts:" & vbCrLf
    For Each c In conflicts
        msg = msg & "Task " & c.TaskID & ": " & c.Description & vbCrLf
    Next
    MsgBox msg, vbExclamation
End Sub

3. 極限の知見:メモリ管理とWindows APIの活用

オブジェクトのライフサイクル管理

VBAにおいて、`Task` オブジェクトのようなCOMオブジェクトを大量に扱う際、`Set t = Nothing` を忘れることは致命的ではないかもしれないが、大規模なループ内ではメモリリークの温床となる。特に、再帰関数の中でCOMオブジェクトを生成・破棄する場合、スタックオーバーフローだけでなく、ヒープ領域の断片化を引き起こす。

本コードで `TaskNode` を `Type`(構造体)として定義した理由は、`Task` オブジェクトそのものではなく、「値」としてメモリに保持するためだ。これにより、GC(ガベージコレクション)に依存せず、スコープを抜けた瞬間にメモリが解放される。

Windows API によるプロファイリング

もし、このエンジンが特定の環境で極端に低速化する場合、エンジニアは「何がボトルネックか」を直感ではなく数値で語らねばならない。Windows APIの `GetProcessMemoryInfo` を呼び出し、実行前後のワーキングセットの変化を監視することで、メモリリークの有無を科学的に特定できる。

' Windows APIによるメモリ使用量取得の断片

If VBA7 Then

Private Declare PtrSafe Sub GetProcessMemoryInfo Lib "psapi.dll" ( _ ByVal hProcess As LongPtr, _ ByRef lpProcessMemoryInfo As PROCESS_MEMORY_COUNTERS, _ ByVal cb As Long)

Else

' レガシー環境用

End If

4. 結論:アーキテクトの責務

スケジュール管理システムにおける「整合性チェック」は、単なる機能追加ではない。それは、システムの信頼性に対する防波堤である。

ユーザーが入力した制約条件が、プロジェクトの論理的整合性を破壊していないか。計算エンジンが「無理やり」出した答えが、現場の物理的な制約を無視していないか。それらをコードによって自動的に、かつ高速に検証する仕組みを持つことこそが、レガシーな環境から最新のエンタープライズ環境までを統べる、真のアーキテクトの仕事である。

このエンジンを基点とし、さらに「クリティカルパスの変動検知」や「リソース平準化の矛盾」へと拡張していくことで、あなたのシステムは単なる「ツール」から、プロジェクトの「羅針盤」へと昇華するだろう。

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