【実務・中級編】VB.NETにおけるModule(モジュール)の功罪:グローバル関数を乱用しないためのオブジェクト指向リファクタリング – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

VB.NETにおけるModule(モジュール)の功罪:グローバル関数を乱用しないためのオブジェクト指向リファクタリング

開発現場でこんなコードを見たことはないだろうか。

Module GlobalHelper
Public DbConnection As SqlConnection

Public Function FormatDate(ByVal d As DateTime) As String
Return d.ToString(“yyyy/MM/dd”)
End Function
End Module

VB.NETに慣れ親しんだプログラマにとって、`Module` は非常に魅力的な機能だ。インスタンス化の呪縛から解放され、どこからでもドット繋ぎで関数を呼び出せる。VBA(Visual Basic for Applications)時代からの名残もあり、共通関数置き場としてとりあえず `Module` に処理を詰め込んでいく。

しかし、私がリードするプロジェクトにおいて、理由なき `Module` の使用は「技術的負債の第一歩」として厳禁としている。

今回は、VB.NETの `Module` がもたらす甘い罠(功)と、それによって引き起こされる保守性の崩壊(罪)を解き明かし、現場でスケーラブルなコードを書くためのオブジェクト指向リファクタリング手法を伝授する。

—

1. なぜ `Module` は危険なのか?(罪の正体)

`Module` は裏側で「静的クラス(Static Class / Shared Class)」としてコンパイルされる。すべてのメンバが `Shared`(C#でいう `static`)扱いとなり、アプリケーションのライフサイクル全体にわたってメモリ上に存在し続ける。

これが実務の現場で牙をむく理由は主に3つある。

① 状態の共有による「予期せぬサイドエフェクト(副作用)」

冒頭のコードのように、`Module` 内にグローバルな変数を持たせた場合、アプリケーションのどこからでもその値を書き換えられる。
「A画面の処理が終わった後にB画面を開いたら、なぜかDB接続が切れていた」「あるスレッドの処理が、別のスレッドの変数を書き換えてしまった」――こうしたマルチスレッド環境や複雑な画面遷移におけるバグの温床となる。

② 単体テスト(Unit Test)の不可能性

これが最大の罪だ。`Module` 内のメソッドが外部のファイルシステム、DB、あるいは別のグローバル変数に依存している場合、テスト時にその依存関係を「モック(Mock)」に置き換えることができない。
結果として、「動かしてみないと分からない」という、テスト駆動開発(TDD)とは真逆のレガシーなデバッグ作業に時間を奪われることになる。

③ オブジェクト指向の原則(SOLID原則)への違反

「何でも屋(God Object / God Module)」が生まれやすい。関連性のないユーティリティ関数が数百行もダラダラと並んだモジュールは、誰にも全貌を把握できず、改修するたびにデグレ(退行不具合)を引き起こす。

—

2. 唯一許容される `Module` の使い道(功の側面)

もちろん、VB.NETにおける `Module` が完全悪というわけではない。以下の要件を満たす場合に限り、その利便性を享受してよい。

  • 状態を持たない(Statelessであること):内部に変数を保持せず、入力値に対して一意に出力を返す純粋関数(Pure Function)のみで構成されている。
  • 言語拡張や真のユーティリティ:例えば、C#の拡張メソッド(Extension Methods)を定義するためには、VB.NETでも `Module` を使用する必要がある。

—

3. 実践:グローバルモジュールから「堅牢なクラス設計」へのリファクタリング

ここからは、実務でよくある「ファイル読み込みとログ出力を行うグローバル関数」を例に、オブジェクト指向の設計へリファクタリングする手順を示す。

❌ アンチパターン:すべてを詰め込んだModule

以下のコードは、ファイル操作と例外処理が密結合しており、テストが不可能である。

‘ 【絶対に真似してはいけないアンチパターン】
Module BadFileUtility
Public Sub ProcessCsv(ByVal filePath As String)
Try
‘ 直接ファイルを読み込んでいるため、テスト時にダミーデータを流し込めない
Dim lines() As String = System.IO.File.ReadAllLines(filePath)
For Each line As String In lines
‘ 処理…
:
Console.WriteLine(“処理成功: ” & filePath)
Catch ex As Exception
‘ ログ出力がハードコーディングされている
Console.WriteLine(“エラー: ” & ex.Message)
End Try
End Sub
End Module

—

プロダクションコード:依存性注入(DI)を活用した堅牢な設計

保守性が高く、単体テストが容易なコードに書き換える。
ポイントは以下の通りだ。
1. インターフェース(Interface)の導入:依存関係を抽象化する。
2. インスタンスの責務分離:ファイル操作を担うクラスと、ログ出力を担うクラスを分離する。
3. コンストラクタインジェクション:外部から依存オブジェクトを注入する。

Imports System.IO

‘ 1. ログ出力の抽象化(インターフェース)
Public Interface ILogger
Sub LogInfo(ByVal message As String)
Sub LogError(ByVal message As String, ByVal ex As Exception)
End Interface

‘ 2. 具体的なログ出力クラス
Public Class ConsoleLogger
Implements ILogger

Public Sub LogInfo(ByVal message As String) Implements ILogger.LogInfo
Console.WriteLine($”[INFO] {DateTime.Now:yyyy-MM-dd HH:mm:ss} – {message}”)
End Sub

Public Sub LogError(ByVal message As String, ByVal ex As Exception) Implements ILogger.LogError
Console.WriteLine($”[ERROR] {DateTime.Now:yyyy-MM-dd HH:mm:ss} – {message} : {ex.Message}”)
End Sub
End Class

‘ 3. ファイル処理クラス(ILoggerに依存し、具象クラスには依存しない)
Public Class CsvProcessor
Private ReadOnly _logger As ILogger

‘ コンストラクタインジェクションにより依存性を注入
Public Sub New(ByVal logger As ILogger)
If logger Is Nothing Then Throw New ArgumentNullException(NameOf(logger))
_logger = logger
End Sub

”’

”’ CSVファイルを安全に処理する
”’

Public Sub ProcessCsv(ByVal filePath As String)
‘ ガード節による早期リターン
If Not File.Exists(filePath) Then
_logger.LogError($”ファイルが見つかりません: {filePath}”, New FileNotFoundException())
Return
End If

Try
Dim lines() As String = File.ReadAllLines(filePath)

For Each line As String In lines
‘ 実際のビジネスロジック(行解析など)
ParseLine(line)
Next

_logger.LogInfo($”CSV処理が正常終了しました: {filePath}”)

Catch ex As Exception
_logger.LogError($”CSV処理中に予期せぬエラーが発生しました: {filePath}”, ex)
‘ 必要に応じて上位へ例外を再スロー、または業務例外としてラップする
Throw
End Try
End Sub

Private Sub ParseLine(ByVal line As String)
‘ 行ごとのパース処理をカプセル化
Dim values() As String = line.Split(“,”c)
‘ TODO: データベース登録処理など
End Sub
End Class

—

4. なぜこの設計が「圧倒的に強い」のか?

上記のモダンなクラス設計に移行することで、現場には以下のような強力なメリットが生まれる。

1. 単体テストが容易になる
テスト時には、`ILogger` のモック(偽物)を渡すことで、実際にコンソールやファイルに出力させずに、ログが正しく呼ばれたかテストできる。また、ファイル読み込み部分についても、ストリームや抽象化層を挟むことでモック化が可能になる。
2. 変更に強い(オープン・閉鎖の原則:OCP)
将来的に「コンソール出力ではなく、ログを専用のDBテーブルに保存したい」となった場合でも、`ILogger` を実装した `DbLogger` クラスを新規作成し、それを注入するだけでよく、`CsvProcessor` のコードを1行も書き換える必要がない。
3. ライフサイクルが明確
グローバル変数や `Shared` の海で溺れることがなくなり、オブジェクトがいつ生成され、いつ破棄されるかがGC(ガベージコレクション)の管理下に正しく置かれる。

—

5. チーフアーキテクトからの提言

VB.NETは、その歴史的背景から「動けばいい」という即席のコードが書けてしまう優しさを持った言語だ。しかし、その優しさに甘えていると、システムが成長した瞬間に開発速度は急激に低下し、バグ修正に怯える日々がやってくる。

`Module` は、文字通り「名前空間の中の小さな道具箱」程度にとどめよ。
ビジネスロジックやデータ連携、ファイル・DB操作を伴う処理は、必ず Class(クラス)と Interface(インターフェース) の世界で構築すること。

オブジェクト指向の原則に則った堅牢な設計こそが、日々のノンプログラマブルな手戻りを無くし、真の業務効率化(自動化)を支える唯一の盾となる。コードを書く手を一度止め、「これは本当に `Module` であるべきか?」と自問してみてほしい。

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