整合性チェックエンジンの設計思想:MS Projectにおける依存関係と制約条件の論理的矛盾を排除する
プロジェクト・マネジメントにおける「スケジュール」とは、単なる日付の羅列ではない。それは、リソース、制約、そして論理的な依存関係が織りなす、高度に決定論的な計算モデルである。
多くのPM(プロジェクトマネージャー)は、Microsoft Projectの「制約条件(Constraint)」を安易に使用する。`Must Start On` や `Start No Earlier Than` といった制約は、一見するとスケジュールを制御する強力な武器に見えるが、その実態は、プロジェクトの「論理的整合性」を破壊する毒となり得る。先行タスクの終了日と、タスク自身に課された制約日が衝突したとき、Projectの計算エンジンは警告を出すが、大規模なプロジェクトにおいては、その矛盾がクリティカルパスを歪め、プロジェクト全体を崩壊させる。
本稿では、単なる「ツール」の作り方を解説するのではない。MS ProjectのCOMオブジェクトモデルの背後にある挙動を理解し、依存関係と制約条件の矛盾を、計算機科学的なアプローチで検出し、修正案を提示する「整合性チェック・エンジン」の設計と実装について、その極限の知見を共有する。
1. 矛盾の正体:制約条件 vs 依存関係のデッドロック
MS Projectにおいて、タスクの開始日は以下の二つの力の均衡によって決定される。
1. 依存関係による計算(Predecessor Logic): 先行タスクの終了日(または進捗)に基づいた、論理的な「最短可能日」。
2. 制約条件による拘束(Constraint Logic): ユーザーが手動で設定した「固定的な日付」。
矛盾が発生するのは、「依存関係が要求する最短開始日」が「制約条件で許可された開始日」よりも後になる場合である。このとき、スケジュールは「論理的な整合性」を失い、ユーザーが意図しない形でタスクが押し出されるか、あるいは不自然な「ラグ(遅延)」が発生する。
シニアエンジニアが構築すべきなのは、Projectの計算エンジンに頼り切るのではなく、独自の計算レイヤーを構築し、Projectの内部状態を監査(Audit)するエンジンである。
2. アーキテクチャ設計:メモリ管理とパフォーマンスの最適化
大規模プロジェクト(数千タスク規模)を扱う場合、VBAの標準的なループ処理では致命的なパフォーマンス低下を招く。COMオブジェクトへのアクセスは、メモリ空間を跨ぐオーバーヘッドが極めて大きいからだ。
高速化のための戦略
- オブジェクト・キャッシュの回避: `ActiveProject.Tasks` をループ内で何度も参照するのは愚策である。一度、必要なプロパティ(ID, Start, Finish, ConstraintType, ConstraintDate, Predecessors)をVariant型の配列に一括で吸い出し、メモリ上の配列に対してロジックを展開すること。
- Dictionaryオブジェクトの活用: タスク間の依存関係を高速に解決するため、`Scripting.Dictionary` を用いてタスクIDとオブジェクト(あるいは配列のインデックス)を O(1) で引けるように設計する。
- 明示的なメモリ解放: クラスモジュールを多用する場合、参照カウントの管理を怠ると、ExcelやProjectのプロセスがメモリに残存する。`Set obj = Nothing` は基本だが、循環参照を防ぐ設計が不可欠である。
3. 実装:整合性チェック・エンジン
以下に、矛盾を検出し、修正案を提示するエンジンのコアロジックを示す。ここでは、保守性と拡張性を考慮し、ロジックをクラスモジュールに分離する設計思想に基づいている。
Module: `modIntegrityEngine`
Option Explicit
' -------------------------------------------------------------------------
' 整合性チェックエンジン・メインモジュール
' -------------------------------------------------------------------------
Public Sub RunIntegrityCheck()
Dim engine As New clsIntegrityEngine
Dim conflicts As Collection
Dim conflict As clsConflict
On Error GoTo ErrorHandler
' エンジンの初期化と実行
Set engine = New clsIntegrityEngine
Set conflicts = engine.Analyze(ActiveProject)
' 結果の出力
If conflicts.Count = 0 Then
MsgBox "整合性チェック完了:矛盾は見つかりませんでした。", vbInformation
Else
' 矛盾がある場合、詳細をレポート(ここではイミディエイトウィンドウと簡易表示)
Debug.Print "--- Integrity Check Report: " & conflicts.Count & " conflicts found ---"
For Each conflict In conflicts
Debug.Print "Task ID: " & conflict.TaskID & " | Name: " & conflict.TaskName
Debug.Print " [Conflict] Constraint Date: " & conflict.ConstraintDate & _
" vs Calculated Earliest Start: " & conflict.CalculatedStart
Debug.Print " [Suggestion] " & conflict.Suggestion
Debug.Print "-------------------------------------------------------"
Next conflict
MsgBox conflicts.Count & " 件の矛盾が検出されました。詳細はイミディエイトウィンドウを確認してください。", vbExclamation
End If
CleanUp:
Set engine = Nothing
Set conflicts = Nothing
Exit Sub
ErrorHandler:
MsgBox "致命的なエラーが発生しました: " & Err.Description, vbCritical
Resume CleanUp
End Sub
Class Module: `clsIntegrityEngine`
Option Explicit
' -------------------------------------------------------------------------
' clsIntegrityEngine: 整合性分析のコアロジック
' -------------------------------------------------------------------------
Private Type TaskData
ID As Long
Name As String
ConstraintType As Long
ConstraintDate As Date
CalculatedStart As Date
HasConstraint As Boolean
End Type
' 矛盾情報を格納するためのコレクション用クラス(別途 clsConflict を作成)
' ここでは簡略化のため、内部構造として扱う
Public Function Analyze(ByVal proj As MSProject.Project) As Collection
Dim conflicts As New Collection
Dim t As MSProject.Task
Dim taskMap As Object ' Scripting.Dictionary
Set taskMap = CreateObject("Scripting.Dictionary")
' 1. 高速化のため、タスクデータをメモリ(Dictionary)にキャッシュ
' COMオブジェクトへのアクセス回数を最小限に抑える
For Each t In proj.Tasks
If Not t Is Nothing Then
Dim data As TaskData
data.ID = t.ID
data.Name = t.Name
data.ConstraintType = t.ConstraintType
data.ConstraintDate = t.ConstraintDate
' 制約があるかどうかを判定 (pjTaskConstraintAsSoonAsPossible は 0)
data.HasConstraint = (t.ConstraintType <> 0)
' 計算上の開始日(依存関係を考慮した後の値)を取得
' ※Projectの再計算が終わっていることが前提
data.CalculatedStart = t.Start
taskMap.Add t.ID, data
End If
Next t
' 2. 矛盾の検知ロジック
' 各タスクについて、制約条件と実態の乖離を検証
Dim key As Variant
For Each key In taskMap.Keys
Dim currentTask As TaskData
currentTask = taskMap(key)
If currentTask.HasConstraint Then
' ケースA: Must Start On / Must Finish On 等の強力な制約
' 先行タスクとの依存関係により、制約日より後に開始せざるを得ない場合
' ここでは、Projectが計算したStart dateが、ConstraintDateより
' 後ろにズレている(制約が無視されている、あるいは論理的に不可能な)場合を検知
' 注意:Projectの挙動として、制約を破ることはできないため、
' 「制約によって発生している不自然なラグ」を検出するのがプロの仕事である。
' 簡易的なロジック:制約日と実際の開始日の差分を評価
' 本来、Must Start On なら Start == ConstraintDate であるべき
If currentTask.ConstraintType = 1 Then ' pjTaskConstraintMustStartOn
If Abs(currentTask.ConstraintDate - currentTask.CalculatedStart) > 0.1 Then
' 矛盾発見:制約日と計算上の開始日が一致していない
Dim c As New clsConflict
c.TaskID = currentTask.ID
c.TaskName = currentTask.Name
c.ConstraintDate = currentTask.ConstraintDate
c.CalculatedStart = currentTask.CalculatedStart
c.Suggestion = "制約条件を 'As Soon As Possible' に変更するか、先行タスクの終了日を調整してください。"
conflicts.Add c
End If
End If
End If
Next key
Set Analyze = conflicts
End Function
*(注: `clsConflict` クラスは、プロパティを持つ単純なデータ保持用クラスとして定義されているものとする)*
4. 極限の知見:Windows APIによるシステムレベルの監視
さらに高度なシステム、例えば「プロジェクト管理サーバー」の一部としてVBAを動作させる場合、メモリリークやゾンビプロセスの発生は許されない。
MS ProjectのCOMインターフェースは、不適切なエラーハンドリングによって、ExcelやProjectのメモリ空間に「未解放の参照」を残しやすい。これを防ぐために、Windows APIの `GetProcessMemoryInfo` を呼び出し、実行中のプロセスのプライベートワーキングセットを監視する機能を組み込むことは、エンタープライズ級の自動化ツールにおける「作法」である。
また、大規模なバッチ処理を行う際は、`Declare Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)` を用いて、CPUの過負荷を制御し、OSのスケジューラに制御を戻す配慮も、レガシー環境を安定させるための知見である。
5. 結言
整合性チェックエンジンとは、単にエラーを見つけるためのものではない。それは、プロジェクトの「論理的な完全性」を保証するための、防波堤である。
シニアエンジニアとして、我々はツールが提示する結果を鵜呑みにしてはならない。ツールが「計算した結果」の裏側にある、制約と依存関係の力学を読み解き、それをコードによって検証する。そのプロセスこそが、不確実なプロジェクト管理という戦場において、確実な航路を示す唯一の手段となるのだ。
