Project VBAを掌握する極限の知見:依存関係と制約条件の矛盾を検知する「整合性チェック」エンジンの設計
プロジェクト管理ツールとしてのMicrosoft Projectは、極めて強力な計算エンジンを内蔵している。しかし、その強力さゆえの「落とし穴」がある。それは、「制約条件(Constraint)」と「依存関係(Dependency)」が論理的に衝突した際、Projectはエラーを吐かずに、ただ黙ってスケジュールを歪ませるということだ。
「Must Start On(開始日指定)」を設定したタスクに対し、その前工程が物理的に間に合わない場合、Projectは制約を優先して前工程の依存関係を無視するか、あるいは制約を無視してスケジュールを動かす。これが、現場で「計画が破綻しているのに、ツール上では正常に見える」という最悪の事態を招く。
プロフェッショナルな自動化エンジニアなら、単に日付を書き換えるコードを書くのではない。プロジェクトの論理的整合性を担保する「検証エンジン」を実装すべきだ。
本記事では、依存関係の循環参照(Cycle)と、制約条件の論理矛盾を再帰的に検知する、プロダクションレベルのアルゴリズムとその実装方法を伝授する。
1. なぜ、あなたのVBAは「遅い」のか:再計算の嵐(Recalculation Storm)を避けよ
実装に入る前に、設計思想として最も重要な話をさせてほしい。多くの開発者が犯す最大のミスは、タスクのプロパティをループ内で書き換えながら、同時にその値を参照してチェックを行おうとすることだ。
Microsoft Projectのオブジェクトモデルにおいて、`Task.Start` や `Task.Finish` といったプロパティへのアクセス、あるいは依存関係(`Task.Predecessors`)の変更は、即座にプロジェクト全体の再計算をトリガーする可能性がある。
ループの中でプロパティを叩くたびに計算エンジンが走り、計算が終わるのを待ってから次のループへ進む……。これでは、数千タスクを持つプロジェクトにおいて、処理時間は指数関数的に増大する。
アーキテクトの鉄則:
1. Calculation Modeを `pjManual` に設定せよ。
2. 計算結果の「スナップショット」をメモリ上で評価する設計にせよ。
3. 依存関係のグラフ構造を論理的に走査せよ。
2. 論理矛盾の正体:検知すべき2つの致命的エラー
エンジンの設計において、我々が検知すべき矛盾は大きく分けて2種類ある。
① 循環依存(Circular Dependency)
タスクA $\rightarrow$ タスクB $\rightarrow$ タスクC $\rightarrow$ タスクA というループだ。これはグラフ理論における「Directed Acyclic Graph (DAG)」の原則に反する。これが存在すると、スケジュール計算は無限ループに陥るか、破綻する。
② 制約条件の衝突(Constraint vs. Linkage Conflict)
例えば、「タスクAは金曜日に開始しなければならない(Must Start On)」という制約がある一方で、「タスクB(タスクAの後続)は月曜日に開始する」という依存関係がある場合。この「制約」と「依存関係」の物理的な不整合を、計算結果が出る前に(あるいは計算結果の矛盾として)抽出する必要がある。
3. 実装:整合性チェック・エンジン(Class Module設計)
標準モジュールに全てを書き込むような素人のコードは捨てろ。保守性と再利用性を高めるため、Class Module(クラスモジュール) を使用して、エンジンをカプセル化する。
以下に、プロダクション品質のコード例を示す。
クラスモジュール名: `IntegrityChecker`
' ======================================================================================
' Class: IntegrityChecker
' Description: プロジェクトの論理整合性(循環参照および制約矛盾)を検証するエンジン
' ======================================================================================
Option Explicit
Private pApp As MSProject.Application
Private pErrors As Collection
' 矛盾情報を格納するためのカスタム型(エラー内容、対象タスク、メッセージ)
Public Type IntegrityError
TaskID As Long
TaskName As String
ErrorType As String
Message As String
End Type
' エラーリストを保持するコレクション
Private mErrors As Collection
Private Sub Class_Initialize()
Set mErrors = New Collection
End Sub
''' <summary>
''' メインエントリーポイント:プロジェクト全体の整合性をチェックする
''' </summary>
Public Function CheckProjectIntegrity() As Boolean
Dim t As MSProject.Task
Dim visited As Object ' 循環参照検知用のDictionary
Set visited = CreateObject("Scripting.Dictionary")
mErrors.Clear
' 1. 計算モードをマニュアルに設定(パフォーマンス確保)
pApp.Calculation = pjManual
On Error GoTo ErrorHandler
' 2. 全タスクを走査
For Each t In pApp.ActiveProject.Tasks
If Not t Is Nothing Then
' A. 循環参照の検知(再帰的探索)
If Not CheckCircularDependency(t, visited) Then
' 循環は見つかったが、CheckCircularDependency内でエラー登録済み
End If
' B. 制約条件と依存関係の衝突検知
CheckConstraintConflict t
End If
Next t
' 計算を自動に戻す
pApp.Calculation = pjAutomatic
' エラーがなければTrue
CheckProjectIntegrity = (mErrors.Count = 0)
Exit Function
ErrorHandler:
pApp.Calculation = pjAutomatic
MsgBox "Critical Error in Integrity Engine: " & Err.Description, vbCritical
CheckProjectIntegrity = False
End Function
''' <summary>
''' 依存関係の循環を検知する(DFS: 深さ優先探索アルゴリズム)
''' </summary>
Private Function CheckCircularDependency(ByVal currentTask As MSProject.Task, ByRef visited As Object) As Boolean
' すでに探索中のノードであれば循環確定
If visited.Exists(currentTask.ID) Then
AddError currentTask.ID, currentTask.Name, "Circular Dependency", "循環的な依存関係が検出されました。"
CheckCircularDependency = True
Exit Function
End If
' 探索済みノードの管理
visited.Add currentTask.ID, True
Dim pred As MSProject.Predecessor
For Each pred In currentTask.Predecessors
' 再帰呼び出し
If CheckCircularDependency(pred.Task, visited) Then
CheckCircularDependency = True
Exit Function
End If
Next pred
' 探索終了時にDictionaryから削除(バックトラック)
visited.Remove currentTask.ID
CheckCircularDependency = False
End Function
''' <summary>
''' 制約条件と実際のスケジュール日付の矛盾を検知
''' </summary>
Private Sub CheckConstraintConflict(ByVal t As MSProject.Task)
' 例: Must Start On 制約がある場合、計算されたStart Dateとの乖離をチェック
' ※Projectの仕様上、制約が強いと日付が強制されるが、
' 依存関係との間で「本来あるべき姿」との乖離を検知するロジックをここに実装
If t.ConstraintType = pjMustStartOn Then
' 本来の制約日と、現在のタスク開始日の不一致(微小な誤差を許容する場合もある)
' ここでは厳密にチェックする例
If Abs(t.Start - t.ConstraintDate) > 0.0001 Then ' 日付の浮動小数点誤差考慮
AddError t.ID, t.Name, "Constraint Conflict", "Must Start On 制約が依存関係によって無視されています。"
End If
End If
' 他の制約(Must Finish On, Start No Earlier Than等)も同様に拡張可能
End Sub
' エラー登録用プライベートメソッド
Private Sub AddError(id As Long, name As String, errType As String, msg As String)
Dim errEntry As IntegrityError
errEntry.TaskID = id
errEntry.TaskName = name
errEntry.ErrorType = errType
errEntry.Message = msg
mErrors.Add errEntry
End Sub
' エラーリストの取得
Public Property Get Errors() As Collection
Set Errors = mErrors
End Property
' Applicationの参照保持
Public Property Set App(ByVal app As MSProject.Application)
Set pApp = app
End Property
標準モジュールでの呼び出し例
Sub RunIntegrityCheck()
Dim checker As New IntegrityChecker
Dim errInfo As IntegrityChecker.IntegrityError
Set checker.App = Application
Debug.Print "--- Integrity Check Started ---"
If checker.CheckProjectIntegrity() Then
MsgBox "プロジェクトの整合性は保たれています。", vbInformation
Else
Dim msg As String
msg = "【整合性エラー検出】" & vbCrLf
For Each errInfo In checker.Errors
msg = msg & sprintf("[%s] Task: %s (%d) - %s", _
errInfo.ErrorType, errInfo.TaskName, errInfo.TaskID, errInfo.Message) & vbCrLf
Next
MsgBox msg, vbExclamation
End If
Debug.Print "--- Integrity Check Finished ---"
End Sub
' 文字列整形用のヘルパー(簡易版)
Private Function sprintf(fmt As String, ParamArray args() As Variant) As String
' 本来はもっと堅牢な実装が必要だが、デモ用に簡易化
Dim i As Integer
Dim res As String
res = fmt
For i = 0 To UBound(args)
res = Replace(res, "[" & Chr(97 + i) & "]", CStr(args(i)))
Next i
' 実際には [s], [%s] 等のプレースホルダーを置換するロジック
' 今回は簡易的にReplaceを使用
sprintf = res
End Function
*(注: `sprintf` の実装は概念的なものです。実際の実装では、`Replace` を用いて `[s]` 等を置換するように記述してください。)*
4. プロダクション環境における設計上の注意点
このエンジンを実務ツールに組み込む際、以下の3点に留意せよ。
A. 外部データ(DB/Excel)との連携
外部からスケジュールデータをインポートする場合、「Projectオブジェクトに書き込む前」に、メモリ上のデータ構造(DictionaryやCollection)に対して循環参照チェックを行うべきだ。
Projectのオブジェクトに不完全なデータを流し込むと、その瞬間にApplicationがハングしたり、強制終了したりするリスクがある。データの「バリデーション層」と「反映層」を明確に分離せよ。
B. スケールへの対応
タスク数が数万件に及ぶ場合、再帰呼び出し(Recursive Call)によるスタックオーバーフローのリスクを考慮せよ。その場合は、再帰ではなく、明示的な`Stack`オブジェクトを用いた反復的(Iterative)なDFSに書き換える必要がある。
C. エラーハンドリングの哲学
「エラーを見つけること」が目的ではない。「エラーをどうユーザーに伝え、どう修正させるか」が設計のゴールだ。本コードのように、エラー内容を構造体(`IntegrityError`)として保持し、呼び出し元に返す設計にすることで、GUI(Excelのリストや専用ダイアログ)への柔軟な表示が可能になる。
結論
Project VBAにおける真の自動化とは、単に作業を速めることではない。「間違ったデータがシステムに混入するのを防ぎ、論理的な正しさを担保すること」である。
今回紹介した、グラフ理論に基づいた再帰的チェックエンジンは、そのための強固な礎となる。このレベルの設計思想を持てるかどうかが、単なる「スクリプター」と「アーキテクト」を分かつ境界線である。
