制約の呪縛を解け:Project VBAによるスケジュール最適化と「制約強制解除」の極限アルゴリズム
プロジェクトマネジメントの現場において、MS Projectは強力なエンジンであるが、同時に「ユーザーが設定した制約(Constraints)」という名の重力に縛られた閉鎖系でもある。
多くのPMは、「Must Start On」や「Must Finish On」といったハード制約を積み上げることで、プロジェクトの進捗を管理しているつもりになっている。しかし、エンジニアリングの視点から見れば、これらはスケジュールの数学的な自由度を奪い、クリティカルパスを隠蔽し、真のボトルネックを不可視化させる「ノイズ」に他ならない。
本稿では、プロジェクトの「真の最短終了日」を算出するために、VBAを用いて制約条件を一時的に剥離・解析し、最適化を行うアーキテクチャについて詳述する。これは単なる自動化ではない。プロジェクトの構造的欠陥を炙り出すための、計算機科学的アプローチである。
1. 概念的フレームワーク:制約の「剥離」と「再構築」
スケジュール最適化において、我々が戦うべきは「遅延」ではなく「制約による計算の歪み」である。
MS Projectの計算エンジンは、制約条件を最優先事項として処理する。例えば、あるタスクに「Must Start On」が設定されている場合、そのタスクの前続関係がどれほど余裕(Float)を持っていたとしても、エンジンは制約に従ってタスクを固定する。これにより、プロジェクト全体のクリティカルパスが、本来の論理的依存関係ではなく、制約によって「偽装」される。
我々が実装すべきアルゴリズムのフローは以下の通りである。
1. State Capture(状態の捕捉): 現在のすべてのタスクの制約タイプ(`ConstraintType`)と制約日付(`ConstraintDate`)を、メモリ上の高速なデータ構造(`Dictionary`オブジェクト等)に退避させる。
2. Constraint Stripping(制約の剥離): すべてのタスクの制約を `pjTaskConstraintNone`(なし)へと強制的に書き換える。
3. Engine Recalculation(エンジンの再計算): `Project.Calculate` を実行し、論理的な依存関係のみに基づいた「純粋な」スケジュールを算出する。
4. Bottleneck Analysis(ボトルネック解析): 剥離前後の終了日の差分、および総スラックの変動を解析し、どの制約がプロジェクトを最も圧迫しているかを特定する。
5. State Restoration(状態の復元): 退避させていたデータを元に、元の制約を正確に再適用する。
2. 実装における技術的障壁とメモリ管理
このプロセスを大規模プロジェクト(数万タスク規模)で実行する場合、単純なループ処理では致命的なパフォーマンス低下、あるいはメモリリークを引き起こす。
COMオブジェクトのライフサイクル管理
MS ProjectのタスクオブジェクトはCOM(Component Object Model)インターフェースを介して操作される。ループ内で大量の `Task` オブジェクトを参照する場合、スコープを適切に管理し、明示的に `Set task = Nothing` を行うことが、ガベージコレクションの負荷を軽減し、メモリの断片化を防ぐ鍵となる。
Windows APIによる制御
計算負荷が高いプロセスでは、OSレベルでのスリープや、高精度なプロファイリングが必要になる。ここでは、`kernel32.dll` から `Sleep` 関数を呼び出し、計算エンジンのプロセスに「呼吸」をさせる手法を取り入れる。
3. 極限の最適化コード:ConstraintOptimizer クラス
以下に、プロフェッショナルな現場で使用に耐えうる、堅牢な実装例を示す。
Option Explicit
' Windows APIの宣言: 計算エンジンの負荷を制御するため
If VBA7 Then
Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As LongPtr)
Else
Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
End If
' 制約情報を保持するためのカスタム型
Private Type ConstraintState
ConstraintType As Long
ConstraintDate As Variant
End Type
' メモリ効率を考慮した制約保持用辞書
Private m_ConstraintMap As Object ' Scripting.Dictionary
''' <summary>
''' プロジェクトの制約を解析・最適化するアーキテクト・クラス
''' </summary>
Public Class ConstraintOptimizer
Private m_Project As MSProject.Project
Public Sub New(ByRef proj As MSProject.Project)
Set m_Project = proj
Set m_ConstraintMap = CreateObject("Scripting.Dictionary")
End Sub
''' <summary>
''' 全タスクの制約を一時的に解除し、理論上の最短期間を算出する
''' </summary>
Public Function OptimizeSchedule() As Double
Dim t As MSProject.Task
Dim originalFinish As MSProject.Date
Dim optimizedFinish As MSProject.Date
Dim taskId As Long
On Error GoTo ErrorHandler
' 現在のプロジェクト終了日を記録
originalFinish = m_Project.FinishDate
' 高速化のため、計算モードを自動に設定
m_Project.Calculation = pjManual
Debug.Print "--- Optimization Started ---"
' 1. State Capture & Stripping
' 大規模プロジェクトでは、タスクの走査自体がコストになる
For Each t In m_Project.Tasks
If Not t Is Nothing Then
taskId = t.ID
' 制約がある場合のみ記録
If t.ConstraintType <> pjTaskConstraintNone Then
Dim state As ConstraintState
state.ConstraintType = t.ConstraintType
state.ConstraintDate = t.ConstraintDate
' 辞書に格納 (Key: Task ID, Value: Custom Type)
' ※VBAのDictionaryはUserDefinedTypeを直接保持できないため、
' 実際の実装ではClassまたは配列を組み合わせて使用する
m_ConstraintMap.Add taskId, Array(state.ConstraintType, state.ConstraintDate)
' 制約を解除
t.ConstraintType = pjTaskConstraintNone
End If
End If
Next t
' 2. Engine Recalculation
' 制約が剥離された状態で、論理依存関係のみによる再計算を実行
m_Project.Calculate
optimizedFinish = m_Project.FinishDate
Debug.Print "Original Finish: " & originalFinish
Debug.Print "Optimized Finish: " & optimizedFinish
Debug.Print "Potential Compression: " & DateDiff("d", optimizedFinish, originalFinish) & " days"
' 3. State Restoration (Rollback)
RestoreConstraints
' 再計算して元の状態に戻す
m_Project.Calculation = pjAutomatic
m_Project.Calculate
OptimizeSchedule = DateDiff("d", optimizedFinish, originalFinish)
Exit Function
ErrorHandler:
Debug.Print "Critical Error: " & Err.Description
RestoreConstraints ' エラー時でも必ず制約を復元する(整合性確保)
Err.Raise Err.Number, "ConstraintOptimizer", Err.Description
End Function
''' <summary>
''' 保持していた制約を元のタスクに再適用する
''' </summary>
Private Sub RestoreConstraints()
Dim taskId As Variant
Dim data As Variant
Dim t As MSProject.Task
If m_ConstraintMap.Count = 0 Then Exit Sub
For Each taskId In m_ConstraintMap.Keys
Set t = m_Project.Tasks(taskId)
If Not t Is Nothing Then
data = m_ConstraintMap(taskId)
t.ConstraintType = data(0)
t.ConstraintDate = data(1)
End If
' メモリ解放の明示的呼び出し
Set t = Nothing
Next taskId
' 辞書のクリア
m_ConstraintMap.RemoveAll
Set m_ConstraintMap = Nothing
' OSへの制御権譲渡
DoEvents
Sleep 100
End Sub
End Class
4. アーキテクトの洞察:ボトルネックの正体
このアルゴリズムを実行した際、`Potential Compression`(圧縮可能期間)が極端に大きい場合、そのプロジェクトには「論理的依存関係を無視した、管理上の都合による制約」が蔓延していることを意味する。
シニアエンジニアが注目すべきは、単に期間を短縮することではない。
「どの制約を外したときに、最もクリティカルパスが劇的に変化するか」を特定することだ。
- Constraint-Induced Critical Path: 制約によって強制的にクリティカルになったパス。これはリソースの最適配分を阻害する。
- Hidden Critical Path: 制約によって隠蔽されていた、真にリソース不足や論理的詰まりを起こしているパス。
これらを区別することで、プロジェクトマネージャーに対して「制約を外して、論理的な依存関係に基づいたリソース投入を行え」という、データに基づいた強力な提言が可能になる。
5. 結論
MS Project VBAを用いた最適化の本質は、UIが提供する「予定管理」の枠組みを超え、プロジェクトを「動的なネットワークモデル」として再定義することにある。
制約を「絶対的な事実」として受け入れるのではなく、計算機科学的な「変数」として扱い、一時的に剥離・解析する。このアプローチこそが、レガシーな管理手法から脱却し、真のプロジェクト・デリバリーを実現するためのエンジニアリングの真髄である。
*Author: Chief Architect of Automation Systems*
