制約の鎖を解き放て:Project VBAによる「スケジュール最適化エンジン」の設計思想
プロジェクトマネジメントの現場において、MS Projectは単なる「ガントチャート描画ツール」ではない。それは、複雑な依存関係とリソースの制約が絡み合う「動的な計算エンジン」である。
しかし、多くの開発者が陥る罠がある。それは、ユーザーが入力した「開始日指定(Must Start On)」や「開始後指定(Start No Earlier Than)」といった「制約条件(Constraints)」を、そのままの状態でスケジュール計算に投入してしまうことだ。
不適切な制約は、ネットワーク図(論理関係)の整合性を破壊し、本来存在するはずの「クリティカルパス」を隠蔽し、プロジェクトの最短終了日を不当に遅らせる。
本稿では、プロフェッショナルな自動化エンジニアとして、これらの「人工的なボトルネック」を特定し、一時的に制約を解除することでプロジェクトの理論的限界値(Theoretical Minimum Finish Date)を算出する、高度な最適化アルゴリズムの設計と実装について解説する。
1. アーキテクチャの核心:なぜ「単純な削除」ではいけないのか
初心者は、制約を解除するために単に `Task.ConstraintType = pjConstraintAsSoonAsPossible` と書き換える。だが、これはエンジニアリングとして極めて脆弱な設計だ。
なぜなら、「計算(Calculation)」は「状態(State)」の遷移であるからだ。
もし、制約の解除中にエラーが発生し、プログラムが中断されたらどうなるか? ユーザーの元々のスケジュール設定は破壊され、復元不可能な状態に陥る。これはツールとして「致命的な欠陥」である。
我々が実装すべきは、「トランザクション・パターン」である。
1. Snapshot: 現在の制約状態(ConstraintTypeと日付)をメモリ上に完全にバックアップする。
2. Transformation: 全ての制約を「ASAP(可能な限り早く)」に一括変換する。
3. Calculation: エンジンを再計算させ、理論上の最短終了日を抽出する。
4. Rollback / Commit: 状態を元の設定に戻す(あるいは、最適化された新しい制約を適用する)。
この設計思想こそが、堅牢な業務アプリケーションを支える脊髄となる。
2. 実装:制約制御最適化エンジン
ここでは、保守性を極限まで高めるため、制約情報を保持するためのクラスモジュールと、計算を実行するメインロジックに分けて実装する。
クラスモジュール: `clsConstraintState`
まず、個々のタスクの「元の状態」を封じ込めるためのコンテナを作成する。
' --- Class Module: clsConstraintState ---
Option Explicit
Public TaskID As Long
Public OriginalConstraintType As Long
Public OriginalDate As Variant ' Date型はnullの可能性があるためVariant
' コンストラクタ的な役割
Public Sub InitializeState(t As MSProject.Task)
Me.TaskID = t.ID
Me.OriginalConstraintType = t.ConstraintType
Me.OriginalDate = t.ConstraintDate
End Sub
メインモジュール: `modOptimizer`
次に、エンジンの本体を実装する。ここでは、`On Error GoTo` を用いた厳格なエラーハンドリングと、計算負荷を抑えるための `Calculation` モードの制御が肝となる。
' --- Standard Module: modOptimizer ---
Option Explicit
''' <summary>
''' プロジェクトの制約を一時的に解除し、理論上の最短終了日を算出する
''' </summary>
Public Sub OptimizeProjectSchedule()
Dim proj As MSProject.Project
Set proj = Application.ActiveProject
' 状態を保存するためのコレクション
Dim backupCollection As New Collection
Dim t As MSProject.Task
Dim state As clsConstraintState
' 1. トランザクション開始:現在の状態をバックアップ
' ---------------------------------------------------------
On Error GoTo ErrorHandler
' 計算モードを一時的に手動にし、処理中の再計算によるオーバーヘッドを防ぐ
Application.Calculation = pjCalculationManual
For Each t In proj.Tasks
' 空の行やサマリータスクを除外(必要に応じて条件を調整)
If Not t Is Nothing Then
If t.ConstraintType <> pjConstraintAsSoonAsPossible Then
' 制約があるタスクのみをバックアップ
Set state = New clsConstraintState
state.InitializeState(t)
backupCollection.Add state, CStr(t.ID)
' 制約を解除(ASAPへ)
t.ConstraintType = pjConstraintAsSoonAsPossible
End If
End If
Next t
' 2. 再計算の実行
' ---------------------------------------------------------
' 全ての制約が外れた状態で、プロジェクトの論理関係のみで計算させる
Application.Calculation = pjCalculationAutomatic
Application.Calculate
Dim theoreticalFinish As Date
theoreticalFinish = proj.FinishDate
MsgBox "最適化計算完了。" & vbCrLf & _
"理論上の最短終了日: " & Format(theoreticalFinish, "yyyy/mm/dd"), vbInformation, "Optimizer Result"
' 3. ロールバック:元の状態へ復元
' ---------------------------------------------------------
' ここでエラーが起きても、必ず元の状態に戻るように設計する
RestoreConstraints backupCollection
' 計算モードを元に戻す(通常は自動)
Application.Calculation = pjCalculationAutomatic
MsgBox "スケジュールを元の制約状態に復元しました。", vbInformation
Exit Sub
ErrorHandler:
MsgBox "致命的なエラーが発生しました。復旧を試みます。" & vbCrLf & _
"Error: " & Err.Description, vbCritical
' エラー時でも、可能な限りバックアップから復元を試みる
RestoreConstraints backupCollection
Application.Calculation = pjCalculationAutomatic
End Sub
''' <summary>
''' バックアップコレクションから制約を復元する
''' </summary>
Private Sub RestoreConstraints(ByRef col As Collection)
Dim state As clsConstraintState
Dim t As MSProject.Task
Dim i As Long
' コレクション内の各状態を適用
On Error Resume Next ' 復元中のエラーは最小限に抑える
For i = 1 To col.Count
Set state = col(i)
Set t = Application.ActiveProject.Tasks(state.TaskID)
If Not t Is Nothing Then
t.ConstraintType = state.OriginalConstraintType
' 日付制約がある場合のみ適用
If state.OriginalConstraintType <> pjConstraintAsSoonAsPossible Then
t.ConstraintDate = state.OriginalDate
End If
End If
Next i
On Error GoTo 0
End Sub
3. プロフェッショナルとしての設計指針
このコードをプロダクション環境に投入するにあたり、以下の3点を肝に銘じてほしい。
① パフォーマンスの重みを知れ
`For Each t In proj.Tasks` は、タスク数が数千規模になると無視できない時間を要する。
計算エンジンを回す前に `Application.Calculation = pjCalculationManual` を設定しているのは、タスクを一つずつ書き換えるたびにMS Projectが全プロジェクトを再計算する「計算地獄」を避けるためだ。これは、大規模プロジェクトを扱うエンジニアにとっての鉄則である。
② データの整合性と外部連携
もしこの結果をデータベース(SQL ServerやAccess)に書き出す場合、単に「日付」だけを保存してはならない。
「どの制約を解除した結果、その日付になったのか」というメタデータ(制約の種類、解除したタスクIDのリスト)をセットで保存すべきだ。これがないと、後からの監査や「なぜスケジュールが変動したのか」という問いに答えられなくなる。
③ 境界条件の考慮
サマリータスク(中間工程)に対して制約を解除すべきか、それともリーフタスク(末端作業)のみに限定すべきか。これはプロジェクトの性質に依存する。
上記のコードでは、全ての制約を対象としているが、実務では「特定の属性(例:`Text1 = “OptimizationTarget”`)」を持つタスクのみを対象とするように、フィルタリングロジックを組み込むのが、真に「使える」ツールへの道だ。
結論
スケジュール最適化の本質は、制約を消すことではない。「制約が及ぼしている影響を可視化すること」にある。
VBAを用いて、一時的な「自由な状態」を作り出し、エンジンの真の力を引き出す。このアプローチができるようになれば、あなたは単なる「自動化担当者」から、プロジェクトの構造をコントロールする「アーキテクト」へと昇華するだろう。
