【極限の設計】Project VBAにおけるカスタムフィールド管理の脱・ハードコード戦略
業務自動化の現場において、多くのエンジニアが陥る「致命的な設計ミス」がある。それは、MS Projectのカスタムフィールド(数式)をVBAのソースコード内に直接書き込む(ハードコーディングする)ことだ。
`Task.CustomFields(1).Formula = “[Cost]*0.1″`
一見、これ以上なくシンプルで効率的に見える。しかし、プロジェクトが大規模化し、計算ロジックが頻繁に変更される運用フェーズに入った瞬間、このコードは「技術的負債の塊」へと変貌する。数式を一つ修正するたびに、VBAプロジェクトを書き換え、コンパイルし、配布し直す……。これでは、真の自動化エンジニアとは呼べない。
本稿では、カスタムフィールドの数式を外部テキストファイルで管理し、環境に応じて動的に切り替える「プロフェッショナルな設計手法」を伝授する。これは単なるテクニックではない。「コード(ロジックの実行)」と「データ(ビジネスルール)」を分離するという、ソフトウェア工学における鉄則の実践である。
1. なぜハードコーディングを捨てなければならないのか
設計者として、以下の3つのリスクを直視せよ。
1. 変更コストの増大: 計算式(ビジネスロジック)の変更は、業務要件の変化に伴い頻繁に発生する。コードを触るたびに回帰テストが必要になり、デプロイの心理的・物理的ハードルが上がる。
2. 環境の混在: 開発環境では「テスト用の計算式」を使い、本番環境では「確定した計算式」を使う必要がある。コード内にこれらが混在すると、条件分岐(`If DevMode Then…`)が肥大化し、バグの温床となる。
3. 監査とトレーサビリティの欠如: 数式がVBAのバイナリ(.mppや.xlsm)の中に隠蔽されていると、非エンジニア(PMや監査担当者)が「今、どのような計算ロジックが適用されているか」を確認することが困難になる。
2. アーキテクチャ設計:Formula Engineの概念
目指すべきは、VBAを「数式を読み込み、Projectに注入するだけのエンジン(実行器)」にすることだ。
ファイル構造の定義
設定ファイルは、シンプルかつGit等のバージョン管理システムと相性の良い、キー・バリュー形式のテキストファイルを採用する。
`formulas.conf` (設定ファイルの例)
# Task Custom Fields
Format: FieldIndex|Formula
T_TF1|[Cost]*0.1
T_TF2|IIf([Duration]>60;"Over Limit";"Normal")
Resource Custom Fields
R_TF1|[Unit]*[Rate]
設計のポイント
- 識別子の設計: `T_TF1`(Taskの1番目のカスタムフィールド)のように、対象オブジェクトとインデックスを明示したキーを用いる。
- 環境切り替え: 実行時に読み込むファイルパスを、環境変数や実行引数から決定する。
3. 実装:堅牢なFormula Manager
以下に、プロダクション環境で使用することを想定した、エラーハンドリングと柔軟性を兼ね備えたコード例を示す。ここでは、外部ライブラリへの依存を最小限にするため、Late Binding(遅延結合)を活用し、配布時の参照設定エラーを防いでいる。
プロダクション・コード例
Option Explicit
' ==============================================================================
' Module: modFormulaEngine
' Description: カスタムフィールドの数式を外部ファイルから読み込み、Projectに適用する
' ==============================================================================
Public Sub ApplyCustomFieldFormulas(ByVal configFilePath As String)
Dim fso As Object
Dim ts As Object
Dim line As String
Dim parts() As String
Dim targetType As String
Dim fieldIdx As Integer
Dim formula As String
Set fso = CreateObject("Scripting.FileSystemObject")
' 1. ファイルの存在チェック(堅牢性の基本)
If Not fso.FileExists(configFilePath) Then
Err.Raise vbObjectError + 513, "modFormulaEngine", "設定ファイルが見つかりません: " & configFilePath
End If
On Error GoTo ErrorHandler
Set ts = fso.OpenTextFile(configFilePath, 1) ' 1 = ForReading
Do Until ts.AtEndOfStream
line = Trim(ts.ReadLine)
' コメント行または空行をスキップ
If line <> "" And Left(line, 1) <> "#" Then
' 区切り文字 "|" で分割
parts = Split(line, "|")
If UBound(parts) = 1 Then
targetType = parts(0) ' T_TF1 or R_TF1
formula = parts(1)
' キーの解析
' T_ = Task, R_ = Resource
If Left(targetType, 2) = "T_" Then
fieldIdx = ExtractIndex(Mid(targetType, 3))
ApplyTaskFormula fieldIdx, formula
ElseIf Left(targetType, 2) = "R_" Then
fieldIdx = ExtractIndex(Mid(targetType, 3))
ApplyResourceFormula fieldIdx, formula
End If
End If
End If
Loop
ts.Close
MsgBox "カスタムフィールドの更新が完了しました。", vbInformation
Exit Sub
ErrorHandler:
If Not ts Is Nothing Then ts.Close
MsgBox "エラーが発生しました。" & vbCrLf & _
"Error No: " & Err.Number & vbCrLf & _
"Description: " & Err.Description, vbCritical, "致命的エラー"
End Sub
' --- 内部補助関数 ---
' 文字列からインデックス番号を抽出
Private Function ExtractIndex(ByVal strIdx As String) As Integer
ExtractIndex = CInt(strIdx)
End Function
' Taskフィールドへの適用
Private Sub ApplyTaskFormula(ByVal idx As Integer, ByVal formula As String)
' MS ProjectのCustomFieldコレクションは、グローバルな定義を書き換える
' TaskオブジェクトのCustomFieldsプロパティ経由でアクセス
On Error Resume Next
ActiveProject.Tasks.CustomFields(idx).Formula = formula
If Err.Number <> 0 Then
Debug.Print "Task Field Error [Idx:" & idx & "]: " & Err.Description
Err.Clear
End If
On Error GoTo 0
End Sub
' Resourceフィールドへの適用
Private Sub ApplyResourceFormula(ByVal idx As Integer, ByVal formula As String)
On Error Resume Next
ActiveProject.Resources.CustomFields(idx).Formula = formula
If Err.Number <> 0 Then
Debug.Print "Resource Field Error [Idx:" & idx & "]: " & Err.Description
Err.Clear
End If
On Error GoTo 0
End Sub
4. 実務における「極限の注意点」
この設計を導入する際、単にコードをコピーするだけでは不十分だ。以下の3点を設計に組み込め。
① 「数式の構文エラー」という時限爆弾
VBAから `Formula = “…”` を実行した際、もし数式に構文エラー(例:括弧の閉じ忘れ、存在しないフィールド名)が含まれていれば、Projectはランタイムエラーを吐く。
上記のコードでは `On Error Resume Next` を局所的に使用しているが、これは「どの行でエラーが起きたか」をログに残す設計とセットでなければ意味をなさない。実務では、エラーが発生した際に `Debug.Print` だけでなく、エラーログファイルへ書き出す仕組みを検討せよ。
② 環境切り替えのロジック
本番環境と開発環境を切り替える際、以下のような実装を推奨する。
Dim configPath As String
' 環境変数 "PROJECT_ENV" が "PROD" なら本番用、そうでなければ開発用
If Environ("PROJECT_ENV") = "PROD" Then
configPath = "C:\App\Config\formulas.prod.conf"
Else
configPath = ThisWorkbook.Path & "\config\formulas.dev.conf"
End If
Call ApplyCustomFieldFormulas(configPath)
これにより、コードを一切書き換えることなく、デプロイメント・パイプライン(CI/CD)に適合させることが可能になる。
③ データの整合性と「Dirty State」
数式を書き換えた際、既存のタスクに残っている「計算結果の値」が古いまま残る場合がある。数式を更新した直後に、必要に応じて `Task.Calculate` や、全タスクをループして再計算を促す処理を挟むのが、プロフェッショナルな振る舞いだ。
結論
優れたエンジニアは、コードを書くことよりも、「変化に強い構造」を作ることに情熱を注ぐ。
カスタムフィールドの数式を外部化することは、単なる手間の削減ではない。それは、ソフトウェアのライフサイクル全体を見据えた、極めて論理的な意思決定である。この設計思想を武器に、メンテナンス地獄から解放された、真に堅牢な自動化ツールを構築せよ。
