Beyond Hardcoding: MS Project VBAにおける「数式エンジン」の外部化設計
開発者が陥る最も浅はかな罠の一つは、ビジネスロジック、とりわけカスタムフィールドの計算式(Formula)をVBAのコード内に直接記述(ハードコーディング)することだ。
MS Projectのカスタムフィールドは、プロジェクトの魂とも言えるデータ構造を規定する。しかし、その数式は極めて脆弱だ。一つカンマを間違えただけで、プロジェクト全体の計算結果は狂い、ユーザーの信頼は失墜する。さらに、開発環境(Dev)と本番環境(Prod)で計算ロジックの微調整が必要になった際、コードを書き換えて再配布するというプロセスは、エンジニアリングの観点から言えば「敗北」に等しい。
本稿では、数式を外部テキストファイル(またはJSON/XML等)として分離し、VBAを単なる「適用エンジン」へと昇華させる、高度に疎結合なアーキテクチャを解説する。これは、単なるテクニックではない。システムの保守性とスケーラビリティを担保するための、設計思想の転換である。
1. アーキテクチャの全体像:疎結合による環境分離
我々が目指すのは、「ロジック(数式)」と「実行基盤(VBA)」の完全なる分離である。
構成要素
1. Configuration File (External): 数式定義を格納する外部ファイル。`FieldID|Type|Formula` のような構造を持つ。
2. Formula Engine (VBA Class): ファイルをパースし、MS Projectのオブジェクトモデルへ注入する責務を持つ。
3. MS Project Application: 注入された数式に基づき、計算を実行する実行環境。
この設計により、数式の変更はテキストファイルの置換だけで完了する。VBAのバイナリ(.mpp内のマクロやアドイン)を触る必要は一切ない。
2. 実装:高効率な数式注入エンジンの構築
ここでは、堅牢なクラスモジュールを用いた実装例を示す。単に読み込むだけでなく、オブジェクトのライフサイクルを制御し、メモリリークを防ぐ設計を施している。
外部定義ファイルのフォーマット (`formulas.cfg`)
セパレータには、数式内で頻出するカンマやセミコロンを避けるため、パイプライン(`|`)を採用するのが定石である。
# ID|Type|Formula
TF01|Task|[Cost]*0.1
TF02|Task|IIf([Finish]<[Start], "Error", "OK")
RF01|Resource|[Work]/[Duration]
クラスモジュール: `clsFormulaEngine`
このクラスは、ファイル I/O から MS Project オブジェクトへの反映までを一手に引き受ける。
Option Explicit
' 定数定義: フィールドタイプを明示的に管理
Private Enum FieldType
ftTask = 1
ftResource = 2
End Enum
' 内部変数
Private m_targetApp As MSProject.Application
''' <summary>
''' コンストラクタ: MS Projectアプリケーションの参照を受け取る
''' </summary>
Public Sub Initialize(ByRef app As MSProject.Application)
Set m_targetApp = app
End Sub
''' <summary>
''' 外部ファイルから数式を読み込み、カスタムフィールドに適用する
''' </summary>
''' <param name="configPath">設定ファイルのフルパス</param>
Public Sub ApplyExternalFormulas(ByVal configPath As String)
Dim fso As Object
Dim ts As Object
Dim line As String
Dim parts() As String
Dim fieldID As String
Dim fType As FieldType
Dim formula As String
' FileSystemObjectの生成 (Late Bindingによる互換性確保)
Set fso = CreateObject("Scripting.FileSystemObject")
' ファイル存在確認: 堅牢なエラーハンドリングの基本
If Not fso.FileExists(configPath) Then
Err.Raise vbErr_FileNotFound, "clsFormulaEngine", "Configuration file not found: " & configPath
Exit Sub
End If
On Error GoTo ErrorHandler
' ファイルオープン
Set ts = fso.OpenTextFile(configPath, 1) ' ForReading
Do Until ts.AtEndOfStream
line = Trim(ts.ReadLine)
' コメント行または空行をスキップ
If line <> "" And Left(line, 1) <> "#" Then
parts = Split(line, "|")
If UBound(parts) = 2 Then
fieldID = parts(0)
' 型の判定
Select Case parts(1)
Case "Task": fType = ftTask
Case "Resource": fType = ftResource
Case Else: GoTo NextLine
End Select
formula = parts(2)
' MS Projectのカスタムフィールドへの適用
Call InjectFormula(fieldID, fType, formula)
End If
End If
NextLine:
Loop
' リソースの明示的解放
Set ts = Nothing
Set fso = Nothing
' プロジェクト全体の再計算をトリガー
m_targetApp.Calculate
Debug.Print "Formula injection completed successfully."
Exit Sub
ErrorHandler:
' エラーログの出力(本来はログファイルへ書き出す)
Debug.Print "Critical Error in ApplyExternalFormulas: " & Err.Description
If Not ts Is Nothing Then ts.Close
Set ts = Nothing
Set fso = Nothing
Err.Raise Err.Number, "clsFormulaEngine", Err.Description
End Sub
''' <summary>
''' 個別のフィールドに対して数式を注入するプライベートメソッド
''' </summary>
Private Sub InjectFormula(ByVal id As String, ByVal fType As FieldType, ByVal formula As String)
Dim cf As MSProject.CustomField
' MS ProjectのCustomFieldsコレクションを走査
' ※Task型かResource型かを厳密に判別する必要がある
For Each cf In m_targetApp.CustomFields
' IDの一致と、Type(Task/Resource)の一致を確認
' MS ProjectのCustomField.Typeプロパティは列挙型で管理されている
' (注: 実際のプロパティ仕様に基づき、Task/Resourceの判定ロジックを最適化)
' ここでは簡略化のため、IDによるマッチングを優先
If cf.Name = id Then
' 数式の適用
cf.Formula = formula
Exit Sub
End If
Next cf
' 見つからない場合は、新規作成を検討するか、警告を出す
Debug.Print "Warning: Custom Field ID not found: " & id
End Sub
3. アーキテクトの視点:パフォーマンスとメモリ管理の極限
この実装において、シニアエンジニアが留意すべきは以下の3点だ。
① オブジェクトのライフサイクル管理
VBAにおいて、`FileSystemObject` や `TextStream` などの外部COMオブジェクトは、スコープを抜けたとしてもメモリ上に残留するリスクがある。`Set ts = Nothing` のように、明示的な解放(Explicit Release)を徹底すること。特に、ループ内で大量のオブジェクトを生成する場合、この一歩がシステム全体の安定性を左右する。
② 計算負荷の制御
`m_targetApp.Calculate` は、カスタムフィールドの数式が変更された後に呼び出す必要があるが、これは非常に重い処理だ。数式を一つ適用するたびに `Calculate` を呼ぶのは愚策である。必ず「全数式の注入が完了した後、一度だけ」実行すること。これが、大規模プロジェクトにおけるレスポンス性能の分水嶺となる。
③ エラーの局所化
カスタムフィールドの数式は、VBAのコンパイルエラーではなく、「実行時の計算エラー」として現れる。`InjectFormula` 内で `On Error Resume Next` を多用してエラーを握り潰すことは、致命的なバグを温存することと同義だ。エラーが発生した場合は、どのIDのどの数式が原因かを特定できるログ構造を設計せよ。
4. 結論:保守性を支配する者が、システムを支配する
数式をコードから切り離すという行為は、一見するとオーバーエンジニアリングに見えるかもしれない。しかし、エンタープライズレベルの運用、特に「数式が頻繁に変わる」「複数の環境を管理する」という条件下では、この設計こそが唯一の生存戦略となる。
コードは「動かし方」を定義し、外部ファイルは「何を計算するか」を定義する。この役割分担(Separation of Concerns)を理解し、実装できるかどうかが、単なるコーダーと、伝説的なアーキテクトを分かつ境界線である。
Keep your logic external, and your code immutable.
(ロジックは外部に、コードは不変にせよ)
