【テクニカル・上級編】【上級プロ】クラスモジュールを用いた「タスク管理エンジン」の構築:複雑なロジックのカプセル化 – Project VBA解析バイブル

スポンサーリンク

【上級プロ】クラスモジュールを用いた「タスク管理エンジン」の構築:複雑なロジックのカプセル化

MS ProjectのVBA開発において、標準の `Task` オブジェクトや `Assignment` オブジェクトをそのまま手続き型のスパゲッティコードで操作する時代は終わった。数万行に及ぶ大規模なスケジュール管理アドインや、基幹システムとの双方向連携において、ビジネスロジックがUI層やデータ層と密結合している設計は、遅かれ早かれ「保守不可能なレガシー」という名の負債と化す。

真にスケーラブルで堅牢なProject VBAアーキテクチャを構築するためには、クラスモジュールによるオブジェクト指向のカプセル化が不可欠だ。

今回は、生(生体)の `Task` オブジェクトを安全にラップし、高度なバリデーション、動的な進捗計算、さらにはメモリ管理と例外処理のライフサイクルを完全に制御する「タスク管理エンジン」の設計思想と実装の全貌を公開する。

1. なぜProject VBAに「クラスエンジン」が必要なのか

MS ProjectのCOMオブジェクトモデルは、強力であると同時に極めて脆弱だ。
特に以下の課題が現場のエンジニアを苦しめる。

  • イベント駆動の欠如: Projectのネイティブイベントは限定的であり、タスクのプロパティ変更(コスト超過、工期遅延など)をリアクティブに検知する機構がない。
  • 暗黙の参照保持によるメモリリーク: VBAのガベージコレクションは参照カウント方式(COMの `AddRef`/`Release`)に依存しているが、オブジェクト変数の解放漏れや循環参照により、Projectプロセス(WINPROJ.EXE)がメモリ上に残留し、最悪の場合はファイル破損を引き起こす。
  • ビジネスルールの散在: 「特定のWBS階層以下は工数変更を禁止する」「前提条件を満たしていないタスクのステータス変更を弾く」といったバリデーションが、各モジュールのあちこちに散らばる惨状。

これらを解決するのが、「TaskWrapperクラス」を中核としたドメイン駆動的なカプセル化である。

2. アーキテクチャの全体像

今回構築する「タスク管理エンジン」は、以下の3層構造をとる。

1. Wrapper層 (` clsManagedTask`): 生の `Task` オブジェクトを保持し、プロパティのゲッター/セッターを通じてビジネスロジック(バリデーション等)を強制する。
2. Collection層 (` clsTaskManager`): 複数の `clsManagedTask` を一元管理し、一括処理やパフォーマンス最適化(画面描画の抑止など)を統括する。
3. Client層 (標準モジュール): エンジンを呼び出すビジネスロジックのエントリーポイント。

3. 実装コード:堅牢な「TaskWrapper」クラスの構築

VBAのVBEを開き、クラスモジュールを挿入して名前を `clsManagedTask` とする。

以下のコードは、生の `Task` オブジェクトへのアクセスを完全に抽象化し、不正なデータの混入を防ぐバリデーションロジックを内包した決定版だ。

‘ =================================================================
‘ クラス名: clsManagedTask
‘ 概要: MS ProjectのTaskオブジェクトをラップし、ビジネスロジックをカプセル化する
‘ =================================================================
Option Explicit

‘ 保持するネイティブオブジェクト
Private m_RawTask As Object ‘ MSProject.Task

‘ 内部状態管理用プライベート変数
Private m_IsDirty As Boolean
Private m_CachedCost As Currency

‘ イベント風コールバック用のデリゲート(必要に応じて拡張)
‘ Private m_ParentManager As Object

‘ — イニシャライザとデストラクタ —
Private Sub Class_Initialize()
m_IsDirty = False
m_CachedCost = -1
End Sub

Private Sub Class_Terminate()
‘ 明示的な参照の切断(COMのメモリリーク防止の極意)
Set m_RawTask = Nothing
End Sub

‘ — 初期化インジェクション —
Public Sub Initialize(ByRef TargetTask As Object)
If TargetTask Is Nothing Then
Err.Raise 91, “clsManagedTask”, “ラップ対象のTaskオブジェクトがNothingです。”
End If
Set m_RawTask = TargetTask
m_IsDirty = True
End Sub

‘ — プロパティ:カプセル化とバリデーション —

Public Property Get ID() As Long
If m_RawTask Is Nothing Then Exit Property
ID = m_RawTask.ID
End Property

Public Property Get UniqueID() As Long
If m_RawTask Is Nothing Then Exit Property
UniqueID = m_RawTask.UniqueID
End Property

Public Property Get Name() As String
If m_RawTask Is Nothing Then Exit Property
Name = m_RawTask.Name
End Property

Public Property Let Name(ByVal Value As String)
‘ 【バリデーション】空白名の禁止および特殊文字の排除
Value = Trim$(Value)
If Len(Value) = 0 Then
Err.Raise 380, “clsManagedTask”, “タスク名を空にすることは許可されていません。”
End If

If InStr(1, Value, “¥”) > 0 Or InStr(1, Value, “/”) > 0 Then
Err.Raise 380, “clsManagedTask”, “タスク名にパス区切り文字を含めることはできません。”
End If

If Not m_RawTask Is Nothing Then
m_RawTask.Name = Value
m_IsDirty = True
End If
End Property

Public Property Get PercentComplete() As Integer
If m_RawTask Is Nothing Then Exit Property
PercentComplete = m_RawTask.PercentComplete
End Property

Public Property Let PercentComplete(ByVal Value As Integer)
‘ 【ビジネスルール】進捗率は0〜100の整数に強制
If Value < 0 Then Value = 0 If Value > 100 Then Value = 100

‘ 【ロジック】親タスク(サマリータスク)への直接書き込み制御
If Not m_RawTask Is Nothing Then
If m_RawTask.Summary Then
‘ サマリータスクは通常読み取り専用だが、強制上書きが必要な場合の例外処理など
‘ ここでは警告またはスルーする設計にする
End If
m_RawTask.PercentComplete = Value
m_IsDirty = True
End If
End Property

‘ — 高度なビジネスロジック:動的コスト計算とキャッシュ機構 —
Public Property Get CalculatedCost() As Currency
If m_RawTask Is Nothing Then Exit Property

‘ パフォーマンス最適化:変更がない場合はキャッシュを返すことでCOM通信コストを削減
If Not m_IsDirty And m_CachedCost >= 0 Then
CalculatedCost = m_CachedCost
Exit Property
End If

‘ 複雑なコスト算出ロジック(例:残工数×標準単価 + 独自のアドオンコスト)
Dim baseCost As Currency
baseCost = m_RawTask.Cost

‘ ここに社内固有の複雑な財務計算ロジックをカプセル化する
m_CachedCost = baseCost 1.05 ‘ 例: 管理費5%上乗せ
m_IsDirty = False

CalculatedCost = m_CachedCost
End Property

‘ — ネイティブオブジェクトへの安全なエスケープ hatch —
Public Property Get GetRawTask() As Object
Set GetRawTask = m_RawTask
End Property

4. 実装コード:マネージャー層によるパフォーマンス最適化

数千件のタスクをループ処理する際、MS Projectはセルやプロパティが変更されるたびに画面の再描画(UIの再計算)走り、実行速度が劇的に低下する。
これを防ぐため、コレクション層である `clsTaskManager` でトランザクション的な一括処理(ScreenUpdatingの制御)を実装する。

標準モジュールまたはクラス (`clsTaskManager`):

‘ =================================================================
‘ クラス名: clsTaskManager
‘ 概要: 複数のclsManagedTaskを統括し、一括処理とパフォーマンスを制御する
‘ =================================================================
Option Explicit

Private m_Tasks As Collection

Private Sub Class_Initialize()
Set m_Tasks = New Collection
End Sub

Private Sub Class_Terminate()
‘ コレクション内の全オブジェクトを確実に破棄
Dim t As Variant
For Each t In m_Tasks
‘ 各ラップクラスのデストラクタを誘発
Next t
Set m_Tasks = Nothing
End Sub

Public Sub LoadProjectTasks(ByRef proj As Object)
Dim t As Object
Dim managedTask As clsManagedTask

‘ 【パフォーマンス極限最適化】画面描画と自動再計算を停止
Application.ScreenUpdating False
Application.Calculation = pjManual

On Error GoTo ErrorHandler

Set m_Tasks = New Collection
For Each t in proj.Tasks
If Not t Is Nothing Then
Set managedTask = New clsManagedTask
managedTask.Initialize t
m_Tasks.Add managedTask, “K_” & t.UniqueID
End If
Next t

ErrorHandler:
‘ 確実に設定を復元する(これを怠るとProjectがフリーズしたような挙動になる)
Application.Calculation = pjAutomatic
Application.ScreenUpdating True

If Err.Number <> 0 Then
Err.Raise Err.Number, “clsTaskManager”, “タスク読み込み中に致命的なエラーが発生しました: ” & Err.Description
End If
End Sub

Public Function GetTaskByUniqueID(ByVal UniqueID As Long) As clsManagedTask
On Error Resume Next
Set GetTaskByUniqueID = m_Tasks(“K_” & UniqueID)
On Error GoTo 0
End Function

5. レガシー環境・システム間連携における実践的知見

このようなクラス設計を導入することで、外部システム(REST APIやERP、Excelマスタなど)との連携において圧倒的な優位性が生まれる。

1. 例外処理とトランザクションの保証

外部APIから取得したJSONデータをタスクに反映する際、途中でバリデーションエラー(例:存在しないリソースの割り当てや不正な日付)が発生した場合、プロジェクト全体が中途半端な状態で保存されてはならない。
クラスモジュール化されていれば、変更履歴を一時バッファに保持し、全データの検証が完了した後に一括コミット(またはエラー時のロールバック)するパターンを容易に実装できる。

2. COMオブジェクトの解放とメモリ管理の鉄則

VBAにおける最大の罠は、「オブジェクト変数の参照が1つでも残っていると、アプリケーション終了後もプロセスがゾンビとしてメモリに残り続ける」という点である。
特にMS Projectのオブジェクトモデルは階層が深く(`ActiveProject.Tasks.Item(1)…`)、暗黙の参照生成が多発する。
今回のコードのように:

  • ループ内でのオブジェクト取得は必ず変数に受ける。
  • クラスの `Class_Terminate` で `Set m_RawTask = Nothing` を明示的に呼ぶ。
  • 終了時にはコレクション自体を `Nothing` にクリアする。

この鉄則を徹底することで、何日稼働させてもメモリリークを起こさない堅牢な常駐型マクロ・アドインが完成する。

6. まとめ

VBAは「簡易的なスクリプト言語」ではない。アーキテクチャの設計次第で、エンタープライズレベルの耐障害性と拡張性を持つ強力な開発プラットフォームに変貌する。

今回解説したクラスモジュールによるカプセル化とパフォーマンス制御の極意をあなたのプロジェクトに導入し、保守性の呪縛から完全に解放された次世代のVBA開発を実現してほしい。

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