【テクニカル・上級編】実務中級:プロジェクトテンプレート適用時の「制約条件クリーンアップ」自動化 – Project VBA解析バイブル

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

テンプレートの呪縛を解け:Project VBAにおける制約条件クリーンアップの極致

プロジェクト管理の現場において、テンプレート(雛形)の活用は不可避である。しかし、多くのシニアエンジニアが直面する「見えない壁」がある。それは、テンプレートから生成された新プロジェクトが、「過去の制約条件」という亡霊に支配され、スケジュール計算エンジンが正しく駆動しないという問題だ。

テンプレートに含まれる「Must Start On(開始日指定)」や「Must Finish On(終了日指定)」といったハードな制約条件は、新プロジェクトの開始日(Project Start Date)を無視してタスクを固定してしまう。これを手動で修正するのは、エンジニアリングの敗北である。

本稿では、Microsoft Projectの計算エンジンを完全に掌握し、プロジェクトを「数学的にクリーンな状態」へリセットするための、極限の自動化手法を解説する。

1. 計算エンジンのメカニズム:なぜ単純なループでは不十分なのか

多くの開発者は、`For Each task In ActiveProject.Tasks` でループを回し、`task.ConstraintType = pjConstraintAsSoonAsPossible` と書き換えるだけのコードを書く。しかし、これは「動いているように見えるだけの、不完全な実装」だ。

決定論的なスケジューリングの阻害要因

Microsoft Projectの計算エンジンは、タスク間の依存関係(Predecessors)と制約条件(Constraints)の相互作用によって、決定論的なスケジュールを算出する。テンプレートから持ち越された制約条件は、以下の二重の悪影響を及ぼす。

1. 計算の不整合: 制約条件が残っている状態で、後続タスクの依存関係を変更しても、エンジンは制約条件を優先するため、計算結果が論理的に破綻する。
2. COMインターフェースのオーバーヘッド: 大規模プロジェクト(数千タスク規模)において、タスクごとに計算モードを切り替えたり、プロパティを書き換えたりすることは、COM(Component Object Model)経由の通信コストを爆発させ、アプリケーションを「応答なし」の状態に陥れる。

我々が目指すべきは、「計算エンジンを一時停止し、メモリ上で制約を剥ぎ取り、一括で再計算を走らせる」という、原子的な(Atomic)操作である。

2. アーキテクチャ設計:高効率なクリーンアップ・パターン

効率的な実装には、以下の3つのレイヤーが必要となる。

1. Execution Control Layer: 計算モードを `pjManual` に切り替え、計算エンジンの暴走を防ぐ。
2. Data Transformation Layer: 制約条件(ConstraintType)だけでなく、それに付随する日付属性をクリアする。
3. Memory & Lifecycle Management Layer: COMオブジェクトの参照を適切に管理し、メモリリークを防止する。

実装の核となるロジック

単に `pjConstraintAsSoonAsPossible` を設定するだけでは、内部的な「制約日付」が残存する場合がある。これを確実に排除するためには、制約タイプを ASAP(できるだけ早く)にリセットした上で、タスクの開始日・終了日が「制約による固定」ではなく「依存関係による計算結果」であることを保証しなければならない。

3. 極限のコード実装:`CleanseProjectConstraints`

以下に、実務レベルで使用可能な、堅牢かつ高速な実装例を示す。このコードは、単なるスクリプトではなく、システムの一部として組み込まれることを想定した設計である。

Option Explicit

' ==============================================================================
' Module: ProjectArchitectUtils
' Description: プロジェクトの制約条件を強制的にクリーンアップする
' Author: Legendary Chief Architect
' ==============================================================================

''' <summary>
''' プロジェクトの全タスクに対し、テンプレート由来の制約条件を排除し、
''' プロジェクト開始日を起点としたASAP(できるだけ早く)状態へリセットする。
''' </summary>
''' <param name="newProjectStart">新プロジェクトの基準開始日</param>
Public Sub CleanseProjectConstraints(ByVal newProjectStart As Date)
    Dim pjApp As MSProject.Application
    Dim pjProj As MSProject.Project
    Dim t As MSProject.Task
    Dim taskCount As Long
    Dim i As Long
    
    Set pjApp = MSProject.Application
    Set pjProj = pjApp.ActiveProject
    
    ' 1. 計算モードをマニュアルに切り替え、計算エンジンの負荷を最小化する
    ' これを行わないと、1タスク書き換えるごとに全タスクの再計算が走り、処理時間が指数関数的に増大する
    pjApp.Calculation = pjManual
    
    On Error GoTo ErrorHandler
    
    ' タスク総数の取得(進捗表示用)
    taskCount = pjProj.Tasks.Count
    
    ' 2. タスクの反復処理
    ' For Each よりもインデックスによるアクセスの方が、大規模オブジェクトコレクションでは安定する場合がある
    For i = 1 To taskCount
        Set t = pjProj.Tasks(i)
        
        ' タスクが存在しない(空行)場合はスキップ
        If Not t Is Nothing Then
            
            ' サマリータスクを除外するかどうかは要件次第だが、
            ' 基本的に制約はリーフタスク(作業タスク)に適用すべきである。
            ' ここでは、制約を持つすべてのタスクを対象とする。
            
            ' 制約条件が ASAP (pjConstraintAsSoonAsPossible) 以外の場合のみ処理
            If t.ConstraintType <> pjConstraintAsSoonAsPossible Then
                
                ' 制約タイプを ASAP にリセット
                t.ConstraintType = pjConstraintAsSoonAsPossible
                
                ' 重要:制約タイプを変更しても、内部的な「制約日付」が残るケースがある。
                ' これを完全にクリアするため、タスクの開始日をプロジェクト開始日に寄せる。
                ' ※依存関係がある場合は、後の Recalculate で正しい位置に移動する。
                If t.Start < newProjectStart Then
                    t.Start = newProjectStart
                End If
                
            End If
            
            ' メモリ管理:オブジェクトの参照を明示的に解放
            ' ループ内でのSet Nothingは、COMオブジェクトのライフサイクルを制御するために重要
            Set t = Nothing
        End If
        
        ' Windows API的な配慮:極めて大規模なプロジェクトの場合、
        ' OSに制御を戻して「応答なし」を防ぐ
        If i Mod 500 = 0 Then
            DoEvents
        End If
    Next i

    ' 3. プロジェクト全体の再計算を実行
    ' 全ての制約が外れた状態で、依存関係に基づいた正しいスケジュールを再構築させる
    pjProj.Recalculate
    
    ' 計算モードを自動に戻す
    pjApp.Calculation = pjAutomatic
    
    MsgBox "Constraint Cleanup Complete. Project is now deterministic.", vbInformation, "Architect Success"
    
ExitSub:
    ' 後処理:オブジェクトの完全な解放
    Set t = Nothing
    Set pjProj = Nothing
    Set pjApp = Nothing
    Exit Sub

ErrorHandler:
    pjApp.Calculation = pjAutomatic ' エラー時も計算モードを戻さないと、ユーザーが操作不能になる
    MsgBox "Critical Error during Constraint Cleanup: " & Err.Description, vbCritical, "System Failure"
    Resume ExitSub
End Sub

4. シニアエンジニアへの技術的補足

メモリ管理とCOMの挙動

VBAにおける `Set t = Nothing` は、単なる作法ではない。MS Projectのような巨大なCOMサーバーにおいて、オブジェクトの参照カウントが適切に減少しない場合、メモリリークが発生し、バックグラウンドで動作する `MSProject.exe` プロセスが肥大化し続ける。特に、サーバーサイド(Windows Service等)からProjectを操作する場合、このライフサイクル管理のミスは即、システムダウンに直結する。

`DoEvents` の戦略的配置

`DoEvents` は、Windowsのメッセージキューを処理するための呼び出しである。ループの全ステップで行うとパフォーマンスが著しく低下するが、`i Mod 500 = 0` のように、一定の閾値(Chunk size)で呼び出すことで、ユーザーインターフェースのフリーズを防ぎつつ、スループットを維持する。これは、レガシーなデスクトップアプリケーションを制御する際の鉄則である。

計算エンジンの「確定」

`pjProj.Recalculate` は、単なる「再計算」ではない。これは、グラフ理論における「トポロジカル・ソート」を、制約条件の制約なしに再実行するプロセスである。テンプレートから持ち越された「Must Start On」というノードの固定を解除した後、このメソッドを呼ぶことで初めて、プロジェクトは「数学的に正しい状態」へと遷移する。

結論

テンプレートの適用は、プロジェクト管理における「初期化」のプロセスである。その初期化が不完全であれば、その後のあらゆるデータ分析、クリティカルパス解析、リソース平準化はすべて無意味なものとなる。

プロフェッショナルなエンジニアは、ツールの挙動を信じるのではなく、その背後にある計算エンジンとオブジェクトのライフサイクルを制御することに心血を注ぐべきである。この「制約条件クリーンアップ」の実装は、その第一歩となるだろう。

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