【テクニカル・上級編】初心者向け:VB.NETにおけるModule(モジュール)の正しい使い方と、グローバル変数を乱用しないための設計指針 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

VB.NET Moduleの呪縛と解放:グローバル変数を断ち、オブジェクト指向の極限へ

VB.NETにおける `Module` は、VBAやVB6からの移行者にとってあまりにも甘美な存在だ。インスタンス化の呪縛から逃れ、どこからでも呼び出せるユーティリティ関数やグローバル変数を量産できる。

しかし、長年レガシーシステムの最前線に立ち、数百万行に及ぶスパゲッティコードの鎮圧に当たってきた私から言わせてもらえば、無計画なModuleの使用とグローバル変数の乱用は、システムの寿命を削る「合法的な毒薬」に他ならない。

今回は、Moduleの本質的な挙動、メモリ管理の裏側、そしてグローバル変数が引き起こす悪夢からシステムを救うための設計指針を、実戦的なコードとともに徹底的に解説する。

1. Moduleの本質:C#にはない「syntactic sugar(糖衣構文)」の正体

VB.NETの `Module` は、一見すると独自のオブジェクト指向構造を持っているように見えるが、IL(Intermediate Language)レベルで見ればその正体は明白である。

コンパイラは、`Module` を「すべてが `Public Shared`(静的)で構成され、インスタンス化が不可能(`NotInheritable`)なSealedクラス」へと翻訳する。

‘ 私たちが書くコード
Module AppUtility
Public Sub Execute()
‘ …
End Sub
End Module

これがコンパイルされると、裏側では以下のようなC#相当のクラスが生成されている。

// 内部的な実体
[StandardModule]
public sealed class AppUtility {
public static void Execute() {
// …
}
}

なぜこれが危険なのか?

`Shared` メンバーの本質は「アプリケーションのライフサイクル全体にわたってメモリ上に常駐し続けること」である。
数個のユーティリティ関数であれば問題ないが、ここに業務データや状態(State)を持たせた瞬間、ガベージコレクション(GC)の恩恵を受けられないメモリリークの温床、あるいはマルチスレッド環境における競合(Race Condition)のトリガーとなる。

2. レガシー現場の悪夢:グローバル変数がもたらす結合度の地獄

「どこからでもアクセスできる」という利便性は、システム全体の結合度(Coupling)を極限まで高める。

以下のアンチパターンを見てほしい。

‘ 【アンチパターン】グローバル変数による状態管理
Module GlobalState
Public CurrentUser As UserInfo
Public DBConnection As SqlConnection
Public IsProcessing As Boolean
End Module

この設計の何が致命的か:
1. 依存関係の隠蔽(Hidden Dependencies): どのメソッドがどの変数書き換えているのか、コードを上から下まで追いかけなければ分からない。
2. 単体テストの不可能(Untestability): グローバルな状態に依存しているコードは、テストの実行順序によって結果が変わり、モック化(Mocking)が極めて困難になる。
3. 並行処理の崩壊(Thread Safety): マルチスレッドやAsync/Awaitを使用した非同期処理において、予期せぬタイミングで `CurrentUser` が書き換えられる致命的なバグを生む。

3. Windows APIとメモリ最適化におけるModuleの「正しい使い所」

では、Moduleは一切使うべきではないのか?答えは「NO」だ。
オブジェクト指向の原則に則りつつ、ステートレス(状態を持たない)なP/Invoke(Platform Invoke)のラッパーとしては、Moduleは依然として極めて有用な選択肢である。

以下に、Windows API(例:メモリのコピーや安全なリソース解放)を安全に扱う実戦的なモジュールの実装を示す。

Imports System.Runtime.InteropServices

‘ ステートレスなAPIラッパーとしての正しいModule使用法
Public NotInheritable Module NativeMethods

‘ Win32 APIの定義(状態を持たず、純粋な入出力のみ)

Public Friend Declare Function CopyMemory(
ByVal dest As IntPtr,
ByVal src As IntPtr,
ByVal length As UIntPtr
) As IntPtr

”’

”’ マネージド/アンマネージドの境界を越えた安全なメモリ操作の例
”’

Public Sub SafeCopy(sourceArray As Byte(), ByRef destArray As Byte())
If sourceArray Is Nothing OrElse destArray Is Nothing Then
Throw New ArgumentNullException(“Arrays must not be null.”)
End If

If destArray.Length < sourceArray.Length Then Throw New ArgumentException("Destination array is too small.") } ' GCHandleによるピン留め(GCによるメモリ移動を防ぐ) Dim srcHandle As GCHandle = GCHandle.Alloc(sourceArray, GCHandleType.Pinned) Dim destHandle As GCHandle = GCHandle.Alloc(destArray, GCHandleType.Pinned) Try Dim srcPtr As IntPtr = srcHandle.AddrOfPinnedObject() agents Dim destPtr As IntPtr = destHandle.AddrOfPinnedObject() CopyMemory(destPtr, srcPtr, CType(sourceArray.Length, UIntPtr)) Finally ' 確実にハンドルを解放し、メモリリークを防ぐ If srcHandle.IsAllocated Then srcHandle.Free() If destHandle.IsAllocated Then destHandle.Free() End Try End Sub End Module 【エンジニアの知見】
このコードでは、`GCHandle` を用いてガベージコレクタからメモリを守りつつ、`Try…Finally` 構文によって例外発生時であっても確実にハンドルを解放(`Free()`)している。ステートレスな関数であれば、Moduleであっても堅牢性を担保できる。

4. オブジェクト指向への脱却:DI(依存性注入)とインスタンス化の設計指針

グローバル変数を排除し、保守性の高いモダンなアーキテクチャへ移行するためには、「静的(Shared)への依存」を「インスタンスとインターフェースへの依存」に置き換える必要がある。

ステップ1:インターフェースの定義

まずは、処理の契約(Interface)を定義する。

Public Interface IUserService
Function GetCurrentUser() As UserInfo
Sub SetCurrentUser(user As UserInfo)
End Interface

ステップ2:具象クラスの実装

状態(State)は、グローバル空間ではなく「オブジェクトのライフサイクル内」に閉じ込める。

Public Class UserService
Implements IUserService

‘ 状態はインスタンス変数として保持する
Private _currentUser As UserInfo

Public Function GetCurrentUser() As UserInfo Implements IUserService.GetCurrentUser
Return _currentUser
End Function

Public Sub SetCurrentUser(user As UserInfo) Implements IUserService.SetCurrentUser
_currentUser = user
End Sub
End Class

ステップ3:DIコンテナやコンストラクタインジェクションの活用

呼び出し側では、インスタンスを注入(Inject)することで、テスト容易性と疎結合性を同時に手に入れる。

Public Class OrderProcessor
Private ReadOnly _userService As IUserService

‘ コンストラクタインジェクションによる依存性の注入
Public Sub New(userService As IUserService)
_userService = userService
End Sub

Public Sub ProcessOrder()
Dim user = _userService.GetCurrentUser()
If user Is Nothing Then
Throw New InvalidOperationException(“User is not authenticated.”)
End If

‘ 業務ロジックの実行…
End Sub
End Class

このように設計することで、単体テスト時には `IUserService` のモック(Mock)を簡単に渡し、予期せぬグローバル状態の汚染からテスト環境を守ることができる。

総括:伝説的アーキテクチャからの提言

VB.NETの `Module` とグローバル変数は、コードを素早く書き上げるための麻薬のようなものだ。小規模なスクリプトやレガシーなVBAからの移植初期においては有効かもしれないが、システムが成長するにつれ、開発チームの足枷となり、テクニカルデット(技術的負債)の山を築くこと主犯となる。

  • Moduleは「ステートレスなユーティリティ」と「P/Invoke」に限定せよ。
  • グローバル変数は一切排除し、状態はインスタンスとDIによって管理せよ。

この鉄則を守るだけで、あなたの書くVB.NETコードの品質は、レガシーの枠を超え、モダンなエンタープライズ水準へと劇的に進化するはずだ。アーキテクチャの主導権を、言語の甘えに渡してはならない。

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