【実務・中級編】定数管理の外部化:設定ファイル(INI/JSON)から定数を読み込む手法 – Excel VBA解析バイブル

スポンサーリンク

1. コード内に潜む「静かな暗殺者」——なぜハードコードは悪なのか

開発現場で未だに散見される悲劇があります。

「接続先フォルダのパスが変わったので、VBAのコードを修正してください」
「テスト環境から本番環境に切り替えるため、コード内のURLを書き換えてコンパイルし直します」

このような運用を行っているプロジェクトは、すでに技術的負債の泥沼に足を踏み入れています。

コード内に設定値(ファイルパス、データベース接続文字列、閾値、APIキーなど)を直接記述するハードコード(Hard Coding)は、単に「美しくない」だけではありません。運用の現場において致命的なバグとコストを引き起こす静かな暗殺者です。

ハードコードがもたらす4つの大罪

1. VBE(Visual Basic Editor)を開かせるリスク
非エンジニアの運用担当者にVBEを開かせ、コードを書き換えさせる運用は論外です。タイポ一つで構文エラー(Syntax Error)が発生し、ツール全体が沈黙します。最悪の場合、関係のないロジックを誤って消去されるリスクすらあります。
2. 環境差分の吸収不能
開発・検証・本番環境の切り替え時に、コードそのものを書き換える設計は、人間の手作業によるヒューマンエラーを絶対に排除できません。「検証用のAPIエンドポイントのまま本番稼働してしまった」という事故の原因は、常にこれです。
3. バージョン管理の崩壊
定数を変更するためだけにブック(.xlsm)を保存し直すと、リビジョン履歴が「単なる設定変更」で埋め尽くされます。バイナリファイルであるExcelブックはGit等での差分検出が難しいため、ロジック変更なのか設定変更なのかが不可視になります。
4. コンパイル定数と動的設定の混同
`Const MAX_ROW As Long = 1000` のように「ドメインルールとして不変のロジック定数」と、「運用によって変わり得る設定値」を同一視している点に、設計の幼さが出ます。

プロフェッショナルが構築すべきは、「コードを1行も触らずに、設定ファイル(INI/JSON)を差し替えるだけで振る舞いを完全に制御できる堅牢なツール」です。

2. アーキテクチャ設計:設定値管理のライフサイクルと評価基準

外部ファイルから定数を読み込む設計を行う際、まずはどのファイルフォーマットを採用すべきか、そして取得した設定値をメモリ上でどう保持すべきかを定義しなければなりません。

設定ファイルフォーマットの選定基準

実務において選択肢となるのは主に INIJSON です。それぞれの特性を正しく理解し、プロジェクトの規模と要求に応じて選定してください。

| 評価軸 | INIファイル | JSONファイル |
| :— | :— | :— |
| 構造 | 平坦(セクション+キー=値) | 階層構造・配列を表現可能 |
| 視認性・編集性 | 非常に高い(メモ帳で誰でも書ける) | 高い(カッコの閉じ忘れに注意が必要) |
| 適用シーン | 単純なフラグ、パス管理、環境設定 | 複雑なマッピング、リストデータ、API連携 |
| VBA実装コスト | Windows API一発で読み込み可能(極めて低コスト) | パーサー(解析器)の準備が必要 |

  • INIを採用すべきケース: 保守担当者がITリテラシーの低い現場、設定項目が数個〜数十個程度のフラットな構造である場合。
  • JSONを採用すべきケース: 設定値自体がリスト(配列)を持っている場合や、Web API連携ツールなど、他のシステムと設定形式を統一したい場合。

メモリ空間への展開とライフサイクル(パフォーマンスの観点)

設定ファイルを読み込む際、絶対にやってはならない実装があります。それは、「設定値が必要になるたびに、何度も外部ファイルにI/O(ファイルアクセス)を発生させる」ことです。

ディスクI/Oは、メモリ上の処理と比較して数千倍〜数万倍低速です。ループ処理の中で外部ファイルを都度読み込むようなコードは、ツール全体のパフォーマンスを著しく低下させます。

[アンチパターン]
関数が呼ばれる ──> 毎回ファイルを開く ──> 設定値を取得 ──> ファイルを閉じる(超非効率)

[プロフェッショナルな設計]
アプリケーション起動(または初回アクセス)
──> 外部ファイルを1回だけ読み込む
──> メモリ上のキャッシュ(Dictionary / Singleton Class)に保持
──> 2回目以降はメモリから超高速に取得

設定値は、「初回アクセス時に一括ロード(Lazy Loading)し、アプリケーションの生存期間中メモリにキャッシュする」のが鉄則です。

3. 実装手法①:Windows APIを用いた「INIファイル」読み込み(超堅牢版)

まずは、軽量でWindows環境との親和性が抜群に高いINIファイルによる管理手法を解説します。

VBAからINIファイルを操作する場合、ファイル入出力(`Open` ステートメント)で文字列解析を自作するのは筋が悪いです。Windows OSが標準提供している Win32 API `GetPrivateProfileString` を使用します。これにより、バッファオーバーフローを防ぎ、高速かつ堅牢に値を読み込むことが可能です。

64bit/32bit 完全互換のINI管理クラス (`CConfigINI`)

以下のコードをクラスモジュール名 `CConfigINI` として作成してください。

Option Explicit

‘ ==============================================================================
‘ クラス名: CConfigINI
‘ 概要 : Win32 APIを使用したINIファイル読み込みクラス(64bit/32bit両対応)
‘ 責任 : INIファイルからの設定値取得、メモリキャッシュ、デフォルト値フォールバック
‘ ==============================================================================

If VBA7 Then
‘ 64bit Excel環境向けAPI宣言
Private Declare PtrSafe Function GetPrivateProfileString Lib “kernel32” _
Alias “GetPrivateProfileStringA” ( _
ByVal lpAppName As String, _
ByVal lpKeyName As String, _
ByVal lpDefault As String, _
ByVal lpReturnedString As String, _
ByVal nSize As Long, _
ByVal lpFileName As String) As Long
Else
‘ 32bit Excel環境向けAPI宣言
Private Declare Function GetPrivateProfileString Lib “kernel32” _
Alias “GetPrivateProfileStringA” ( _
ByVal lpAppName As String, _
ByVal lpKeyName As String, _
ByVal lpDefault As String, _
ByVal lpReturnedString As String, _
ByVal nSize As Long, _
ByVal lpFileName As String) As Long
End If

‘ プライベート変数
Private m_FilePath As String
Private m_Cache As Object ‘ Scripting.Dictionary にて「セクション|キー」をキャッシュ

Private Sub Class_Initialize()
Set m_Cache = CreateObject(“Scripting.Dictionary”)
m_Cache.CompareMode = vbTextCompare ‘ キーの大小文字を区別しない
End Sub

Private Sub Class_Terminate()
Set m_Cache = Nothing
End Sub

‘ ——————————————————————————
‘ 初期化メソッド: INIファイルのパスを設定
‘ ——————————————————————————
Public Sub Initialize(ByVal strFilePath As String)
Dim fso As Object
Set fso = CreateObject(“Scripting.FileSystemObject”)

If Not fso.FileExists(strFilePath) Then
Err.Raise Number:=vbObjectError + 1001, _
Source:=”CConfigINI.Initialize”, _
Description:=”指定された設定ファイルが存在しません: ” & strFilePath
End If

m_FilePath = strFilePath
m_Cache.RemoveAll ‘ パス変更時はキャッシュクリア
End Sub

‘ ——————————————————————————
‘ 設定値取得メソッド(文字列)
‘ ——————————————————————————
Public Function GetValue(ByVal strSection As String, ByVal strKey As String, Optional ByVal strDefault As String = “”) As String
If Len(m_FilePath) = 0 Then
Err.Raise Number:=vbObjectError + 1002, _
Source:=”CConfigINI.GetValue”, _
Description:=”クラスが初期化されていません。Initializeメソッドを呼んでください。”
End If

Dim cacheKey As String
cacheKey = strSection & “|” & strKey

‘ メモリキャッシュが存在すればAPIを叩かずに即却返す
If m_Cache.Exists(cacheKey) Then
GetValue = m_Cache(cacheKey)
Exit Function
End If

‘ Win32 APIバッファの準備(256バイト)
Dim buffer As String
Dim readLen As Long
buffer = String$(256, vbNullChar)

‘ API呼び出し
readLen = GetPrivateProfileString(strSection, strKey, strDefault, buffer, Len(buffer), m_FilePath)

‘ Null文字を除去して取得
Dim resultValue As String
If readLen > 0 Then
resultValue = Left$(buffer, readLen)
Else
resultValue = strDefault
End If

‘ キャッシュに保存
m_Cache(cacheKey) = resultValue
GetValue = resultValue
End Function

‘ ——————————————————————————
‘ 設定値取得メソッド(数値への安全な変換)
‘ ——————————————————————————
Public Function GetLong(ByVal strSection As String, ByVal strKey As String, Optional ByVal lngDefault As Long = 0) As Long
Dim strVal As String
strVal = GetValue(strSection, strKey, CStr(lngDefault))

If IsNumeric(strVal) Then
GetLong = CLng(strVal)
Else
‘ 開発者に気づかせるためのログ判定、またはデフォルト値を返す
GetLong = lngDefault
End If
End Function

‘ ——————————————————————————
‘ 設定値取得メソッド(Booleanへの安全な変換)
‘ ——————————————————————————
Public Function GetBool(ByVal strSection As String, ByVal strKey As String, Optional ByVal boolDefault As Boolean = False) As Boolean
Dim strVal As String
strVal = LCase$(Trim$(GetValue(strSection, strKey, CStr(boolDefault))))

Select Case strVal
Case “true”, “1”, “yes”, “on”
GetBool = True
Case “false”, “0”, “no”, “off”
GetBool = False
Case Else
GetBool = boolDefault
End Select
End Function

4. 実装手法②:依存なし・64bit完全対応の「JSON設定ファイル」パーサー

現代の標準フォーマットである JSON を採用する場合、VBA界隈では大きな罠が存在します。

かつて頻繁に使われていた `MSScriptControl.ScriptControl` (JScript) や `htmlfile` を利用したパース手法は、64bit版のExcelでは動作しない(または極めて不安定になる)という致命的な欠点があります。

プロダクション環境で動かすツールは、外部DLL依存やビット数依存を完全に排除すべきです。ここでは、サードパーティライブラリに一切頼らず、標準のVBA機能(`Scripting.Dictionary` と文字列パース)のみで動作する軽量JSON解析エンジンを実装します。

サンプル設定ファイル (`config.json`)

ツールと同階層に以下の `config.json` を配置することを想定します。

{
“Environment”: “Production”,
“TimeoutSec”: 30,
“IsDebugMode”: false,
“Paths”: {
“ImportDir”: “C:\\Data\\Import”,
“ExportDir”: “C:\\Data\\Export”
}
}

JSON解析クラス (`CConfigJSON`)

以下のコードをクラスモジュール名 `CConfigJSON` として作成してください。ネストされたJSON構造をドット記法(例: `Paths.ImportDir`)で透過的に取得できる設計にしています。

Option Explicit

‘ ==============================================================================
‘ クラス名: CConfigJSON
‘ 概要 : 外部依存ゼロ・64bit完全対応の軽量JSON設定管理クラス
‘ 責任 : JSONファイルのロード、再帰パースによるDictionary化、ドット記法参照
‘ ==============================================================================

Private m_ConfigData As Object ‘ Scripting.Dictionary

Private Sub Class_Initialize()
Set m_ConfigData = CreateObject(“Scripting.Dictionary”)
m_ConfigData.CompareMode = vbTextCompare
End Sub

Private Sub Class_Terminate()
Set m_ConfigData = Nothing
End Sub

‘ ——————————————————————————
‘ JSONファイルのロードと初期化
‘ ——————————————————————————
Public Sub LoadFile(ByVal strFilePath As String)
Dim fso As Object
Dim stream As Object
Dim jsonText As String

Set fso = CreateObject(“Scripting.FileSystemObject”)
If Not fso.FileExists(strFilePath) Then
Err.Raise Number:=vbObjectError + 2001, _
Source:=”CConfigJSON.LoadFile”, _
Description:=”JSONファイルが見つかりません: ” & strFilePath
End If

‘ UTF-8での読み込みを担保するために ADODB.Stream を使用
Set stream = CreateObject(“ADODB.Stream”)
With stream
.Type = 2 ‘ adTypeText
.Charset = “UTF-8”
.Open
.LoadFromFile strFilePath
jsonText = .ReadText
.Close
End With

‘ 解析実行
Set m_ConfigData = CreateObject(“Scripting.Dictionary”)
m_ConfigData.CompareMode = vbTextCompare

Dim index As Long
index = 1
SkipWhitespace jsonText, index

If Mid$(jsonText, index, 1) = “{” Then
Set m_ConfigData = ParseObject(jsonText, index)
Else
Err.Raise Number:=vbObjectError + 2002, Description:=”不正なJSON形式です。ルートはオブジェクトでなければなりません。”
End If
End Sub

‘ ——————————————————————————
‘ ドット記法による値取得 (例: “Paths.ImportDir”)
‘ ——————————————————————————
Public Function GetValue(ByVal strKeyPath As String, Optional ByVal defaultValue As Variant = Empty) As Variant
Dim keys() As String
keys = Split(strKeyPath, “.”)

Dim currentObj As Object
Set currentObj = m_ConfigData

Dim i As Long
For i = 0 To UBound(keys) – 1
If currentObj.Exists(keys(i)) Then
If IsObject(currentObj(keys(i))) Then
Set currentObj = currentObj(keys(i))
Else
GetValue = defaultValue
Exit Function
End If
Else
GetValue = defaultValue
Exit Function
End If
Next i

Dim lastKey As String
lastKey = keys(UBound(keys))

If currentObj.Exists(lastKey) Then
If IsObject(currentObj(lastKey)) Then
Set GetValue = currentObj(lastKey)
Else
GetValue = currentObj(lastKey)
End If
Else
GetValue = defaultValue
End If
End Function

‘ ==============================================================================
‘ プライベート・再帰パーサーメソッド群(内部処理)
‘ ==============================================================================

Private Function ParseObject(ByRef json As String, ByRef index As Long) As Object
Dim dict As Object
Set dict = CreateObject(“Scripting.Dictionary”)
dict.CompareMode = vbTextCompare

index = index + 1 ‘ ‘{‘ をスキップ

Do While index <= Len(json) SkipWhitespace json, index Dim char As String char = Mid$(json, index, 1) If char = "}" Then index = index + 1 Set ParseObject = dict Exit Function ElseIf char = "," Then index = index + 1 Else ' キーのパース Dim keyName As String keyName = ParseString(json, index) SkipWhitespace json, index If Mid$(json, index, 1) <> “:” Then
Err.Raise vbObjectError + 2003, Description:=”JSONパースエラー: ‘:’ が見つかりません。”
End If
index = index + 1 ‘ ‘:’ をスキップ

‘ 値のパース
Dim val As Variant
val = ParseValue(json, index)

If IsObject(val) Then
Set dict(keyName) = val
Else
dict(keyName) = val
End If
End If
Loop
Set ParseObject = dict
End Function

Private Function ParseValue(ByRef json As String, ByRef index As Long) As Variant
SkipWhitespace json, index
Dim char As String
char = Mid$(json, index, 1)

If char = “{” Then
Set ParseValue = ParseObject(json, index)
ElseIf char = “””” Then
ParseValue = ParseString(json, index)
ElseIf char = “t” Or char = “f” Then
ParseValue = ParseBoolean(json, index)
ElseIf char = “n” Then
ParseValue = ParseNull(json, index)
ElseIf IsNumeric(char) Or char = “-” Then
ParseValue = ParseNumber(json, index)
Else
Err.Raise vbObjectError + 2004, Description:=”JSONパースエラー: 不正な文字 ‘” & char & “‘”
End If
End Function

Private Function ParseString(ByRef json As String, ByRef index As Long) As String
SkipWhitespace json, index
index = index + 1 ‘ 開始ダブルクォーテーションをスキップ

Dim startIdx As Long
startIdx = index

Do While index <= Len(json) Dim char As String char = Mid$(json, index, 1) If char = """" Then ParseString = Mid$(json, startIdx, index - startIdx) ' エスケープ文字の簡易処理 (\\ と \") ParseString = Replace(ParseString, "\""", """") ParseString = Replace(ParseString, "\\", "\") index = index + 1 Exit Function End If index = index + 1 Loop End Function Private Function ParseNumber(ByRef json As String, ByRef index As Long) As Variant Dim startIdx As Long: startIdx = index Do While index <= Len(json) Dim char As String char = Mid$(json, index, 1) If Not (IsNumeric(char) Or char = "." Or char = "-" Or char = "e" Or char = "E") Then Exit Do End If index = index + 1 Loop Dim numStr As String numStr = Mid$(json, startIdx, index - startIdx) If InStr(numStr, ".") > 0 Then
ParseNumber = CDbl(numStr)
Else
ParseNumber = CLng(numStr)
End If
End Function

Private Function ParseBoolean(ByRef json As String, ByRef index As Long) As Boolean
If Mid$(json, index, 4) = “true” Then
ParseBoolean = True
index = index + 4
ElseIf Mid$(json, index, 5) = “false” Then
ParseBoolean = False
index = index + 5
End If
End Function

Private Function ParseNull(ByRef json As String, ByRef index As Long) As Variant
If Mid$(json, index, 4) = “null” Then
ParseNull = Null
index = index + 4
End If
End Function

Private Sub SkipWhitespace(ByRef json As String, ByRef index As Long)
Do While index <= Len(json) Select Case Mid$(json, index, 1) Case " ", vbTab, vbCr, vbLf index = index + 1 Case Else Exit Sub End Select Loop End Sub ---

5. 実務投入用コード:シングルトン風の設計で呼び出しを極限までシンプルにする

上記で作成したクラス群を、業務コードから呼び出す際の手法を解説します。

毎回クラスを `New` してファイルをロードするコードを書いていては意味がありません。標準モジュール内にアプリケーション全体で唯一の設定インスタンスを保持させる(シングルトン・パターン風の)ラッパーモジュールを用意するのが最も洗練された設計です。

標準モジュール `mod_AppConfig` の作成

Option Explicit

‘ ==============================================================================
‘ モジュール名: mod_AppConfig
‘ 概要 : アプリケーション全体の設定保持インターフェース
‘ 責任 : 設定インスタンスの生存期間管理(Lazy Initialization)
‘ ==============================================================================

Private m_Config As CConfigJSON

‘ ——————————————————————————
‘ 設定管理オブジェクトのグローバルアクセスポイント
‘ ——————————————————————————
Public Function AppConfig() As CConfigJSON
If m_Config Is Nothing Then
Set m_Config = New CConfigJSON

‘ ツールと同じディレクトリにある config.json を自動探知
Dim configPath As String
configPath = ThisWorkbook.Path & “\config.json”

‘ 堅牢なエラーハンドリング付きでロード
On Error GoTo ErrorHandler
m_Config.LoadFile configPath
On Error GoTo 0
End If

Set AppConfig = m_Config
Exit Function

ErrorHandler:
MsgBox “設定ファイルの読み込みに失敗しました。” & vbCrLf & _
“ファイルパス: ” & configPath & vbCrLf & _
“詳細: ” & Err.Description, vbCritical, “システムエラー”
End ‘ 処理を安全に完全停止
End Function

‘ ——————————————————————————
‘ メモリ解放用(ツールの終了処理時などに呼び出し)
‘ ——————————————————————————
Public Sub TerminateConfig()
Set m_Config = Nothing
End Sub

実際の業務ロジックからの呼び出し例

設定管理の仕組みを導入した結果、業務ロジック側のコードがいかに美しく、かつ安全になるかを確認してください。

Option Explicit

‘ ==============================================================================
‘ メイン業務処理モジュール
‘ ==============================================================================
Public Sub RunDataProcessing()
On Error GoTo ErrorHandler

‘ 1. 設定値の読み込み(ドット記法で超直感的に参照可能)
‘ 初回呼び出し時に自動的に JSON がロードされ、メモリにキャッシュされる
Dim importPath As String
importPath = AppConfig.GetValue(“Paths.ImportDir”, “C:\DefaultImport”)

Dim timeoutVal As Long
timeoutVal = AppConfig.GetValue(“TimeoutSec”, 60)

Dim isDebug As Boolean
isDebug = AppConfig.GetValue(“IsDebugMode”, False)

‘ デバッグログ出力(設定に応じて動的に挙動を変更)
If isDebug Then
Debug.Print “— DEBUG MODE ACTIVE —”
Debug.Print “Import Path : ” & importPath
Debug.Print “Timeout Sec : ” & timeoutVal
End If

‘ 2. メインのファイル処理ロジックへ
ProcessImportFiles importPath, timeoutVal

MsgBox “処理が正常に完了しました。”, vbInformation, “完了”
Exit Sub

ErrorHandler:
MsgBox “業務処理中にエラーが発生しました: ” & Err.Description, vbCritical, “エラー”
End Sub

Private Sub ProcessImportFiles(ByVal path As String, ByVal timeout As Long)
‘ 実際のファイル取り込みロジックをここに記述
‘ コード内にハードコードされた文字列や数値は存在しない!
End Sub

6. 結論:変更に強いVBAツールを構築するための「設計チェックリスト」

Excel VBA開発を「単なるマクロ作成」から「堅牢なソフトウェア開発」へ昇華させるために、本記事で解説した設計原則を意識してください。

プロフェッショナル開発者のための要件チェックリスト

  • [ ] コード内に特定のPC環境に依存するフォルダパスや接続文字列が直接書かれていないか?
  • [ ] 運用者が設定変更を行う際、VBE(コードエディタ)を開く必要がないか?
  • [ ] 設定ファイルの読み込みは、大量ループの外側(またはLazy Loadingによる初回のみ)でキャッシュされているか?
  • [ ] 設定ファイルが誤って削除・破損していた場合、適切なエラーメッセージを表示して安全に停止できるか?
  • [ ] 64bit化されたExcel環境で、外部COMオブジェクト(ScriptControl等)の非互換性で落ちるリスクが排除されているか?

「動けば良い」コードは、ツールが完成した瞬間から劣化が始まります。
一方、定数を正しく外部化し、カプセル化された堅牢なアーキテクチャを持つコードは、時代の変化や運用変更に即座に対応できる「真の資産」となります。

次にVBAツールを構築する際は、まず1個の `config.json`(または `.ini`)を作成することから始めてください。その一歩が、あなたの開発者としての格を確実に引き上げます。

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