【実務・中級編】実務中級:リソース平準化と制約条件の競合をVBAで解決する優先順位調整 – Project VBA解析バイブル

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

制約とリソースの衝突を制する: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は単なる「自動化スクリプト」から、プロジェクトを支える「インテリジェントなエンジン」へと昇華するはずだ。

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