リファクタリングの深淵:スパゲッティVBScriptを「Class構文」で解体・再構築する極限の設計論
現場のシニアエンジニア諸君、あるいは、数千行に及ぶ「動くが、誰も触れない」VBScriptの化石に立ち向かっている管理者諸君。
我々が直面しているのは、単なるコードの乱れではない。それは、「状態(State)」と「振る舞い(Behavior)」が未分化のまま、グローバルスコープという泥沼に沈殿した、技術的負債の極致である。
手続き型で書かれた巨大な `.vbs` ファイル。至る所に散らばる `Dim` 宣言。副作用を撒き散らすグローバル変数。条件分岐の迷宮。これらを放置することは、将来のシステム崩壊を予約することと同義だ。
本稿では、単なる「書き換え」ではない。VBScriptにおけるオブジェクト指向への転換、すなわち Class構文を用いたアーキテクチャの再構築 について、メモリ管理からWindows APIとの境界線に至るまで、その真髄を解き明かす。
1. 手続き型コードが陥る「決定論の喪失」
なぜ、手続き型の巨大なスクリプトは壊れるのか? 理由は単純だ。「副作用の予測不能性」にある。
手続き型コードでは、ある関数(Sub/Function)が、意図せずグローバル変数を書き換え、それが全く無関係な別の処理の挙動を狂わせる。この「隠れた依存関係」が、コードの規模に比例して指数関数的に増大していく。
破綻の兆候
- 変数のスコープが広すぎる: `Dim` がスクリプトの冒頭に並び、どこで値が変わったか追跡できない。
- 単一責任原則の欠如: 一つの `Sub` が、ファイル操作、ログ出力、DB接続、計算までをすべて担っている。
- エラーハンドリングの不在: `On Error Resume Next` が乱用され、異常が発生しても「何が起きたか」を検知できないまま処理が進む。
これらを打破する唯一の手段が、カプセル化(Encapsulation)である。
2. Class構文による「境界線」の確立
VBScriptにおける `Class` は、単なるコードの整理術ではない。それは、「データ」と「それを操作するロジック」を一つの閉じた世界に閉じ込めるための防壁である。
リファクタリングの第一歩は、既存の処理を「何をするものか」という概念(ドメイン)に基づいてクラスへと切り出すことだ。
設計の黄金律
1. プロパティは最小限に: 外部から直接書き換えられる変数は極力減らし、`Property Get` / `Property Let` を通じて制御せよ。
2. メソッドは自己完結させる: クラスのメソッドは、外部の状態に依存せず、クラス内部のプロパティのみを用いて完結させるべきである。
3. コンストラクタの役割を明確にする: `Class_Initialize` で、オブジェクトが「正しい状態」から始まることを保証せよ。
3. メモリ最適化とオブジェクトのライフサイクル
VBScript(WSH)環境において、エンジニアが最も軽視しがちなのが、COMオブジェクトのライフサイクル管理である。
VBScriptは参照カウント方式でメモリを管理している。巨大なループ内で `FileSystemObject` や `WScript.Shell` などのCOMオブジェクトを生成し、適切に解放しなければ、メモリリークを引き起こし、長期間稼働するバッチ処理においてシステム全体の不安定化を招く。
鉄則:明示的解放
オブジェクトへの参照が不要になった瞬間、直ちに `Nothing` を代入せよ。
' 悪い例:スコープが切れるまでメモリに残り続ける
Set objFSO = CreateObject("Scripting.FileSystemObject")
' ... 処理 ...
' 良い例:使用直後に解放する
Set objFSO = CreateObject("Scripting.FileSystemObject")
' ... 処理 ...
Set objFSO = Nothing ' 参照カウントを確実に減らす
クラス設計においては、`Class_Terminate` を活用し、クラスが破棄される際に内部で保持しているCOMオブジェクトも確実に解放する設計を徹底すること。
4. 実践:リファクタリング・パターン
ここでは、バラバラに書かれた「ファイル操作」と「ログ記録」のコードを、どのようにクラスへ統合すべきか、具体的なコード例を示す。
【Before】スパゲッティ状態(手続き型)
' どこで何が変わるか分からない、典型的なレガシーコード
Dim logFile, targetDir, errorCount
logFile = "C:\logs\app.log"
targetDir = "C:\data"
errorCount = 0
On Error Resume Next
Set fso = CreateObject("Scripting.FileSystemObject")
Set ts = fso.OpenTextFile(logFile, 8, True)
' ファイル操作ロジックが混在
Dim files, f
Set files = fso.GetFolder(targetDir).Files
For Each f In files
If f.Name = "config.tmp" Then
f.Delete
If Err.Number <> 0 Then
ts.WriteLine Now & " [ERROR] Delete failed: " & Err.Description
errorCount = errorCount + 1
Err.Clear
End If
End If
Next
ts.Close
Set ts = Nothing
Set fso = Nothing
WScript.Echo "Errors: " & errorCount
【After】アーキテクチャ再構築(Class構文)
クラスを分割し、責任を明確化する。
' ============================================================
' Logger Class: ログ出力の責任のみを持つ
' ============================================================
Class Logger
Private logPath
Private ts
Private Sub Class_Initialize()
' 初期化ロジック
End Sub
Public Sub Open(path)
On Error Resume Next
Set ts = CreateObject("Scripting.FileSystemObject").OpenTextFile(path, 8, True)
If Err.Number <> 0 Then
WScript.Echo "Critical Error: Cannot open log file."
WScript.Quit
End If
On Error GoTo 0
End Sub
Public Sub Write(message)
If Not ts Is Nothing Then
ts.WriteLine Now & " [" & message & "]"
End If
End Sub
Public Sub Close()
If Not ts Is Nothing Then
ts.Close
Set ts = Nothing
End If
End Sub
Private Sub Class_Terminate()
Close
End Sub
End Class
' ============================================================
' FileProcessor Class: ファイル操作のビジネスロジックを担う
' ============================================================
Class FileProcessor
Private fso
Private logger
Private Sub Class_Initialize()
Set fso = CreateObject("Scripting.FileSystemObject")
End Sub
' 依存性の注入 (Dependency Injection)
' 外部からLoggerを渡すことで、テスト容易性と結合度を制御する
Public Sub SetLogger(logObj)
Set logger = logObj
End Sub
Public Function CleanTempFiles(targetDir)
Dim errorCount, files, f
errorCount = 0
If Not fso.FolderExists(targetDir) Then
If Not logger Is Nothing Then logger.Write "DIR NOT FOUND: " & targetDir
CleanTempFiles = 0
Exit Function
End If
Set files = fso.GetFolder(targetDir).Files
For Each f In files
If f.Name = "config.tmp" Then
On Error Resume Next
f.Delete
If Err.Number <> 0 Then
If Not logger Is Nothing Then logger.Write "DELETE FAILED: " & f.Name & " - " & Err.Description
errorCount = errorCount + 1
Err.Clear
End If
On Error GoTo 0
End If
Next
CleanTempFiles = errorCount
End Function
Private Sub Class_Terminate()
Set fso = Nothing
Set logger = Nothing
End Sub
End Class
' ============================================================
' Main Execution Logic
' ============================================================
Sub Main()
Dim myLogger, myProcessor
Dim errs
' オブジェクトの生成
Set myLogger = New Logger
Set myProcessor = New FileProcessor
' 依存関係の設定
myLogger.Open "C:\logs\app.log"
myProcessor.SetLogger myLogger
' 実行
errs = myProcessor.CleanTempFiles("C:\data")
WScript.Echo "Process Completed. Total Errors: " & errs
' 明示的な破棄(Class_Terminateが走り、内部のCOMも解放される)
Set myProcessor = Nothing
Set myLogger = Nothing
End Sub
Main
5. 極限の知見:Windows APIとの境界線
VBScript単体では、レジストリの高度な操作や、プロセス制御、低レイヤーのメモリ操作には限界がある。伝説的なアーキテクトは、ここで無理にVBScriptだけで解決しようとはしない。
もし、業務要件がVBScriptの限界を超えているならば、以下の戦略を取るべきだ。
1. COMコンポーネントの活用: 複雑なロジックやWindows APIを多用する部分は、C#やC++でCOMサーバー(DLL)として実装し、VBScriptからは `CreateObject` で呼び出す。
2. WScript.Shellによるブリッジ: `WshShell.Run` を使い、PowerShellや高度なコマンドラインツールを「カプセル化されたメソッド」として実行する。
VBScriptはあくまで「オーケストレーター(指揮者)」であるべきだ。重い楽器(低レイヤー操作)をすべてVBScriptに演奏させるのではなく、適切な専門家に指示を出す。これが、レガシー環境における真のアーキテクチャ設計である。
結びに代えて
リファクタリングは、単なる「コードの掃除」ではない。それは、混沌としたシステムに「秩序」をもたらす聖なる儀式である。
Class構文を使いこなし、オブジェクトのライフサイクルを支配し、責任の所在を明確にする。この原則を貫くことで、君たちが遺すコードは、次世代のエンジニアにとっての「資産」となるだろう。
幸運を。
