【実務・中級編】【実務中級】サブプロジェクト統合時の制約条件競合を自動解消するVBAスクリプト – Project VBA解析バイブル

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

【極限の知見】サブプロジェクト統合時の「制約条件の衝突」を制御せよ ― スケジュール計算エンジンの挙動を支配するアーキテクチャ

プロジェクト管理の現場において、複数のサブプロジェクトを一つのマスタープロジェクトへ統合する作業は、避けては通れない「儀式」だ。しかし、多くの現場ではこの統合作業が、スケジュール計算エンジンの暴走を招く「地雷原」となっていることに気づいていない。

サブプロジェクトを統合した瞬間、意図しないタスクの移動、あるいは数ヶ月単位の遅延がマスター側に波及する。その原因は何か? 答えは単純だ。「制約条件(Constraints)の不整合」である。

本稿では、単に「制約を外す」という初歩的な手法ではなく、エンジニアとして、またアーキテクトとして、どのように制約条件の競合を検知し、ロジカルに、かつ破壊を最小限に抑えて自動解消すべきかを論じる。

1. なぜ、単純な「日付上書き」は破滅を招くのか

多くの初級者は、統合時に発生するスケジュールのズレを直そうとして、以下のようなコードを書く。

' 典型的な「やってはいけない」コード
For Each t In ActiveProject.Tasks
    If t.Start < TargetDate Then t.Start = TargetDate
Next t

これは最悪の設計だ。なぜか?

1. 再計算の連鎖(Recalculation Cascades): MS Projectのエンジンは、一つのタスクの日付が変更されるたびに、依存関係(Predecessors/Successors)に基づきプロジェクト全体の再計算を試みる。ループ内で日付を書き換えることは、計算エンジンに対して数千回の「再計算命令」を投げているに等しい。これはパフォーマンスの致命的な低下を招くだけでなく、計算途中の不安定な状態を参照するリスクを生む。
2. 制約条件の無視: `Task.Start` を直接書き換えても、そのタスクに `Must Start On`(開始日固定)などの「硬い制約」が残っていれば、エンジンは再び元の位置へタスクを引き戻そうとする。これは「コードによる操作」と「エンジンのロジック」のデッドロック状態だ。

プロフェッショナルは、「日付を操作する前に、制約(ConstraintType)を操作する」。これが鉄則である。

2. 設計思想:検知・変換・再計算の3フェーズ

堅牢な自動解消スクリプトには、以下の3つのフェーズが必要不可欠である。

Phase 1: 検知 (Detection)

現在の制約条件をスキャンし、「ASAP(可能な限り早く)」以外の、スケジュールを拘束している「硬い制約(Hard Constraints)」を特定する。

Phase 2: 変換 (Transformation)

特定された制約を、ロジカルな戦略に基づいて「軟らかい制約(Soft Constraints)」、すなわち `pjTaskConstraintAsSoonAsPossible` (ASAP) へ変換する。

Phase 3: 整合 (Reconciliation)

すべての制約変換が終わった後に、一度だけプロジェクト全体の再計算をトリガーし、整合性を確保する。

3. プロダクション・グレードのコード実装

以下に、実務に耐えうる、保守性と堅牢性を備えた実装例を示す。このコードは、単に制約を消すだけでなく、「どのタスクが変更されたか」をログとして出力する設計を採用している。

Option Explicit

' --------------------------------------------------------------------------------
' Module: ConstraintResolver
' Description: サブプロジェクト統合時の制約条件競合を自動解消する
' Author: Chief Architect
' --------------------------------------------------------------------------------

' 定数定義:マジックナンバーを排除し、可読性と保守性を高める
Private Const LOG_PREFIX As String = "[ConstraintResolver] "

''' <summary>
''' プロジェクト内の「硬い制約」を検知し、ASAPへ変換するメインルーチン
''' </summary>
Public Sub ResolveConstraintConflicts()
    Dim t As Task
    Dim conflictCount As Long
    Dim totalTasks As Long
    Dim startTime As Double
    
    ' パフォーマンス計測開始
    startTime = Timer
    conflictCount = 0
    totalTasks = 0
    
    ' エラーハンドリングの構築
    On Error GoTo ErrorHandler
    
    ' 画面更新と計算を一時的に抑制(※Project VBAではCalculationの制御は限定的だが、
    ' 処理の整合性を保つために明示的に扱う)
    Application.ScreenUpdating = False
    
    Debug.Print LOG_PREFIX & "Starting constraint resolution process..."

    ' プロジェクト内の全タスクを走査
    ' ※大規模プロジェクトの場合、ここでFilterを使用して対象を絞り込む設計が望ましい
    For Each t In ActiveProject.Tasks
        ' 削除済みタスクや空のタスクをスキップ
        If Not t Is Nothing Then
            If Not t.Deleted Then
                totalTasks = totalTasks + 1
                
                ' 制約条件の判定
                ' pjTaskConstraintAsSoonAsPossible (0) 以外の制約を「競合リスクあり」とみなす
                If t.ConstraintType <> pjTaskConstraintAsSoonAsPossible Then
                    
                    ' ここで「本当に変更すべきか」のロジックを挟む
                    ' 例:特定のフィールドに「Manual_Override」フラグがある場合はスキップするなど
                    If ShouldResolve(t) Then
                        Call TransformToASAP(t)
                        conflictCount = conflictCount + 1
                    End If
                    
                End If
            End If
        End If
    Next t

    ' 全ての変換完了後、一度だけ再計算を強制する
    ' これにより、依存関係に基づく正しいスケジュールの再構築が行われる
    Application.Calculate
    
    Application.ScreenUpdating = True
    
    ' 結果のレポート
    Debug.Print LOG_PREFIX & "Process Completed."
    Debug.Print LOG_PREFIX & "Total Tasks Scanned: " & totalTasks
    Debug.Print LOG_PREFIX & "Conflicts Resolved: " & conflictCount
    Debug.Print LOG_PREFIX & "Execution Time: " & Round(Timer - startTime, 2) & " seconds"
    
    MsgBox "制約条件の解消が完了しました。" & vbCrLf & _
           "スキャン数: " & totalTasks & vbCrLf & _
           "解消数: " & conflictCount, vbInformation, "完了"
    
    Exit Sub

ErrorHandler:
    Application.ScreenUpdating = True
    MsgBox "致命的なエラーが発生しました。" & vbCrLf & _
           "Error Number: " & Err.Number & vbCrLf & _
           "Description: " & Err.Description, vbCritical, "Error"
    Debug.Print LOG_PREFIX & "ERROR: " & Err.Description
End Sub

''' <summary>
''' 制約を解消すべきかどうかの判定ロジック(戦略パターン)
''' </summary>
Private Function ShouldResolve(t As Task) As Boolean
    ' 実務上の要件に基づき、ここをカスタマイズする。
    ' 例: Must Start On (2) や Must Finish On (3) は解消対象とするが、
    ' 既に設定されている緩やかな制約は無視するなど。
    
    Select Case t.ConstraintType
        Case pjTaskConstraintMustStartOn, _
             pjTaskConstraintMustFinishOn, _
             pjTaskConstraintStartNoEarlierThan, _
             pjTaskConstraintStartNoLaterThan, _
             pjTaskConstraintFinishNoEarlierThan, _
             pjTaskConstraintFinishNoLaterThan
            ShouldResolve = True
        Case Else
            ShouldResolve = False
    End Select
End Function

''' <summary>
''' タスクの制約を ASAP に変換する
''' </summary>
Private Sub TransformToASAP(t As Task)
    ' ログ出力(プロダクション環境ではファイル出力やDB連携を検討)
    Debug.Print LOG_PREFIX & "Resolving Task: [" & t.ID & "] " & t.Name & _
                " (Current Constraint: " & GetConstraintName(t.ConstraintType) & ")"
    
    ' 制約の変更
    t.ConstraintType = pjTaskConstraintAsSoonAsPossible
    
    ' ※注意:日付そのものを変更する必要はない。
    ' 制約をASAPに変えるだけで、次回のCalculate時にエンジンが適切な位置へ移動させる。
End Sub

''' <summary>
''' 制約タイプを人間が読める形式に変換するヘルパー関数
''' </summary>
Private Function GetConstraintName(cType As Long) As String
    Select Case cType
        Case pjTaskConstraintAsSoonAsPossible: GetConstraintName = "ASAP"
        Case pjTaskConstraintAsLateAsPossible: GetConstraintName = "ALAP"
        Case pjTaskConstraintMustStartOn: GetConstraintName = "Must Start On"
        Case pjTaskConstraintMustFinishOn: GetConstraintName = "Must Finish On"
        Case pjTaskConstraintStartNoEarlierThan: GetConstraintName = "Start No Earlier Than"
        Case pjTaskConstraintStartNoLaterThan: GetConstraintName = "Start No Later Than"
        Case pjTaskConstraintFinishNoEarlierThan: GetConstraintName = "Finish No Earlier Than"
        Case pjTaskConstraintFinishNoLaterThan: GetConstraintName = "Finish No Later Than"
        Case Else: GetConstraintName = "Unknown"
    End Select
End Function

4. アーキテクトの視点:運用上の注意点

このスクリプトを実務のパイプラインに組み込む際、以下の3点に留意せよ。

① データの「不可逆性」への対策

このスクリプトは制約条件を書き換える。つまり、実行後は「元の制約が何であったか」が失われる。
大規模な統合プロセスにおいては、実行前に「現在の制約状態をスナップショットとしてCSVまたはデータベースに書き出す」機能を実装することを強く推奨する。これが、失敗した際の「ロールバック(復元)」を可能にする唯一の手段だ。

② 外部連携(DB/Excel)の際のトランザクション

もし、このスクリプトの結果を外部のデータベースやExcelの管理台帳に反映させる場合、MS Projectの操作とDBの更新を一つの「論理的なトランザクション」として扱う必要がある。
Project側の処理がエラーで中断したのに、DBだけが更新されてしまう事態は、データの整合性を破壊する。`On Error GoTo` 内で、DBの更新をキャンセル(Rollback)するロジックを必ず組み込め。

③ 複雑な制約(Constraint vs Dependency)の境界線

「制約条件(Constraint)」と「依存関係(Predecessor)」を混同してはならない。
本スクリプトが扱うのは、あくまでタスクに直接付与された「制約」である。タスク間の「つながり(リンク)」を操作するのは別の領域だ。もし依存関係そのものに矛盾がある場合は、別途、クリティカルパス解析に基づいたロジックが必要になる。

結論

Project VBAにおける自動化の本質は、「エンジンの挙動を予測し、その挙動を最小限のコストで制御すること」にある。

日付を力任せに書き換えるのではなく、制約という「命令」を書き換える。この視点の差が、バグの起きない堅牢なツールと、現場を混乱させる脆弱なスクリプトを分かつのだ。

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