スコープの汚染を防ぐ:PrivateとPublicの厳格な使い分けルール
Excel VBAにおける開発規模が数百、数千行を超えた瞬間、多くのプログラマブルな負債が表面化する。その最たるものが「グローバル変数の氾濫とスコープ汚染」である。
「どこからでも値にアクセスできるから便利」という安易な理由で、標準モジュールの頭に `Public` を乱立させる設計は、システムを確実に崩壊へと導く。意図しないタイミングでの値の書き換え、デバッグの困難化、そして何よりメモリ上のオブジェクト参照が意図せず保持され続けることによるメモリリーク。これらは「VBAだから仕方ない」のではない。アーキテクトの怠慢である。
本稿では、エンタープライズ領域のVBA開発において、モジュール間の結合度を極限まで下げ、保守性とパフォーマンスを両立させるための `Private` と `Public` の厳格な使い分けルールを、メモリ管理とWindows API連携の観点を交えて解説する。
—
1. なぜ `Public` の乱立はシステムを殺すのか
VBAの実行基盤であるVBA環境(VBE)は、単一のグローバル名前空間を共有する。標準モジュール(`Module1`, `Module2`…)に宣言された `Public` 変数は、プロジェクトがロードされている間、常にメモリ上に存在し続け、どのプロシージャからでも無制限に読み書きが可能になる。
これは、オブジェクト指向における「カプセル化」の完全な破壊を意味する。
‘ 【悪夢のアンチパターン】標準モジュール:Globals
Public g_TargetSheet As Worksheet
Public g_LastRow As Long
Public g_IsProcessing As Boolean
このようなコードが存在するシステムでは、Aという処理で行った `g_LastRow` の書き換えが、全く無関係なBという処理のバグ誘発を引き起こす。いわゆる「副作用(Side Effect)」の温床だ。シニアエンジニアたる者、変数の生存期間(Lifetime)と可視性(Visibility)は、必要最小限に絞らなければならない。
—
2. カプセル化の極意:標準モジュールを捨て、クラスモジュールを使う
真に堅牢なVBAアーキテクチャでは、データとその操作ロジックを同一のカプセル(クラスモジュール)に閉じ込める。これにより、外部から直接書き換えられたくないメンバを隠蔽し、アクセサ(Propertyプロシージャ)を介した制御が可能になる。
以下の例では、外部からの不正な値の代入を防ぎつつ、内部状態を完全に制御するクラス設計を示す。
実装例:安全な設定管理クラス (`clsAppSettings`)
‘ ==========================================
‘ クラスモジュール名: clsAppSettings
‘ ==========================================
Option Explicit
‘ プライベート変数(外部から直接アクセス不可)
Private m_ExportPath As String
Private m_TimeoutSeconds As Long
‘ プロパティ:ExportPath(取得)
Public Property Get ExportPath() As String
ExportPath = m_ExportPath
End Property
‘ プロパティ:ExportPath(設定・バリデーション付き)
Public Property Let ExportPath(ByVal Value As String)
If Len(Trim$(Value)) = 0 Then
Err.Raise 5, “clsAppSettings”, “出力パスが空です。”
End If
‘ パスの存在確認などの堅牢なバリデーションをここに記述
m_ExportPath = Value
End Property
‘ プロパティ:TimeoutSeconds(取得)
Public Property Get TimeoutSeconds() As Long
TimeoutSeconds = m_TimeoutSeconds
End Property
‘ プロパティ:TimeoutSeconds(設定・範囲チェック付き)
Public Property Let TimeoutSeconds(ByVal Value As Long)
If Value <= 0 Or Value > 300 Then
Err.Raise 5, “clsAppSettings”, “タイムアウトは1〜300の範囲で指定してください。”
End If
m_TimeoutSeconds = Value
End Property
‘ クラス初期化時の処理
Private Sub Class_Initialize()
m_TimeoutSeconds = 30 ‘ デフォルト値
End Sub
このクラスを利用する側(呼び出し元)は、変数のスコープを意識することなく、安全にインスタンスを生成・破棄できる。
—
3. メモリ最適化とオブジェクトの明示的解放
`Public` なオブジェクト変数を保持し続けることの最大の問題は、COMコンポーネントの解放漏れによるメモリリークである。特にExcel VBAから外部アプリケーション(Word、Outlook、あるいはWindows API経由のハンドルなど)を操作する場合、スコープ管理を誤るとプロセスがメモリ上に残留し続ける。
Windows API連携におけるスコープの厳格管理
Windows APIを呼び出す際、APIの宣言自体は `Private Declare PtrSafe` を基本とし、それをラップする専用の標準モジュール、あるいはクラス内部に閉じるべきである。グローバルなAPI定義は名前空間を汚染する。
以下は、メモリ最適化とスコープを意識したAPI利用の模範例である。
‘ ==========================================
‘ 標準モジュール: modNativeUtils
‘ ==========================================
Option Explicit
‘ API宣言は必ず Private にし、カプセル化されたモジュール内にとどめる
If VBA7 Then
Private Declare PtrSafe Function SetCursorPos Lib “user32” (ByVal x As Long, ByVal y As Long) As Long
Private Declare PtrSafe Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
Else
Private Declare Function SetCursorPos Lib “user32” (ByVal x As Long, ByVal y As Long) As Long
Private Declare Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
End If
‘ 外部へ公開するインターフェース(Publicプロシージャ)
Public Sub SafeWaitAndMove(ByVal msec As Long, ByVal targetX As Long, ByVal targetY As Long)
‘ スコープを限定したローカル変数による処理
Dim result As Long
If msec > 0 Then
Call Sleep(msec)
End If
result = SetCursorPos(targetX, targetY)
If result = 0 Then
Err.Raise 9999, “SafeWaitAndMove”, “カーソル位置の移動に失敗しました。”
End If
End Sub
オブジェクトの明示的解放(Destructorパターン)
クラスモジュール内では、参照したExcelオブジェクト(`Worksheet`, `Range` 等)や外部COMオブジェクトは、処理の終了時に必ず `Nothing` を代入して参照カウントをデクリメントしなければならない。
‘ ==========================================
‘ クラスモジュール: clsDataProcessor
‘ ==========================================
Option Explicit
Private m_Workbook As Workbook
Public Sub Initialize(ByVal filePath As String)
‘ 外部から渡されたワークブックをカプセル化
Set m_Workbook = Workbooks.Open(filePath, ReadOnly:=True)
End Sub
Public Sub ProcessData()
If m_Workbook Is Nothing Then Exit Sub
Dim ws As Worksheet
Set ws = m_Workbook.Sheets(1)
‘ 処理ロジック…
‘ ローカルオブジェクトの解放
Set ws = Nothing
End Sub
‘ クラス破棄時の確実なクリーンアップ
Private Sub Class_Terminate()
If Not m_Workbook Is Nothing Then
m_Workbook.Close SaveChanges:=False
Set m_Workbook = Nothing
End If
End Sub
呼び出し側で `Set processor = Nothing` を実行した瞬間、`Class_Terminate` が走る。これにより、ファイルが開いたままプロセスが残存するリーク事故を根絶できる。
—
4. レガシー環境の保守におけるリファクタリング戦略
すでに存在する「グローバル変数が縦横無尽に飛び交うレガシーコードベース」を保守・改修する場合、一気に書き換えるのはリスクが高すぎる。以下のステップで段階的にスコープを狭めるリファクタリングを推奨する。
1. 可視性の縮小(`Public` → `Private` への移行テスト)
- 該当の変数を `Private` に変更し、VBEの「コンパイル」を実行する。
- エラーが出た箇所が、その変数を不正に依存していた箇所(設計上のバグ)である。
2. Getter/Setterの導入
- 直接変数を参照している箇所を、プロシージャ経由のアクセスに置き換える。
3. モジュールの分割
- 1,000行を超える単一の標準モジュールを、責務ごとにクラスモジュールへ分割する。
—
5. チーフアーキテクトからの提言
VBAは、その手軽さゆえに「動けばいい」という低品質なコードが量産されやすい言語である。しかし、業務の根幹を支える基幹システムとして運用する場合、アーキテクチャの優劣がそのまま組織の生産性を左右する。
- 変数は原則として `Private`。
- グローバル変数(`Public`)の定義は、アプリケーションの状態管理など、真に不可欠な最小限のエントリポイントに限定する。
- オブジェクトは使い捨て、スコープを抜ける前、あるいはクラスの破棄時に必ず `Nothing` で解放する。
この鉄の掟を遵守することで、あなたの書くVBAコードは、他のどの言語にも劣らない堅牢性と美しさを獲得するだろう。妥協なき設計を、すべての現場で。
