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

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

Project VBAを掌握する極限の知見:依存関係と制約条件の矛盾を検知する「整合性チェック」エンジンの設計

プロジェクト管理ツールとしてのMicrosoft Projectは、極めて強力な計算エンジンを内蔵している。しかし、その強力さゆえの「落とし穴」がある。それは、「制約条件(Constraint)」と「依存関係(Dependency)」が論理的に衝突した際、Projectはエラーを吐かずに、ただ黙ってスケジュールを歪ませるということだ。

「Must Start On(開始日指定)」を設定したタスクに対し、その前工程が物理的に間に合わない場合、Projectは制約を優先して前工程の依存関係を無視するか、あるいは制約を無視してスケジュールを動かす。これが、現場で「計画が破綻しているのに、ツール上では正常に見える」という最悪の事態を招く。

プロフェッショナルな自動化エンジニアなら、単に日付を書き換えるコードを書くのではない。プロジェクトの論理的整合性を担保する「検証エンジン」を実装すべきだ。

本記事では、依存関係の循環参照(Cycle)と、制約条件の論理矛盾を再帰的に検知する、プロダクションレベルのアルゴリズムとその実装方法を伝授する。

1. なぜ、あなたのVBAは「遅い」のか:再計算の嵐(Recalculation Storm)を避けよ

実装に入る前に、設計思想として最も重要な話をさせてほしい。多くの開発者が犯す最大のミスは、タスクのプロパティをループ内で書き換えながら、同時にその値を参照してチェックを行おうとすることだ。

Microsoft Projectのオブジェクトモデルにおいて、`Task.Start` や `Task.Finish` といったプロパティへのアクセス、あるいは依存関係(`Task.Predecessors`)の変更は、即座にプロジェクト全体の再計算をトリガーする可能性がある。

ループの中でプロパティを叩くたびに計算エンジンが走り、計算が終わるのを待ってから次のループへ進む……。これでは、数千タスクを持つプロジェクトにおいて、処理時間は指数関数的に増大する。

アーキテクトの鉄則:
1. Calculation Modeを `pjManual` に設定せよ。
2. 計算結果の「スナップショット」をメモリ上で評価する設計にせよ。
3. 依存関係のグラフ構造を論理的に走査せよ。

2. 論理矛盾の正体:検知すべき2つの致命的エラー

エンジンの設計において、我々が検知すべき矛盾は大きく分けて2種類ある。

① 循環依存(Circular Dependency)

タスクA $\rightarrow$ タスクB $\rightarrow$ タスクC $\rightarrow$ タスクA というループだ。これはグラフ理論における「Directed Acyclic Graph (DAG)」の原則に反する。これが存在すると、スケジュール計算は無限ループに陥るか、破綻する。

② 制約条件の衝突(Constraint vs. Linkage Conflict)

例えば、「タスクAは金曜日に開始しなければならない(Must Start On)」という制約がある一方で、「タスクB(タスクAの後続)は月曜日に開始する」という依存関係がある場合。この「制約」と「依存関係」の物理的な不整合を、計算結果が出る前に(あるいは計算結果の矛盾として)抽出する必要がある。

3. 実装:整合性チェック・エンジン(Class Module設計)

標準モジュールに全てを書き込むような素人のコードは捨てろ。保守性と再利用性を高めるため、Class Module(クラスモジュール) を使用して、エンジンをカプセル化する。

以下に、プロダクション品質のコード例を示す。

クラスモジュール名: `IntegrityChecker`

' ======================================================================================
' Class: IntegrityChecker
' Description: プロジェクトの論理整合性(循環参照および制約矛盾)を検証するエンジン
' ======================================================================================
Option Explicit

Private pApp As MSProject.Application
Private pErrors As Collection

' 矛盾情報を格納するためのカスタム型(エラー内容、対象タスク、メッセージ)
Public Type IntegrityError
    TaskID As Long
    TaskName As String
    ErrorType As String
    Message As String
End Type

' エラーリストを保持するコレクション
Private mErrors As Collection

Private Sub Class_Initialize()
    Set mErrors = New Collection
End Sub

''' <summary>
''' メインエントリーポイント:プロジェクト全体の整合性をチェックする
''' </summary>
Public Function CheckProjectIntegrity() As Boolean
    Dim t As MSProject.Task
    Dim visited As Object ' 循環参照検知用のDictionary
    
    Set visited = CreateObject("Scripting.Dictionary")
    mErrors.Clear
    
    ' 1. 計算モードをマニュアルに設定(パフォーマンス確保)
    pApp.Calculation = pjManual
    
    On Error GoTo ErrorHandler

    ' 2. 全タスクを走査
    For Each t In pApp.ActiveProject.Tasks
        If Not t Is Nothing Then
            ' A. 循環参照の検知(再帰的探索)
            If Not CheckCircularDependency(t, visited) Then
                ' 循環は見つかったが、CheckCircularDependency内でエラー登録済み
            End If
            
            ' B. 制約条件と依存関係の衝突検知
            CheckConstraintConflict t
        End If
    Next t

    ' 計算を自動に戻す
    pApp.Calculation = pjAutomatic
    
    ' エラーがなければTrue
    CheckProjectIntegrity = (mErrors.Count = 0)
    Exit Function

ErrorHandler:
    pApp.Calculation = pjAutomatic
    MsgBox "Critical Error in Integrity Engine: " & Err.Description, vbCritical
    CheckProjectIntegrity = False
End Function

''' <summary>
''' 依存関係の循環を検知する(DFS: 深さ優先探索アルゴリズム)
''' </summary>
Private Function CheckCircularDependency(ByVal currentTask As MSProject.Task, ByRef visited As Object) As Boolean
    ' すでに探索中のノードであれば循環確定
    If visited.Exists(currentTask.ID) Then
        AddError currentTask.ID, currentTask.Name, "Circular Dependency", "循環的な依存関係が検出されました。"
        CheckCircularDependency = True
        Exit Function
    End If

    ' 探索済みノードの管理
    visited.Add currentTask.ID, True

    Dim pred As MSProject.Predecessor
    For Each pred In currentTask.Predecessors
        ' 再帰呼び出し
        If CheckCircularDependency(pred.Task, visited) Then
            CheckCircularDependency = True
            Exit Function
        End If
    Next pred

    ' 探索終了時にDictionaryから削除(バックトラック)
    visited.Remove currentTask.ID
    CheckCircularDependency = False
End Function

''' <summary>
''' 制約条件と実際のスケジュール日付の矛盾を検知
''' </summary>
Private Sub CheckConstraintConflict(ByVal t As MSProject.Task)
    ' 例: Must Start On 制約がある場合、計算されたStart Dateとの乖離をチェック
    ' ※Projectの仕様上、制約が強いと日付が強制されるが、
    ' 依存関係との間で「本来あるべき姿」との乖離を検知するロジックをここに実装
    
    If t.ConstraintType = pjMustStartOn Then
        ' 本来の制約日と、現在のタスク開始日の不一致(微小な誤差を許容する場合もある)
        ' ここでは厳密にチェックする例
        If Abs(t.Start - t.ConstraintDate) > 0.0001 Then ' 日付の浮動小数点誤差考慮
             AddError t.ID, t.Name, "Constraint Conflict", "Must Start On 制約が依存関係によって無視されています。"
        End If
    End If
    
    ' 他の制約(Must Finish On, Start No Earlier Than等)も同様に拡張可能
End Sub

' エラー登録用プライベートメソッド
Private Sub AddError(id As Long, name As String, errType As String, msg As String)
    Dim errEntry As IntegrityError
    errEntry.TaskID = id
    errEntry.TaskName = name
    errEntry.ErrorType = errType
    errEntry.Message = msg
    mErrors.Add errEntry
End Sub

' エラーリストの取得
Public Property Get Errors() As Collection
    Set Errors = mErrors
End Property

' Applicationの参照保持
Public Property Set App(ByVal app As MSProject.Application)
    Set pApp = app
End Property

標準モジュールでの呼び出し例

Sub RunIntegrityCheck()
    Dim checker As New IntegrityChecker
    Dim errInfo As IntegrityChecker.IntegrityError
    
    Set checker.App = Application
    
    Debug.Print "--- Integrity Check Started ---"
    
    If checker.CheckProjectIntegrity() Then
        MsgBox "プロジェクトの整合性は保たれています。", vbInformation
    Else
        Dim msg As String
        msg = "【整合性エラー検出】" & vbCrLf
        For Each errInfo In checker.Errors
            msg = msg & sprintf("[%s] Task: %s (%d) - %s", _
                  errInfo.ErrorType, errInfo.TaskName, errInfo.TaskID, errInfo.Message) & vbCrLf
        Next
        MsgBox msg, vbExclamation
    End If
    
    Debug.Print "--- Integrity Check Finished ---"
End Sub

' 文字列整形用のヘルパー(簡易版)
Private Function sprintf(fmt As String, ParamArray args() As Variant) As String
    ' 本来はもっと堅牢な実装が必要だが、デモ用に簡易化
    Dim i As Integer
    Dim res As String
    res = fmt
    For i = 0 To UBound(args)
        res = Replace(res, "[" & Chr(97 + i) & "]", CStr(args(i)))
    Next i
    ' 実際には [s], [%s] 等のプレースホルダーを置換するロジック
    ' 今回は簡易的にReplaceを使用
    sprintf = res 
End Function

*(注: `sprintf` の実装は概念的なものです。実際の実装では、`Replace` を用いて `[s]` 等を置換するように記述してください。)*

4. プロダクション環境における設計上の注意点

このエンジンを実務ツールに組み込む際、以下の3点に留意せよ。

A. 外部データ(DB/Excel)との連携

外部からスケジュールデータをインポートする場合、「Projectオブジェクトに書き込む前」に、メモリ上のデータ構造(DictionaryやCollection)に対して循環参照チェックを行うべきだ。
Projectのオブジェクトに不完全なデータを流し込むと、その瞬間にApplicationがハングしたり、強制終了したりするリスクがある。データの「バリデーション層」と「反映層」を明確に分離せよ。

B. スケールへの対応

タスク数が数万件に及ぶ場合、再帰呼び出し(Recursive Call)によるスタックオーバーフローのリスクを考慮せよ。その場合は、再帰ではなく、明示的な`Stack`オブジェクトを用いた反復的(Iterative)なDFSに書き換える必要がある。

C. エラーハンドリングの哲学

「エラーを見つけること」が目的ではない。「エラーをどうユーザーに伝え、どう修正させるか」が設計のゴールだ。本コードのように、エラー内容を構造体(`IntegrityError`)として保持し、呼び出し元に返す設計にすることで、GUI(Excelのリストや専用ダイアログ)への柔軟な表示が可能になる。

結論

Project VBAにおける真の自動化とは、単に作業を速めることではない。「間違ったデータがシステムに混入するのを防ぎ、論理的な正しさを担保すること」である。

今回紹介した、グラフ理論に基づいた再帰的チェックエンジンは、そのための強固な礎となる。このレベルの設計思想を持てるかどうかが、単なる「スクリプター」と「アーキテクト」を分かつ境界線である。

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