【テクニカル・上級編】【上級者向け】レガシーなProject VBAコードのモジュール化と保守性向上リファクタリング – Project VBA解析バイブル

スポンサーリンク

【上級者向け】レガシーなProject VBAコードのモジュール化と保守性向上リファクタリング

レガシーなMS ProjectのVBAコードベースに向き合ったとき、多くのエンジニアが絶望を覚える。
数千行に及ぶ単一の標準モジュール。グローバル変数の乱用。あちこちに散らばる `On Error Resume Next` の隠蔽。そして、何を引き金に落ちるのか予測不可能なスパゲッティ構造。

Project VBAは、ExcelやWordのVBAに比べてドキュメントが少なく、オブジェクトモデルの挙動(特にタスク、リソース、アサインメントの三位一体のライフサイクル)が独特であるため、一度負債を抱えると手がつけられなくなる。

本稿では、場当たり的な修正の繰り返しで崩壊寸前となったレガシーコードを、クラスモジュールを活用したモダンなオブジェクト指向設計へと昇華させ、極限まで保守性とパフォーマンスを高めるリファクタリング手法を解説する。

1. レガシーコードが陥る「3大アンチパターン」

MS ProjectのVBA開発において、現場のエンジニアが陥りがちな悪習をまず定義する。

1. グローバルスコープの汚染
`ActiveProject` や `ActiveSelection` に無防ぎに依存し、プロシージャ間で暗黙の状態を共有する。これにより、テスト実行すらままならない。
2. オブジェクト参照の放置(メモリリーク)
ProjectのCOMオブジェクト(特に `Task` や `Resource` のコレクション、および外部のExcelやOutlook連携)の解放漏れによるメモリ肥大化。
3. エラーハンドリングの欠如とサイレントキル
`On Error Resume Next` でエラーを握り潰した結果、タスクのリンク切れや不正なベースライン設定がサイレントに進行する。

これらを打破するためには、「責務の分離」「オブジェクトのライフサイクル管理」をコードに強制する必要がある。

2. アーキテクチャの設計思想:マネージャーとエンティティの分離

保守性を極限まで高めるためのリファクタリングでは、VBAのクラスモジュールを以下のように役割分担させる。

  • Entity(エンティティ)クラス: 個々のMS Projectオブジェクト(タスクやリソース)をラップし、ビジネスロジックをカプセル化する。
  • Manager(マネージャー)クラス: コレクションの操作、一括処理、外部システムとの連携を統括する。

今回は、実務で最も頻発する「特定条件のタスク抽出・一括更新・ベースライン設定」の処理を例に、レガシーコードをクラス化する手順を示す。

3. 実装コード:スパゲッティからの脱却

ステップ1:タスクをカプセル化するエンティティクラス (`clsProjectTask`)

まずは、MS Projectの生(ロー)の `Task` オブジェクトを安全に扱い、独自のプロパティやメソッドを持たせるクラスを作成する。

‘ =========================================================================
‘ クラスモジュール名: clsProjectTask
‘ 概要:MS Projectの個別タスクをカプセル化し、安全な操作インターフェースを提供
‘ =========================================================================
Option Explicit

Private m_Task As Task
Private m_IsValid As Boolean

‘ イニシャライザー(事実上のコンストラクタ)
Public Sub Initialize(ByRef targetTask As Task)
If Not targetTask Is Nothing Then
Set m_Task = targetTask
m_IsValid = True
Else
m_IsValid = False
End If
End Sub

‘ プロパティ: タスク名
Public Property Get Name() As String
If m_IsValid Then Name = m_Task.Name
End Property
Public Property Let Name(ByVal value As String)
If m_IsValid Then m_Task.Name = value
End Property

‘ プロパティ: 独自制約付きの進捗率設定(0〜100に丸める)
Public Property Let PercentComplete(ByVal value As Integer)
If Not m_IsValid Then Exit Property

If value < 0 Then m_Task.PercentComplete = 0 ElseIf value > 100 Then
m_Task.PercentComplete = 100
Else
m_Task.PercentComplete = value
End If
End Property

Public Property Get PercentComplete() As Integer
If m_IsValid Then PercentComplete = m_Task.PercentComplete
End Property

‘ メソッド: 独自のビジネスロジック(例:クリティカルパスかつ未着手のタスク判定)
Public Function IsCriticalAndNotStarted() As Boolean
If Not m_IsValid Then Exit Function
IsCriticalAndNotStarted = (m_Task.Critical = True) And (m_Task.PercentComplete = 0)
End Function

‘ 終了処理(メモリ解放の明示)
Private Sub Class_Terminate()
Set m_Task = Nothing
End Sub

ステップ2:プロジェクト全体を統括するマネージャークラス (`clsProjectManager`)

次に、プロジェクトファイルへのアクセス、エラーハンドリング、トランザクション的な処理(ベースライン設定など)を隠蔽するマネージャークラスを作成する。

‘ =========================================================================
‘ クラスモジュール名: clsProjectManager
‘ 概要: プロジェクト全体の操作とバッチ処理を統括するマネージャー
‘ =========================================================================
Option Explicit

Private m_Project As Project

Public Sub Initialize(ByRef targetProject As Project)
If targetProject Is Nothing Then
Err.Raise 50001, “clsProjectManager”, “有効なプロジェクトが指定されていません。”
End If
Set m_Project = targetProject
End Sub

‘ メソッド: 安全なベースライン設定(APIの挙動をラップ)
Public Sub ApplyBaselineSecurely(ByVal baselineIndex As PjBaseline)
On Error GoTo ErrorHandler

‘ パフォーマンス向上のため画面描画を停止
Application.ScreenUpdating = False

‘ 指定されたベースラインを設定(例: pjBaseline)
m_Project.SetBaseline Baseline:=baselineIndex

Application.ScreenUpdating = True
Exit Sub

ErrorHandler:
Application.ScreenUpdating = True
‘ エラーログの記録やカスタム例外の送出
Err.Raise Err.Number, “clsProjectManager.ApplyBaselineSecurely”, “ベースラインの設定に失敗しました: ” & Err.Description
End Sub

‘ メソッド: 条件に合致するタスクを一括操作する
Public Sub ProcessCriticalTasks()
Dim t As Task
Dim wrappedTask As clsProjectTask

On Error GoTo CleanUp

For Each t In m_Project.Tasks
If Not t Is Nothing Then
If Not t.Summary Then ‘ サマリータスクを除外
Set wrappedTask = New clsProjectTask
wrappedTask.Initialize t

If wrappedTask.IsCriticalAndNotStarted() Then
‘ 例:クリティカルかつ未着手ならタスク名にプレフィックスを付与
wrappedTask.Name = “[URGENT] ” & wrappedTask.Name
End If

Set wrappedTask = Nothing
End If
End If
Next t

CleanUp:
Set wrappedTask = Nothing
If Err.Number <> 0 Then
Err.Raise Err.Number, “clsProjectManager.ProcessCriticalTasks”, Err.Description
End If
End Sub

Private Sub Class_Terminate()
Set m_Project = Nothing
End Sub

ステップ3:エントリポイント(標準モジュール)の極限までの簡素化

リファクタリング後の標準モジュールは、インスタンスを生成してメソッドを叩くだけの「薄いコントローラー」になる。

‘ =========================================================================
‘ 標準モジュール名: modMain
‘ 概要: 処理のエントリポイント(極限までシンプルに保つ)
‘ =========================================================================
Option Explicit

Public Sub Main_ExecuteProjectOptimization()
‘ 厳格なエラーハンドリング
Dim projMgr As clsProjectManager

On Error GoTo ErrorHandler

‘ アクティブプロジェクトの存在確認
If ActiveProject Is Nothing Then
MsgBox “開かれているプロジェクトがありません。”, vbCritical, “システムエラー”
Exit Sub
End If

‘ マネージャーの初期化
Set projMgr = New clsProjectManager
projMgr.Initialize ActiveProject

‘ 1. クリーティカルタスクの処理
projMgr.ProcessCriticalTasks

‘ 2. ベースラインの安全な設定
projMgr.ApplyBaselineSecurely pjBaseline

MsgBox “プロジェクトの最適化処理が正常に完了しました。”, vbInformation, “完了”

CleanUp:
Set projMgr = Nothing
Exit Sub

ErrorHandler:
MsgBox “エラーが発生しました [” & Err.Number & “]: ” & Err.Description, vbCritical, “致命的エラー”
Resume CleanUp
End Sub

4. シニアエンジニアが押さえるべき「メモリとパフォーマンス」の深層知見

MS ProjectのVBAにおいて、なぜこのようなクラス化とオブジェクトの明示的解放(`Set … = Nothing`)が不可欠なのか。その背景にあるアーキテクチャの真実を解説する。

1. COM境界を越える参照のコスト

VBAのランタイム(VB環境)と、MS Projectの背後でうごめくC++製COMコンポーネントの間には「COM境界(Interop Boundary)」が存在する。
`For Each t In m_Project.Tasks` のようなループ内で、生のエントリーをそのまま雑に扱い続けると、COM RCW(Runtime Callable Wrapper)の参照カウントが意図せず跳ね上がり、VBA側でメモリリークを引き起こす。
クラスモジュール(`clsProjectTask`)でラップし、`Class_Terminate` で確実に対象を解放する構造にすることで、ガベージコレクションに頼れないVBA環境下でもメモリフットプリントを安定させられる。

2. `ScreenUpdating` とイベントハンドラの制御

大規模なスケジュールファイル(数万行のWBS)に対してVBAで一括処理を行う際、画面描画や自動計算(Calculation)が有効なままだと、1回のプロパティ変更ごとにMS Projectのエンジン全体がスケジュール再計算を走り、パフォーマンスが数分の一に低下する。
マネージャークラスの内部で `Application.ScreenUpdating = False` や `Application.Calculation = pjManual` を適切にスコープ管理(RAIIパターンの模倣)することで、爆発的な高速化を実現できる。

5. レガシーからの脱却は「設計」からしか始まらない

「動けばいい」で作られたレガシーVBAは、企業の業務拡大とともに必ず破綻する。
今回紹介したクラスモジュールによるカプセル化と、責務の明確な分離は、単にコードを綺麗に見せるためではない。「変更に強く、障害箇所が局所化され、テストが可能なコードベース」を構築するためのエンジニアリングの基本原則である。

もしあなたが今、何千行ものスパゲッティコードのメンテナンスに苦しんでいるのなら、まずはエントリポイントからビジネスロジックを切り離し、小さく堅牢なクラスへとリファクタリングを始めてほしい。その投資は、必ず将来の保守工数の削減という形で何倍にもなって返ってくるはずだ。

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