【実務・中級編】プロ向け:制約条件の「強制解除」によるスケジュール最適化とボトルネック解消 – Project VBA解析バイブル

スポンサーリンク
スポンサーリンク

制約の鎖を解き放て: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を用いて、一時的な「自由な状態」を作り出し、エンジンの真の力を引き出す。このアプローチができるようになれば、あなたは単なる「自動化担当者」から、プロジェクトの構造をコントロールする「アーキテクト」へと昇華するだろう。

タイトルとURLをコピーしました