【極限の知見】サブプロジェクト統合時の「制約条件の衝突」を制御せよ ― スケジュール計算エンジンの挙動を支配するアーキテクチャ
プロジェクト管理の現場において、複数のサブプロジェクトを一つのマスタープロジェクトへ統合する作業は、避けては通れない「儀式」だ。しかし、多くの現場ではこの統合作業が、スケジュール計算エンジンの暴走を招く「地雷原」となっていることに気づいていない。
サブプロジェクトを統合した瞬間、意図しないタスクの移動、あるいは数ヶ月単位の遅延がマスター側に波及する。その原因は何か? 答えは単純だ。「制約条件(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における自動化の本質は、「エンジンの挙動を予測し、その挙動を最小限のコストで制御すること」にある。
日付を力任せに書き換えるのではなく、制約という「命令」を書き換える。この視点の差が、バグの起きない堅牢なツールと、現場を混乱させる脆弱なスクリプトを分かつのだ。
