【上級者向け】レガシーなProject VBAコードのモジュール化と保守性向上リファクタリング
こんにちは。エンタープライズ領域のプロジェクト管理基盤や大規模な自動化ソリューションを手がけるチーフアーキテクトだ。
MS ProjectのVBA開発現場を見渡すと、未だに「標準モジュールに数千行のスパゲッティコードが蔓延している」「`ActiveProject` や `ActiveSelection` があちこちに散らばり、少し仕様が変わっただけで全機能が崩壊する」といった惨状をよく目にする。
特にProject VBAは、ExcelやWordに比べてオブジェクトモデルが独特であり、タスク(Task)、リソース(Resource)、アサインメント(Assignment)、そしてそれらを内包するプロジェクト(Project)のライフサイクルを正確に理解していないと、メモリリークや意図しないデータの書き換えを引き起こす。
今回は、場当たり的に書かれたレガシーなProjectマクロを解体し、「クラスモジュール」を用いた堅牢なオブジェクト指向設計へ生まれ変わらせるためのリファクタリング手法を、実戦的なプロダクションコードと共に伝授しよう。
—
1. なぜレガシーなProject VBAは破綻するのか?
多くの現場で見られるアンチパターンは以下の通りだ。
1. グローバルスコープと `ActiveProject` への依存
現在のコンテキスト(どこのプロジェクトを開いているか)を暗黙的に信用しきっているため、複数プロジェクトを同時に扱うマルチプロジェクト環境や、外部からバッチ処理で呼び出した際に完全に破綻する。
2. エラーハンドリングの欠如とトランザクション概念の不在
タスクの大量追加やベースラインの設定途中でエラーが起きた際、中途半端なデータが残ったまま放置され、ファイルが破損する。
3. ビジネスロジックとUI・データアクセスの混在
「計算処理」「タスクの生成」「ユーザーへのメッセージ表示」が1つのプロシージャに同居しているため、テストが不可能。
これを打破するためには、「プロジェクト操作の抽象化(データアクセス層)」と「業務ロジックの分離」をクラスモジュールによって実現する必要がある。
—
2. 設計方針:クラスモジュールによるカプセル化
今回は、レガシーなコードをリファクタリングするためのデザインとして、以下の2つのクラスを設計する。
- `clsProjectContext`(ラッパークラス)
対象となる `MSProject.Project` オブジェクトを安全に保持し、タスクの検索や新規作成、ベースライン設定といった生プリミティブな操作をカプセル化する。
- `clsTaskBatchProcessor`(ビジネスロジッククラス)
特定の業務ルール(例:特定条件のタスクに対して一括でカスタムフィールドを設定し、ベースラインを引く)を遂行する。
—
3. プロダクションコード実装例
以下のコードは、そのまま開発現場のVBAプロジェクトに投入できるプロダクション品質のコードだ。
※VBAのVBE環境で、標準モジュール1つと、クラスモジュールを2つ用意して配置してほしい。
① クラスモジュール:`clsProjectContext`
プロジェクトファイルへの安全なアクセスと、トランザクション的な一括処理を担保するラッパー。
‘ =================================ो=============================
‘ クラス名: clsProjectContext
‘ 概要: MS Projectのインスタンスを安全に制御し、タスク操作をカプセル化する
‘ ==============================================================================
Option Explicit
Private m_Proj As MSProject.Project
‘ コンストラクタ代わり:対象プロジェクトをバインド
Public Sub Initialize(ByVal targetProject As MSProject.Project)
If targetProject Is Nothing Then
Err.Raise 9999, “clsProjectContext”, “有効なプロジェクトが指定されていません。”
End If
Set m_Proj = targetProject
End Sub
‘ プロジェクト名を取得
Public Property Get ProjectName() As String
ProjectName = m_Proj.Name
End Property
‘ タスクを名前で検索し、最初に見つかったものを返す(なければNothing)
Public Function FindTaskByName(ByVal taskName As String) As MSProject.Task
Dim t As MSProject.Task
For Each t In m_Proj.Tasks
If Not t Is Nothing Then
If t.Name = taskName Then
Set FindTaskByName = t
Exit Function
End If
End If
Next t
Set FindTaskByName = Nothing
End Function
‘ 安全なタスクの追加
Public Function AddTask(ByVal taskName As String, ByVal duration As String) As MSProject.Task
Dim t As MSProject.Task
On Error GoTo ErrorHandler
Set t = m_Proj.Tasks.Add(taskName)
t.Duration = duration
Set AddTask = t
Exit Function
ErrorHandler:
Err.Raise Err.Number, “clsProjectContext:AddTask”, “タスクの追加に失敗しました (” & taskName & “): ” & Err.Description
End Function
‘ ベースラインの設定(全タスク対象)
Public Sub SetBaselineAll(ByVal baselineIndex As PjBaselineIndex)
On Error GoTo ErrorHandler
‘ Projectのネイティブメソッドを安全にラップ
m_Proj.SetBaseline Baseline:=baselineIndex
Exit Sub
ErrorHandler:
Err.Raise Err.Number, “clsProjectContext:SetBaselineAll”, “ベースラインの設定に失敗しました: ” & Err.Description
End Sub
② クラスモジュール:`clsTaskBatchProcessor`
ビジネスロジックを担うクラス。特定の要件(例:特定のプレフィックスを持つタスク群の処理とベースライン保存)を実行する。
‘ ==============================================================================
‘ クラス名: clsTaskBatchProcessor
‘ 概要: タスク群に対する一括処理ビジネスロジックを実装
‘ ==============================================================================
Option Explicit
Private m_Context As clsProjectContext
‘ 依存性の注入(DI)風の初期化
Public Sub Initialize(ByVal projContext As clsProjectContext)
If projContext Is Nothing Then
Err.Raise 9999, “clsTaskBatchProcessor”, “ProjectContextが初期化されていません。”
End If
Set m_Context = projContext
End Sub
‘ 業務ロジックの実行:指定マイルストーンの確認とベースライン実行
Public Sub ExecuteStandardization(ByVal milestoneName As String)
Dim targetTask As MSProject.Task
‘ コンテキスト経由で安全にタスク取得
Set targetTask = m_Context.FindTaskByName(milestoneName)
If targetTask Is Nothing Then
‘ 新規作成するフォールバック処理
Set targetTask = m_Context.AddTask(milestoneName, “0d”)
targetTask.Milestone = True
Else
‘ 既存の場合はフラグを確実に立てる
targetTask.Milestone = True
End If
‘ トランザクションの最後にベースラインを保存 (Baseline 1を使用)
m_Context.SetBaselineAll pjBaseline1
MsgBox “処理が正常終了しました。プロジェクト名: ” & m_Context.ProjectName, vbInformation, “成功”
End Sub
③ 標準モジュール:`MainController`
外部から呼び出されるエントリーポイント。エラーハンドリングとオブジェクトのライフサイクル管理(生成・破棄)に徹する。
‘ ==============================================================================
‘ 標準モジュール: MainController
‘ 概要: アプリケーションのエントリーポイント
‘ ==============================================================================
Option Explicit
Sub RunBatchProcess()
‘ 厳格なエラーハンドリング
On Error GoTo ErrorHandler
‘ パフォーマンス最適化:画面描画と自動再計算を停止
App.ScreenUpdating = False
‘ アクティブプロジェクトの存在確認
If ActiveProject Is Nothing Then
MsgBox “処理対象のプロジェクトが開かれていません。”, vbCritical, “エラー”
Exit Sub
End If
‘ 1. コンテキストの生成と初期化
Dim context As clsProjectContext
Set context = New clsProjectContext
context.Initialize ActiveProject
‘ 2. ビジネスロジックプロセッサの生成と実行
Dim processor As clsTaskBatchProcessor
Set processor = New clsTaskBatchProcessor
processor.Initialize context
‘ 業務処理のキック
processor.ExecuteStandardization “プロジェクト完了判定マイルストーン”
CleanUp:
‘ パフォーマンス設定の復元
App.ScreenUpdating = True
‘ オブジェクトの解放
Set processor = Nothing
Set context = Nothing
Exit Sub
ErrorHandler:
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical, “システムエラー”
Resume CleanUp
End Sub
—
4. チーフアーキテクトからの実務アドバイス
1. `App.ScreenUpdating = False` の重要性
Project VBAにおいて、タスクを数千件ループ処理で操作する場合、画面描画やスケジュール再計算(Calculation)が都度走ると、実行速度が致命的に低下する。大規模バッチを組む際は、必ず画面更新を切り、最後に一括で戻すこと。
2. マルチプロジェクト・ファイル連携時の注意点
もし複数の `.mpp` ファイルをまたいでデータを集計・加工するツールを作る場合、`ActiveProject` に依存したコードを書くと、ユーザーがウィンドウを切り替えた瞬間にバグる。
今回紹介したように、メソッドの引数やクラスのプロパティとして `MSProject.Project` オブジェクトを明示的に渡す設計(Dependency Injectionの思想)を徹底してほしい。これにより、どのファイルがアクティブであっても、意図したプロジェクトオブジェクトを確実に操作できる。
3. データベース(SQL Server / Excel / Access)連携への拡張
将来的に入出力データを外部DBやExcelへ逃がす場合も、今回の設計であれば `clsProjectContext` の内部、あるいはデータアクセスのための専用クラス(例: `clsTaskRepository`)を差し替えるだけでよく、ビジネスロジック層(`clsTaskBatchProcessor`)を一切書き換える必要がなくなる。これがモジュール化最大のメリットだ。
—
5. まとめ
レガシーなVBAコードのモジュール化は、単なる「コードのきれい好き」ではない。将来の仕様変更コストを最小化し、業務システムの耐障害性を担保するための極めてロジカルなエンジニアリングだ。
「動けばいい」のスパゲッティコードから脱却し、メンテナンス性の高い堅牢なProject VBAアーキテクチャを、ぜひ今日の開発現場から実践してほしい。
