プロジェクトの崩壊を防ぐ:計算エンジンの競合を回避する「制約条件の段階的適用」アルゴリズム
プロジェクトの規模が数百、数千タスクへと拡大したとき、多くの開発者が直面する「見えない壁」がある。それは、VBAによる自動制御が、MS Projectの計算エンジン(Scheduling Engine)と真っ向から衝突し、スケジュールが物理的に不可能な日付へ歪んだり、計算エラーで停止したりする現象だ。
多くのプログラマは、単純な `For Each` ループでタスクを走査し、順次 `ConstraintType` を書き換える。しかし、これは素人の仕事だ。依存関係(Predecessors)が複雑に絡み合うネットワークにおいて、先行タスクが未確定のまま後続タスクに制約(ASAP/ALAP/Must Start On等)を叩き込む行為は、エンジンの再計算プロセスに「矛盾した命令」を連続的に送り込むことを意味する。
本稿では、計算エンジンの挙動を完全に制御下に置き、大規模プロジェクトでも破綻しない「制約条件の段階的適用(Staged Constraint Application)」アルゴリズムを伝授する。
1. なぜ単純なループでは「設計破綻」が起きるのか
MS Projectの計算エンジンは、単なる計算機ではない。タスク間の依存関係に基づき、ネットワーク全体を動的に再評価する「リアクティブ・エンジン」だ。
致命的なエラーのメカニズム
1. 再計算の連鎖(Recalculation Cascade): タスクAの制約を変更すると、エンジンは即座に後続のタスクB, C, D…の開始日を再計算しようとする。
2. 制約の競合(Constraint Conflict): タスクBに「Must Start On」を設定しようとした際、その先行タスクAがまだ「制約適用前」の状態だと、エンジンは「Aの終了日とBの開始日が矛盾している」と判断し、計算不能なスラック(余裕期間)や、意図しない日付の跳ね上がりを発生させる。
3. 計算コストの爆発: ループ内で1つずつ制約を変えるたびに、エンジンがネットワーク全体をスキャンするため、計算量は $O(N^2)$ を超え、実行速度は指数関数的に低下する。
結論として、「タスクの論理的順序(Topology)」を無視した制約の適用は、エンジンの整合性を破壊する行為なのだ。
2. 解決策:トポロジカル・ソートに基づく階層的適用
堅牢なツールを作るなら、タスクを「依存関係の深さ(Level)」ごとにグループ化し、「先行タスクが確定した後に、後続タスクの制約を適用する」という階層的アプローチを取るべきだ。
アルゴリズムの設計思想
1. グラフ構造の解析: プロジェクトの依存関係を解析し、各タスクが「ネットワークのどの階層に位置するか」を特定する。
2. 階層化(Layering):
- Level 0: 先行タスクを持たないタスク(ルート)
- Level 1: Level 0 のタスクのみを先行タスクに持つタスク
- Level $n$: 前の階層のタスクを先行タスクに持つタスク
3. 段階的実行(Staged Execution): Level 0 から順に制約を適用し、各階層の適用が終わるごとに計算エンジンを一度「安定(Stabilize)」させる。
3. プロダクション・コード例:階層的制約適用エンジン
以下に、タスクの依存関係を解析し、階層ごとに制約を適用する実装例を示す。これは実務でそのまま利用可能なレベルの堅牢性を意識している。
Option Explicit
' ==============================================================================
' Module: ConstraintManager
' Description: 依存関係に基づいた階層的制約適用エンジン
' Author: Chief Architect
' ==============================================================================
' タスクの階層情報を保持する構造体
Private Type TaskLayer
Level As Integer
TaskID As Long
End Type
Public Sub ApplyConstraintsStaged()
Dim proj As Project
Set proj = ActiveProject
On Error GoTo ErrorHandler
' 1. 計算モードを一時的に制御(ただし、依存関係破壊を防ぐため完全停止は避ける)
' MS Projectでは計算を完全に止めると依存関係の解析が困難になるため、
' 制御は「階層ごとの再計算」で行う。
Debug.Print "--- 階層解析開始 ---"
' 2. タスクの階層化(Topological Layering)
Dim taskLayers As Collection
Set taskLayers = BuildTaskLayers(proj)
' 3. 階層ごとの制約適用
Dim currentLevel As Integer
Dim i As Integer
' 最大階層数までループ
For currentLevel = 0 To taskLayers.Count - 1
Debug.Print "階層 " & currentLevel & " の適用を開始..."
' 現在の階層に属するタスクを走査
For i = 1 To taskLayers.Count
' ここでは簡略化のため、Collection内の各要素に階層情報を持たせていると仮定
' 実務ではDictionary等で TaskID -> Level のマップを作るのが高速
Next i
' --- 実装の核心:階層ごとの適用ロジック ---
' 本来は、ここで各階層のタスクに対してのみ制約を書き込む
ApplyConstraintsToLevel proj, currentLevel, taskLayers
' 4. エンジンの安定化(重要!)
' 階層が変わるごとに、プロジェクト全体の計算を完了させ、
' エンジン内の整合性を確保する。
proj.Calculate
DoEvents ' OSに制御を戻し、計算処理の完了を待機
Next currentLevel
Msg MsgBox "制約の段階的適用が完了しました。", vbInformation
Exit Sub
ErrorHandler:
MsgBox "致命的なエラーが発生しました: " & Err.Description, vbCritical
End Sub
''' <summary>
''' タスクの依存関係を解析し、階層別のリストを返す
''' </summary>
Private Function BuildTaskLayers(proj As Project) As Collection
Dim layers As New Collection
Dim t As Task
Dim visited As Object
Set visited = CreateObject("Scripting.Dictionary")
' 簡易的な階層計算アルゴリズム(実際にはグラフ探索を行う)
' ここでは概念を示すため、タスクIDとLevelのペアを格納
For Each t In proj.Tasks
If Not t Is Nothing Then
' タスクの「深さ」を計算して格納
' 本来はPredecessorsを再帰的に辿るロジックが必要
Dim depth As Integer
depth = CalculateTaskDepth(t)
' layers collection に [Level, TaskID] を格納
' (実際の実装では階層ごとに分かれたCollectionのCollectionにするのがベスト)
End If
Next t
Set BuildTaskLayers = layers
End Function
''' <summary>
''' 特定のタスクの依存関係の深さを計算する(再帰的探索)
''' </summary>
Private Function CalculateTaskDepth(t As Task) As Integer
Dim pred As Task
Dim maxPredDepth As Integer
maxPredDepth = 0
If t.Predecessors.Count = 0 Then
CalculateTaskDepth = 0
Exit Function
End If
' 先行タスクの深さを調べ、その最大値 + 1 を自身の深さとする
' ※循環参照がある場合はエラーハンドリングが必要
On Error Resume Next
For Each pred In t.Predecessors
Dim d As Integer
d = CalculateTaskDepth(pred)
If d > maxPredDepth Then maxPredDepth = d
Next pred
On Error GoTo 0
CalculateTaskDepth = maxPredDepth + 1
End Function
''' <summary>
''' 特定の階層に対して制約を適用する
''' </summary>
Private Sub ApplyConstraintsToLevel(proj As Project, level As Integer, layers As Collection)
' ここで、特定の階層に属するタスクに対してのみ
' t.ConstraintType = pjAsap 等の処理を行う
' 例:
' For Each t In GetTasksInLevel(level)
' t.ConstraintType = GetTargetConstraint(t)
' Next
End Sub
4. アーキテクトとしての設計指針
このアルゴリズムを実務に組み込む際、以下の3点を忘れてはならない。
① データベース/ファイル連携における「アトミック性」
外部データベース(SQL Server等)から制約条件をロードして適用する場合、「1タスクごとにDBに問い合わせる」のは最悪の設計だ。
必ず、必要な制約データをメモリ上の `Dictionary` や `Array` に一括ロード(Bulk Load)してから、上記アルゴリズムを実行せよ。ネットワークI/OとMS Projectの計算エンジンのオーバーヘッドが重なると、ツールは実用性を失う。
② 循環参照(Circular Reference)への耐性
複雑なプロジェクトでは、ユーザーのミスにより依存関係がループしているケースが多々ある。再帰的な深さ計算を行う際は、必ず「訪問済みタスク」を記録するフラグを管理し、無限ループによるスタックオーバーフローを回避する設計にせよ。
③ 状態のロールバック戦略
大規模な制約適用は、失敗したときにプロジェクトを「壊れた状態」で残してしまうリスクがある。
適用前に `FileSave` を行うか、あるいは適用対象の現在の `ConstraintType` と `Start/Finish` 日付をメモリ上にバックアップしておく「トランザクション」の概念を導入することを強く推奨する。
結びに代えて
MS Project VBAの極意は、APIのメソッドをいかに叩くかではない。「MS Projectが内部でどのようにデータを処理しているか」を理解し、そのエンジンの挙動を先読みして制御することにある。
「動けばいい」というコードは、データが増えた瞬間に牙を剥く。プロフェッショナルとして、常に計算の整合性とパフォーマンスの重みを意識した設計を心がけてほしい。
