ループを回すな、ロジックを埋め込め:MS Projectにおける「動的コスト計算エンジン」の設計思想
MS ProjectのVBA開発において、多くの開発者が陥る「初心者特有の罠」がある。それは、「全リソースをループで回し、If文で条件分岐させ、値を一つずつセルに書き込む」という手法だ。
確かに、そのコードは動く。しかし、それは「自動化」ではない。単なる「人間が手で行っていた作業の、低速なデジタルコピー」に過ぎない。リソースが1,000人を超えた時、あるいは単価体系が複雑化した時、そのコードはたちまち保守不能なスパゲッティと化し、プロジェクトの進行を阻害する負債となる。
真のアーキテクトが目指すべきは、「データ(属性)」と「ロジック(計算式)」を分離し、MS Project自身の計算エンジンを最大限に活用する設計である。
本記事では、リソースの属性に基づき、カスタムフィールドを用いて動的にコスト計算ロジックを構築する、プロフェッショナル向けの設計手法を伝授する。
1. 凡庸な設計と、堅牢な設計の決定的な違い
まず、君が書こうとしているコードがどちらの設計思想に近いか、自問自答してほしい。
【非効率な設計:命令型アプローチ】
' 典型的な「やってはいけない」コードのイメージ
For Each r In ActiveProject.Resources
If r.Name = "Senior Engineer" Then
r.Cost = r.Work * 10000
ElseIf r.Name = "Junior Engineer" Then
r.Cost = r.Work * 5000
End If
Next r
この設計には、致命的な欠陥が3つある。
1. 計算の遅延: プロジェクトが大規模になると、ループの実行時間が指数関数的に増大する。
2. ロジックの硬直性: 単価が変わるたびにVBAコードを修正・再デプロイする必要がある。
3. 計算の不整合: VBAで書き込んだ「結果」は、その後のWork(工数)の変更を検知して自動更新されない。
【理想的な設計:宣言型アプローチ】
プロフェッショナルは、VBAを使って「値を書き込む」のではなく、「計算式(Formula)をセットアップする」。
リソースの属性(例えば `Number1` に格納されたレート)を定義し、カスタムフィールドに `[Number1] * [Work]` という式を流し込む。これにより、計算の実行主体はVBAからMS Projectの計算エンジンへと移譲される。
2. アーキテクチャの設計図:メタデータ駆動型計算
我々が構築するのは、以下の3層構造を持つシステムだ。
1. Data Layer (Metadata): リソースの属性(単価、スキルレベル、コスト係数など)を格納するカスタムフィールド。
2. Logic Layer (Formula): カスタムフィールドに設定される計算式。
3. Orchestration Layer (VBA): 外部データ(CSV/DB)から属性を流し込み、適切な計算式をフィールドに適用する制御プログラム。
この設計により、単価が変わっても「Data Layer」を更新するだけで済み、計算ロジックが変わっても「Logic Layer」を書き換えるだけで済む。VBAは、その橋渡しをするだけの存在になるのだ。
3. 実装:プロダクション・レディなコード例
以下に、リソースの属性(`Number1` を単価として使用)に基づき、カスタムフィールド(`Cost_Auto`)に動的な計算式を適用する、堅牢な実装例を示す。
Option Explicit
' ==============================================================================
' Module: ResourceCostEngine
' Description: リソース属性に基づき、カスタムフィールドに動的な計算式を設定する
' Author: Chief Architect
' ==============================================================================
' 定数定義:マジックナンバーを排除し、保守性を高める
Private Const FIELD_RATE_ATTR As String = "Number1" ' 単価を格納する属性
Private Const FIELD_CALC_COST As String = "Cost_Auto" ' 計算結果を表示するフィールド名
Private Const TARGET_FIELD_TYPE As Long = 2 ' 2 = Cost (コスト系フィールド)
Public Sub SetupResourceCostLogic()
Dim r As Resource
Dim targetField As CustomField
Dim successCount As Long
On Error GoTo ErrorHandler
' 1. 画面更新を停止し、パフォーマンスを最適化
Application.ScreenUpdating = False
Debug.Print "--- Cost Engine Initialization Started ---"
' 2. カスタムフィールドの存在確認と初期化
' ここでは、特定のカスタムフィールドが「計算式」を受け入れられる状態かを確認する
' 本来は、外部DB等から取得した単価をまずリソースの属性(Number1)にセットする処理が先行する
' 3. リソースループによる属性セット(本来は外部ファイル連携を推奨)
' ここではデモとして、リソース名に基づいて属性をセットする
For Each r In ActiveProject.Resources
If Not r Is Nothing Then
Select Case r.Name
Case "Senior Engineer"
r.Number1 = 12000 ' 単価
Case "Junior Engineer"
r.Number1 = 5000 ' 単価
Case Else
r.Number1 = 0
End Select
End If
Next r
' 4. 【核心】計算式の動的注入
' リソースのWork(工数)とNumber1(単価)を掛け合わせる式を、
' MS Projectの計算エンジンに「宣言」する。
' 注意:フィールドの型が一致していないとエラーになるため、事前に検証が必要。
' CustomFieldオブジェクトへのアクセス(プロジェクト設定に依存)
' ※ここでは、あらかじめ「Cost_Auto」という名前のカスタムフィールドが
' プロジェクト内で定義されていることを前提とする。
Set targetField = GetCustomFieldByName(FIELD_CALC_COST)
If Not targetField Is Nothing Then
' 計算式を注入。これにより、リソースのWorkが変わるたびに自動再計算される。
' 数式内でリソースの属性を参照させるのがプロの技。
targetField.Formula = "[Number1] * [Work]"
Debug.Print "Logic Layer: Formula injected successfully."
Else
Err.Raise vbObjectError + 513, "SetupResourceCostLogic", _
"Required Custom Field '" & FIELD_CALC_COST & "' not found."
End If
' 5. 再計算の実行
Application.CalculateFull
Application.ScreenUpdating = True
MsgBox "Cost Engine Setup Complete.", vbInformation
Exit Sub
ErrorHandler:
Application.ScreenUpdating = True
MsgBox "Critical Error in Cost Engine:" & vbCrLf & Err.Description, vbCritical, "Engine Failure"
' 実務ではここでエラーログをファイルに書き出す処理を入れる
End Sub
' ------------------------------------------------------------------------------
' Function: GetCustomFieldByName
' Description: 名前からカスタムフィールドオブジェクトを取得する(堅牢な検索)
' ------------------------------------------------------------------------------
Private Function GetCustomFieldByName(ByVal fieldName As String) As CustomField
Dim cf As CustomField
For Each cf In ActiveProject.CustomFields
' 名前が一致するか、あるいは特定のタグが付いているかを確認
If cf.Name = fieldName Then
Set GetCustomFieldByName = cf
Exit Function
End If
Next cf
Set GetCustomFieldByName = Nothing
End Function
4. 伝説のアーキテクトが教える「落とし穴」への回避策
このコードを実戦投入する際、君が直面するであろう壁とその回避策を伝えておく。
① カスタムフィールドの「型」の不一致
MS Projectのカスタムフィールドには、`Text`, `Number`, `Date`, `Cost` といった厳格な型がある。`Cost` 型のフィールドに対して、文字列を返す数式を注入しようとすれば、即座にランタイムエラーで死ぬ。
対策: `GetCustomFieldByName` 関数内で、`cf.Type` をチェックし、期待する型と一致しているかバリデーションを行う工程を必ず組み込め。
② 計算の再帰ループ(Recalculation Overhead)
複雑な数式を多くのフィールドに設定すると、一つの値が変わるたびにプロジェクト全体の再計算が走り、動作が極端に重くなる。
対策:
- 計算式は可能な限りシンプルに保て。
- `Application.Calculation = xlCalculationManual`(※MS Projectにおける相当の制御)を検討せよ。
- 頻繁に更新される値と、静的な値(マスターデータ)を明確に分離せよ。
③ 外部データ連携の整合性
実務では、単価はExcelやSQL Serverにあるはずだ。VBAでそれらを読み込む際、「リソースのユニークID(Resource ID)」をキーにせよ。名前(`r.Name`)をキーにすると、同姓同名のリソースが現れた瞬間に、コスト計算が崩壊する。
結言:保守性こそが最大のパフォーマンスである
優れたエンジニアは、コードの実行速度だけを追求しない。「変更に対する強さ」を追求する。
今回紹介した「メタデータ駆動型」の設計は、コード量を減らし、MS Projectのネイティブ機能を最大限に引き出す。これにより、君が作成したツールは、単なる「便利ツール」から、企業のインフラを支える「堅牢なシステム」へと昇華するのだ。
次にコードを書くときは、自分に問いかけてほしい。
「これは、人間がやっていた作業をなぞっているだけか? それとも、新しい計算の仕組みを構築しているのか?」と。
