制約とリソースの衝突を制する:MS Project VBAにおける「動的制約緩和」の設計思想
プロジェクトマネジメントにおいて、スケジュールは常に「理想」と「現実」の戦場だ。
リソース平準化(Resource Leveling)を実行した瞬間、多くのPMは絶望する。リソースの重複を解消するために、タスクが次々と後ろへ押し出され、あらかじめ設定していた「開始日制約(Constraint)」と衝突し、スケジュールが不整合を起こすからだ。
「制約を守るためにリソースを過負荷にするか?」
「リソースを優先するために制約を無視するか?」
この二者択一を、手作業で、あるいは単純なループ処理で解決しようとする者は、保守性の低い、バグの温床となるツールを生み出す。本稿では、伝説的なアーキテクトの視点から、「リソース平準化による制約衝突を、ロジカルかつ自動的に解決する」ための高度な設計手法と、プロダクションレベルのVBA実装を伝授する。
1. 根本的な設計思想:なぜ、単なる「ループ処理」では失敗するのか
初心者が書くコードは、`For Each task In ActiveProject.Tasks` で全タスクを回し、制約があれば書き換える、といった単純なものだ。しかし、これでは実務では通用しない。
オブジェクトのライフサイクルと計算エンジンのオーバーヘッド
MS Projectの最大の特徴であり、最大の罠は、「プロパティを一つ書き換えるたびに、スケジュール計算エンジンが走る可能性がある」ことだ。
タスクの制約を一つ変更し、その直後に次のタスクを判定する。このとき、エンジンがバックグラウンドで再計算を繰り返すと、計算量は指数関数的に増大し、大規模プロジェクトではExcelがフリーズしたかのような挙動を示す。
制約(Constraint)と優先順位(Priority)の力学
MS Projectにおける「制約」は、スケジューリングエンジンに対する「命令」だ。一方で「優先順位」は「示唆」に過ぎない。
平準化のロジックを組む際は、「制約を緩和する(Constraint Relaxation)」のか、「タスクの優先度を操作してエンジンの判断を誘導する(Priority Steering)」のか、その戦略を明確に分離して設計する必要がある。
2. アーキテクチャ設計:判定ロジックの分離
堅牢なツールを作るには、以下の3層構造で設計すべきである。
1. Detection Layer (検知層): 平準化後に「制約によってリソース平準化が阻害されている箇所」を特定する。
2. Decision Layer (意思決定層): 「制約を緩めるべきか、それともリソースを割り当てるべきか」のビジネスルールを適用する。
3. Execution Layer (実行層): 計算エンジンを最小限の負荷で制御しながら、オブジェクトを書き換える。
3. プロダクションコード:Constraint Resolver
以下に示すのは、リソース平準化後の「制約衝突」を検知し、事前に定義したルールに基づいて制約を `As Soon As Possible (ASAP)` へ自動緩和する、実戦的なクラスモジュールを想定したコード例だ。
Option Explicit
' ==============================================================================
' Module: SchedulingOptimizer
' Description: リソース平準化後の制約衝突を解決する高度な制御ロジック
' ==============================================================================
' 制約緩和の戦略を定義する列挙型
Public Enum RelaxationStrategy
Strategy_Strict ' 制約を絶対遵守(緩和しない)
Strategy_Flexible ' 制約をASAPに緩和してリソースを優先
Strategy_PriorityOnly ' 制約は維持し、優先度のみで調整を試みる
End Enum
''' <summary>
''' スケジュール衝突を解決するメインルーチン
''' </summary>
''' <param name="strategy">適用する緩和戦略</param>
Public Sub ResolveConstraintConflicts(ByVal strategy As RelaxationStrategy)
Dim t As Task
Dim proj As Project
Set proj = ActiveProject
' --- [重要] パフォーマンス最適化: 計算モードをマニュアルに切り替え ---
' これを行わないと、タスク変更のたびに再計算が走り、処理が極端に遅くなる
Dim originalCalculation As Long
originalCalculation = Application.Calculation
Application.Calculation = pjManual
On Error GoTo ErrorHandler
Debug.Print "--- Optimization Started: " & Now & " ---"
' 1. 検知と実行のループ
' 集合操作やコレクションを使用せず、直接Taskオブジェクトを走査するが、
' 変更によるインデックスのズレを防ぐため、プロジェクトの全タスクを走査する
For Each t In proj.Tasks
' 無効なタスク(空行など)をスキップ
If Not t Is Nothing Then
If t.Type <> pjTaskNull Then
' 衝突判定ロジックの呼び出し
If ShouldRelaxConstraint(t, strategy) Then
' 実行層: 制約の緩和
ApplyRelaxation t, strategy
End If
End If
End If
Next t
' --- [重要] 計算エンジンの再起動 ---
' 全ての変更が終わった後に一括で再計算させる
Application.Calculation = pjCalculation
Application.Calculate
Debug.Print "--- Optimization Completed Successfully ---"
MsgBox "スケジュールの最適化が完了しました。", vbInformation
Exit Sub
ErrorHandler:
' エラー発生時も必ず計算モードを元に戻す(これを行わないとMS Projectが使い物にならなくなる)
Application.Calculation = originalCalculation
MsgBox "致命的なエラーが発生しました: " & Err.Description, vbCritical
End Sub
''' <summary>
''' 緩和すべきかどうかを判定するロジック(Decision Layer)
''' </summary>
Private Function ShouldRelaxConstraint(ByVal t As Task, ByVal strategy As RelaxationStrategy) As Boolean
' 戦略がStrictの場合は即座に終了
If strategy = Strategy_Strict Then
ShouldRelaxConstraint = False
Exit Function
End If
' 判定条件の設計:
' 1. 制約が "Must Start On" や "Start No Earlier Than" であるか
' 2. かつ、そのタスクがリソース不足(Overallocated)の状態にあるか
' ※ 本来は、平準化によって「本来動けるはずなのに制約で止まっている」タスクを特定する
Dim isHardConstraint As Boolean
isHardConstraint = (t.ConstraintType = pjTaskConstraintMustStartOn Or _
t.ConstraintType = pjTaskConstraintStartNoEarlierThan Or _
t.ConstraintType = pjTaskConstraintStartNoLaterThan)
' リソースの過負荷判定(簡易的な実装)
Dim isOverallocated As Boolean
isOverallocated = t.ResourceOverallocated
' 判定ロジックの結合
If isHardConstraint And isOverallocated Then
ShouldRelaxConstraint = True
Else
ShouldRelaxConstraint = False
End If
End Function
''' <summary>
''' 実際にオブジェクトの状態を変更する(Execution Layer)
''' </summary>
Private Sub ApplyRelaxation(ByRef t As Task, ByVal strategy As RelaxationStrategy)
Select Case strategy
Case Strategy_Flexible
' 制約を ASAP に変更し、リソースの空き時間に滑り込ませる
t.ConstraintType = pjTaskConstraintAsSoonAsPossible
' 念のため優先度を少し上げることで、次回の平準化時に有利にする
If t.Priority < 1000 Then t.Priority = t.Priority + 100
Debug.Print "Relaxed: Task [" & t.Name & "] to ASAP"
Case Strategy_PriorityOnly
' 制約は変えず、優先度だけを極限まで高めてエンジンの挙動を促す
t.Priority = 1000
Debug.Print "Priority Boosted: Task [" & t.Name & "]"
End Select
End Sub
4. 現場で生き残るための「鉄則」
データベース・外部連携時の注意点
このツールをExcelや外部SQLデータベースと連携させる場合、「MS Projectのオブジェクトを直接DBに書き込まない」ことが鉄則だ。
VBAで計算を回している最中に、外部へのI/O(ファイル書き込みやSQL実行)を差し込むと、オブジェクトのロック競合が発生し、非常に不安定になる。
1. Extract: プロジェクトの状態をメモリ上の構造体(またはDictionary)に抽出する。
2. Process: VBA内でロジックを完結させる。
3. Load: 変更結果をプロジェクトに書き戻し、最後に一括で計算させる。
堅牢なエラーハンドリング
上記のコードでも実装しているが、`Application.Calculation` の制御は、「必ず `ErrorHandler` で元の状態に戻す」ことを保証せよ。これに失敗すると、ユーザーの手元にあるMS Projectは、値を変えるたびに数分間固まる「壊れたツール」へと成り下がる。
5. 結論
リソース平準化と制約の衝突は、プロジェクトにおける「物理的な限界」と「人間が設定したルール」の衝突である。
優れたエンジニアは、単にエラーを消すためのコードを書かない。エンジンの動作原理を理解し、計算負荷を最小化し、ビジネスロジック(制約を優先するかリソースを優先するか)をコードの構造として分離する。
この設計思想を身につけたとき、君の書くVBAは単なる「自動化スクリプト」から、プロジェクトを支える「インテリジェントなエンジン」へと昇華するはずだ。
