【実務・中級編】初心者向け:特定のタスクタイプに定型制約を適用する「プリセット」機能 – Project VBA解析バイブル

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

Project VBAを掌握する極限の知見:ポリシー駆動型タスク制約エンジンへの昇華

多くの開発者が、Microsoft ProjectのVBA(以下、Project VBA)を「単なる操作の自動化」と考えている。タスク名をループで回し、`If`文で条件分岐させ、`ConstraintType`を書き換える。それだけで満足しているなら、君が作っているのは「ツール」ではなく、単なる「使い捨てのスクリプト」だ。

業務が複雑化し、プロジェクトの規模が数千タスクに膨れ上がったとき、そのスパゲッティコードは必ず悲鳴を上げる。タスク名が変わった瞬間に壊れる、条件が増えるたびにコードが肥大化する、計算負荷が高すぎてMS Projectがフリーズする——。

真のアーキテクトは、「何を(Data)」と「どう動かすか(Logic)」を完全に分離する。

本記事では、タスクの属性(調査、開発、承認など)に基づき、ASAP(可能な限り早く)やALAP(可能な限り遅く)といった制約条件を自動適用する「プリセット機能」を、保守性と堅牢性を極限まで高めた設計思想で解説する。

1. 初心者が陥る「アンチパターン」とその代償

まず、多くの新人が書く「典型的な失敗コード」を提示しておこう。

' 【警告】これはプロダクションコードではない。絶対に真似するな。
Sub BadPresetExample()
    Dim t As Task
    For Each t In ActiveProject.Tasks
        If t.Name = "調査" Then
            t.ConstraintType = pjConstraintAsSoonAsPossible
        ElseIf t.Name = "承認" Then
            t.ConstraintType = pjConstraintAsLateAsPossible
        End If
    Next t
End Sub

なぜこれが「ゴミ」なのか?

1. 脆弱性(Fragility): タスク名(`t.Name`)を判定基準にしている。ユーザーが「調査」を「調査業務」に変更した瞬間、このロジックは無力化する。
2. 拡張性の欠如(Inflexibility): 新しいタスクタイプ(例:「テスト」)が増えるたびに、`ElseIf`を書き足さなければならない。これは「Open-Closed Principle(開放閉鎖の原則)」に完全に違反している。
3. 計算コストの無視: プロジェクト規模が大きい場合、ループ内での制約変更は、その都度スケジューリングエンジンの再計算を誘発し、パフォーマンスを著しく低下させる。

2. プロフェッショナルの設計思想:ポリシー駆動型アプローチ

堅牢なツールを作るには、「タスクの属性(Type)」をタスク名ではなく「カスタムフィールド」に持たせるべきだ。そして、属性と制約の紐付けは、コード内にハードコードせず、「マッピング・データ」として管理する。

アーキテクチャの三層構造

1. Data Layer (Custom Fields): タスクの「役割」を格納する(例:`Text1`フィールドに “Research”, “Approval” と入力)。
2. Policy Layer (Mapping Dictionary): 「”Research” なら `pjConstraintAsSoonAsPossible`」という定義を持つデータ構造。
3. Engine Layer (The Core): データを読み込み、ポリシーに従ってオブジェクトを操作するロジック。

3. プロダクショングレードの実装例

以下に、実務でそのまま利用可能な、エラーハンドリングとパフォーマンス対策を施したコードを示す。ここでは、`Scripting.Dictionary` を用いてポリシーを管理する。

' =================================================================
' Module: TaskPresetEngine
' Description: タスクタイプに基づき、制約条件を自動適用するエンジン
' Author: Chief Architect
' =================================================================

Option Explicit

' 定数定義:マジックナンバーを排除し、可読性を確保する
Private Const TYPE_FIELD_NAME As String = "Text1" ' タスクタイプを格納するカスタムフィールド

Public Sub ApplyTaskTypePresets()
    Dim engine As New TaskConstraintEngine
    
    On Error GoTo ErrorHandler
    
    ' 1. ポリシー(プリセット)の定義
    ' 本来は外部ファイル(CSV/JSON)やDBから読み込むのが理想だが、
    ' ここでは保守性を高めた形でコード内に定義する。
    engine.AddPolicy "調査", pjConstraintAsSoonAsPossible
    engine.AddPolicy "開発", pjConstraintAsSoonAsPossible
    engine.AddPolicy "承認", pjConstraintAsLateAsPossible
    engine.AddPolicy "レビュー", pjConstraintAsLateAsPossible
    
    ' 2. 実行
    engine.Execute
    
    MsgBox "プリセットの適用が完了しました。", vbInformation
    Exit Sub

ErrorHandler:
    MsgBox "致命的なエラーが発生しました: " & Err.Description, vbCritical
End Sub

' =================================================================
' Class: TaskConstraintEngine
' Description: エンジン本体。ロジックとデータ構造をカプセル化する
' =================================================================
' ※実際にはクラスモジュールとして作成すること
' =================================================================

' (注: VBAの制約上、クラスの内容をここに記述する)
' -----------------------------------------------------------------
' Private dict As Object
' 
' Private Sub Class_Initialize()
'     Set dict = CreateObject("Scripting.Dictionary")
' End Sub
' 
' Public Sub AddPolicy(ByVal taskType As String, ByVal constraint As Long)
'     dict(taskType) = constraint
' End Sub
' 
' Public Sub Execute()
'     Dim t As Task
'     Dim currentType As String
'     Dim targetConstraint As Long
'     
'     ' パフォーマンス最適化: 計算停止(大量処理時の定石)
'     ' ※Project VBAではExcelほど劇的ではないが、大規模プロジェクトでは重要
'     ' Application.Calculation = pjCalculationManual 
'
'     For Each t In ActiveProject.Tasks
'         ' ライフサイクル管理: Nothingチェックは絶対
'         If Not t Is Nothing Then
'             ' カスタムフィールドからタイプを取得
'             currentType = t.Text1 
'             
'             ' ポリシーに存在するか確認
'             If dict.Exists(currentType) Then
'                 targetConstraint = dict(currentType)
'                 
'                 ' 変更が必要な場合のみ書き換える(無駄なプロパティ書き込みを避ける)
'                 If t.ConstraintType <> targetConstraint Then
'                     t.ConstraintType = targetConstraint
'                 End If
'             End If
'         End If
'     Next t
'     
'     ' Application.Calculation = pjCalculationAutomatic
' End Sub
' -----------------------------------------------------------------

*(注: 上記のクラス部分は、VBAエディタで「クラスモジュール」を新規作成し、`TaskConstraintEngine` という名前で実装してください)*

4. 伝説のアーキテクトが教える、運用上の「急所」

このコードを実戦投入する際、君が絶対に忘れてはならないことが3つある。

① オブジェクトのライフサイクルと`Nothing`チェック

`For Each t In ActiveProject.Tasks` ループにおいて、`t` が `Nothing` になるケース(削除されたタスクや、特殊なプロジェクト構造)は必ず存在する。これを確認せずにプロパティへアクセスするのは、地雷原を素足で歩くようなものだ。`If Not t Is Nothing Then` は、エンジニアとしての最低限の礼儀である。

② 変更の「差分適用」による最適化

`t.ConstraintType = targetConstraint` を無条件に実行してはいけない。たとえ同じ値であっても、プロパティへの書き込みが発生した瞬間に、MS Projectのスケジューリングエンジンは「何かが変わった」と判断し、再計算のトリガーを引く可能性がある。
`If t.ConstraintType <> targetConstraint Then` という条件文を入れるだけで、大規模プロジェクトでの実行速度は劇的に変わる。

③ データ連携の境界線

もし、この「プリセット定義」をExcelや外部データベースから取得するように設計変更する場合、「データの整合性(Data Integrity)」に細心の注意を払え。

  • データベースに「調査 」(末尾にスペース)が入っていたら?
  • 定義されていないタイプがフィールドに入っていたら?

エンジニアは、入力データが「完璧であること」を期待してはならない。常に「汚いデータ」が来ることを前提に、`Trim()` 関数によるトリミングや、例外処理を組み込むべきだ。

結論

「動くコード」を書くのは誰にでもできる。しかし、「変化に強く、大規模でも耐えうるコード」を書ける者こそが、真の自動化エンジニアだ。

タスク名を判定基準にするという安易な道に逃げるな。カスタムフィールドで構造化し、ポリシー(辞書)で制御せよ。それが、君のツールを「ただのスクリプト」から「エンタープライズ・ソリューション」へと昇華させる唯一の道である。

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