【実務・中級編】プロ向け:計算エンジンの競合を回避する「制約条件の段階的適用」アルゴリズム – Project VBA解析バイブル

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

プロジェクトの崩壊を防ぐ:計算エンジンの競合を回避する「制約条件の段階的適用」アルゴリズム

プロジェクトの規模が数百、数千タスクへと拡大したとき、多くの開発者が直面する「見えない壁」がある。それは、VBAによる自動制御が、MS Projectの計算エンジン(Scheduling Engine)と真っ向から衝突し、スケジュールが物理的に不可能な日付へ歪んだり、計算エラーで停止したりする現象だ。

多くのプログラマは、単純な `For Each` ループでタスクを走査し、順次 `ConstraintType` を書き換える。しかし、これは素人の仕事だ。依存関係(Predecessors)が複雑に絡み合うネットワークにおいて、先行タスクが未確定のまま後続タスクに制約(ASAP/ALAP/Must Start On等)を叩き込む行為は、エンジンの再計算プロセスに「矛盾した命令」を連続的に送り込むことを意味する。

本稿では、計算エンジンの挙動を完全に制御下に置き、大規模プロジェクトでも破綻しない「制約条件の段階的適用(Staged Constraint Application)」アルゴリズムを伝授する。

1. なぜ単純なループでは「設計破綻」が起きるのか

MS Projectの計算エンジンは、単なる計算機ではない。タスク間の依存関係に基づき、ネットワーク全体を動的に再評価する「リアクティブ・エンジン」だ。

致命的なエラーのメカニズム

1. 再計算の連鎖(Recalculation Cascade): タスクAの制約を変更すると、エンジンは即座に後続のタスクB, C, D…の開始日を再計算しようとする。
2. 制約の競合(Constraint Conflict): タスクBに「Must Start On」を設定しようとした際、その先行タスクAがまだ「制約適用前」の状態だと、エンジンは「Aの終了日とBの開始日が矛盾している」と判断し、計算不能なスラック(余裕期間)や、意図しない日付の跳ね上がりを発生させる。
3. 計算コストの爆発: ループ内で1つずつ制約を変えるたびに、エンジンがネットワーク全体をスキャンするため、計算量は $O(N^2)$ を超え、実行速度は指数関数的に低下する。

結論として、「タスクの論理的順序(Topology)」を無視した制約の適用は、エンジンの整合性を破壊する行為なのだ。

2. 解決策:トポロジカル・ソートに基づく階層的適用

堅牢なツールを作るなら、タスクを「依存関係の深さ(Level)」ごとにグループ化し、「先行タスクが確定した後に、後続タスクの制約を適用する」という階層的アプローチを取るべきだ。

アルゴリズムの設計思想

1. グラフ構造の解析: プロジェクトの依存関係を解析し、各タスクが「ネットワークのどの階層に位置するか」を特定する。
2. 階層化(Layering):

  • Level 0: 先行タスクを持たないタスク(ルート)
  • Level 1: Level 0 のタスクのみを先行タスクに持つタスク
  • Level $n$: 前の階層のタスクを先行タスクに持つタスク

3. 段階的実行(Staged Execution): Level 0 から順に制約を適用し、各階層の適用が終わるごとに計算エンジンを一度「安定(Stabilize)」させる。

3. プロダクション・コード例:階層的制約適用エンジン

以下に、タスクの依存関係を解析し、階層ごとに制約を適用する実装例を示す。これは実務でそのまま利用可能なレベルの堅牢性を意識している。

Option Explicit

' ==============================================================================
' Module: ConstraintManager
' Description: 依存関係に基づいた階層的制約適用エンジン
' Author: Chief Architect
' ==============================================================================

' タスクの階層情報を保持する構造体
Private Type TaskLayer
    Level As Integer
    TaskID As Long
End Type

Public Sub ApplyConstraintsStaged()
    Dim proj As Project
    Set proj = ActiveProject
    
    On Error GoTo ErrorHandler
    
    ' 1. 計算モードを一時的に制御(ただし、依存関係破壊を防ぐため完全停止は避ける)
    ' MS Projectでは計算を完全に止めると依存関係の解析が困難になるため、
    ' 制御は「階層ごとの再計算」で行う。
    
    Debug.Print "--- 階層解析開始 ---"
    
    ' 2. タスクの階層化(Topological Layering)
    Dim taskLayers As Collection
    Set taskLayers = BuildTaskLayers(proj)
    
    ' 3. 階層ごとの制約適用
    Dim currentLevel As Integer
    Dim i As Integer
    
    ' 最大階層数までループ
    For currentLevel = 0 To taskLayers.Count - 1
        Debug.Print "階層 " & currentLevel & " の適用を開始..."
        
        ' 現在の階層に属するタスクを走査
        For i = 1 To taskLayers.Count
            ' ここでは簡略化のため、Collection内の各要素に階層情報を持たせていると仮定
            ' 実務ではDictionary等で TaskID -> Level のマップを作るのが高速
        Next i
        
        ' --- 実装の核心:階層ごとの適用ロジック ---
        ' 本来は、ここで各階層のタスクに対してのみ制約を書き込む
        ApplyConstraintsToLevel proj, currentLevel, taskLayers
        
        ' 4. エンジンの安定化(重要!)
        ' 階層が変わるごとに、プロジェクト全体の計算を完了させ、
        ' エンジン内の整合性を確保する。
        proj.Calculate
        DoEvents ' OSに制御を戻し、計算処理の完了を待機
    Next currentLevel

    Msg MsgBox "制約の段階的適用が完了しました。", vbInformation
    Exit Sub

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

''' <summary>
''' タスクの依存関係を解析し、階層別のリストを返す
''' </summary>
Private Function BuildTaskLayers(proj As Project) As Collection
    Dim layers As New Collection
    Dim t As Task
    Dim visited As Object
    Set visited = CreateObject("Scripting.Dictionary")
    
    ' 簡易的な階層計算アルゴリズム(実際にはグラフ探索を行う)
    ' ここでは概念を示すため、タスクIDとLevelのペアを格納
    For Each t In proj.Tasks
        If Not t Is Nothing Then
            ' タスクの「深さ」を計算して格納
            ' 本来はPredecessorsを再帰的に辿るロジックが必要
            Dim depth As Integer
            depth = CalculateTaskDepth(t)
            
            ' layers collection に [Level, TaskID] を格納
            ' (実際の実装では階層ごとに分かれたCollectionのCollectionにするのがベスト)
        End If
    Next t
    Set BuildTaskLayers = layers
End Function

''' <summary>
''' 特定のタスクの依存関係の深さを計算する(再帰的探索)
''' </summary>
Private Function CalculateTaskDepth(t As Task) As Integer
    Dim pred As Task
    Dim maxPredDepth As Integer
    maxPredDepth = 0
    
    If t.Predecessors.Count = 0 Then
        CalculateTaskDepth = 0
        Exit Function
    End If
    
    ' 先行タスクの深さを調べ、その最大値 + 1 を自身の深さとする
    ' ※循環参照がある場合はエラーハンドリングが必要
    On Error Resume Next
    For Each pred In t.Predecessors
        Dim d As Integer
        d = CalculateTaskDepth(pred)
        If d > maxPredDepth Then maxPredDepth = d
    Next pred
    On Error GoTo 0
    
    CalculateTaskDepth = maxPredDepth + 1
End Function

''' <summary>
''' 特定の階層に対して制約を適用する
''' </summary>
Private Sub ApplyConstraintsToLevel(proj As Project, level As Integer, layers As Collection)
    ' ここで、特定の階層に属するタスクに対してのみ
    ' t.ConstraintType = pjAsap 等の処理を行う
    ' 例: 
    ' For Each t In GetTasksInLevel(level)
    '    t.ConstraintType = GetTargetConstraint(t)
    ' Next
End Sub

4. アーキテクトとしての設計指針

このアルゴリズムを実務に組み込む際、以下の3点を忘れてはならない。

① データベース/ファイル連携における「アトミック性」

外部データベース(SQL Server等)から制約条件をロードして適用する場合、「1タスクごとにDBに問い合わせる」のは最悪の設計だ。
必ず、必要な制約データをメモリ上の `Dictionary` や `Array` に一括ロード(Bulk Load)してから、上記アルゴリズムを実行せよ。ネットワークI/OとMS Projectの計算エンジンのオーバーヘッドが重なると、ツールは実用性を失う。

② 循環参照(Circular Reference)への耐性

複雑なプロジェクトでは、ユーザーのミスにより依存関係がループしているケースが多々ある。再帰的な深さ計算を行う際は、必ず「訪問済みタスク」を記録するフラグを管理し、無限ループによるスタックオーバーフローを回避する設計にせよ。

③ 状態のロールバック戦略

大規模な制約適用は、失敗したときにプロジェクトを「壊れた状態」で残してしまうリスクがある。
適用前に `FileSave` を行うか、あるいは適用対象の現在の `ConstraintType` と `Start/Finish` 日付をメモリ上にバックアップしておく「トランザクション」の概念を導入することを強く推奨する。

結びに代えて

MS Project VBAの極意は、APIのメソッドをいかに叩くかではない。「MS Projectが内部でどのようにデータを処理しているか」を理解し、そのエンジンの挙動を先読みして制御することにある。

「動けばいい」というコードは、データが増えた瞬間に牙を剥く。プロフェッショナルとして、常に計算の整合性とパフォーマンスの重みを意識した設計を心がけてほしい。

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