ねらい:VBAプロジェクトを「迷子にならないモジュール構成」にする
モジュール構成テンプレのゴールは、こうです。
「新しい処理を追加するときに、どのモジュールに書けばいいか迷わない」
「半年後に開いても、“ああ、このモジュールはこの役割ね”とすぐ分かる」。
そのために、役割ごとにモジュールを分け、名前と中身のルールを決めておきます。
今日は、その“標準構成”を、初心者でも真似しやすい形でテンプレ化していきます。
全体像:標準モジュール構成のパターン
基本のモジュール分類
まず、大枠としてこう分けます。
業務の入口をまとめるモジュール。
業務フロー(バッチ)を制御するモジュール。
ノーコード系エンジン(JOIN・集計・差分・変換など)のモジュール。
共通基盤(ログ・時間計測・設定ユーティリティなど)のモジュール。
必要に応じてクラスモジュール(StopWatch など)。
これを、具体的な名前に落とすと、こんな構成になります。
ModEntry
ModBatchEngine
ModJoinEngine
ModAggEngine
ModDiffEngine
ModConvEngine
ModLogger
ModStopWatch
ModConfigUtil
ここに、業務ごとの「小さな業務マクロ」を追加していくイメージです。
エントリーモジュール:ユーザーが触る入口を集約する
ModEntry の役割
ModEntry は、「ユーザーが直接実行するマクロだけを置く場所」です。
ボタンに割り当てるのも、ショートカットキーに割り当てるのも、ここにある Sub だけにします。
例えば、日次処理の入口はこう書きます。
' ModEntry.bas
Option Explicit
Public Sub RunDailyJobs()
Const MODULE_NAME As String = "RunDailyJobs"
On Error GoTo ErrHandler
LogStart MODULE_NAME, "日次バッチ開始"
RunBatch
LogEnd MODULE_NAME, "日次バッチ終了"
Exit Sub
ErrHandler:
LogError MODULE_NAME, "MAIN", Err
MsgBox "日次処理中にエラーが発生しました。ログを確認してください。", vbExclamation
End Sub
VBここで重要なのは、業務ロジックを一切書かないことです。
やるのは「ログ開始」「バッチ呼び出し」「ログ終了」「エラー捕捉」だけ。
“入口専用モジュール”にしておくと、ユーザーが触る場所と内部ロジックがきれいに分かれます。
バッチモジュール:業務フローを一箇所に集める
ModBatchEngine の役割
ModBatchEngine は、「どの業務マクロを、どの順番で、どう実行するか」を管理する司令塔です。
ConfigBatch シートを読み込み、そこに書かれた MacroName を順番に実行します。
ジョブ1件分を表す構造体はこう定義します。
' ModBatchEngine.bas
Option Explicit
Private Type BatchJob
Enabled As Boolean
JobName As String
MacroName As String
StopOnError As Boolean
Comment As String
End Type
VBConfigBatch を読み込む関数はこうです。
Private Function LoadBatchJobs() As Variant
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("ConfigBatch")
Dim lastRow As Long
lastRow = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row
If lastRow < 2 Then
LoadBatchJobs = Empty
Exit Function
End If
Dim data As Variant
data = ws.Range(ws.Cells(2, 1), ws.Cells(lastRow, 5)).Value
Dim jobs() As BatchJob
ReDim jobs(1 To UBound(data, 1))
Dim i As Long
For i = 1 To UBound(data, 1)
jobs(i).Enabled = (UCase$(CStr(data(i, 1))) = "Y")
jobs(i).JobName = CStr(data(i, 2))
jobs(i).MacroName = CStr(data(i, 3))
jobs(i).StopOnError = (UCase$(CStr(data(i, 4))) = "Y")
jobs(i).Comment = CStr(data(i, 5))
Next
LoadBatchJobs = jobs
End Function
VBメインの RunBatch はこうなります。
Public Sub RunBatch()
Const MODULE_NAME As String = "BatchRunner"
Dim jobs As Variant
jobs = LoadBatchJobs()
If IsEmpty(jobs) Then
MsgBox "ConfigBatchにジョブがありません。", vbInformation
Exit Sub
End If
SpeedOn
LogStart MODULE_NAME, "バッチ処理開始"
Dim i As Long
For i = LBound(jobs) To UBound(jobs)
If jobs(i).Enabled Then
If Not RunOneJob(jobs(i), MODULE_NAME) Then
If jobs(i).StopOnError Then
LogWrite "WARN", MODULE_NAME, "BATCH_STOP", _
"ジョブ[" & jobs(i).JobName & "]でエラー。StopOnError=Yのため中断。"
Exit For
End If
End If
End If
Next i
LogEnd MODULE_NAME, "バッチ処理終了"
SpeedOff
End Sub
VBRunOneJob は、MacroName を Application.Run で呼び出すだけです。
Private Function RunOneJob(ByRef job As BatchJob, ByVal batchModuleName As String) As Boolean
On Error GoTo ErrHandler
LogWrite "INFO", batchModuleName, "JOB_START", _
"ジョブ開始: " & job.JobName & " (" & job.MacroName & ")"
Application.Run job.MacroName
LogWrite "INFO", batchModuleName, "JOB_END", "ジョブ終了: " & job.JobName
RunOneJob = True
Exit Function
ErrHandler:
LogWrite "ERROR", batchModuleName, "JOB_ERROR", _
"ジョブ[" & job.JobName & "]でエラー発生。Err=" & Err.Number & ", " & Err.Description
RunOneJob = False
End Function
VBここでの“モジュール構成的な”ポイントは、業務フローの制御を ModBatchEngine に集約することです。
個々の業務マクロは「自分の仕事だけ」を書き、順番やエラー時の振る舞いはバッチ側に任せます。
エンジン系モジュール:JOIN・集計・差分・変換を役割ごとに分ける
役割ごとにモジュールを分ける理由
ModJoinEngine には JOIN 専用のロジックだけ。
ModAggEngine には 集計専用のロジックだけ。
ModDiffEngine には 差分専用のロジックだけ。
ModConvEngine には 変換専用のロジックだけ。
こう分けることで、「JOINの仕様を変えたい」と思ったときに、見るべきモジュールが一つに絞れます。
逆に、全部を ModMain みたいな一つのモジュールに詰め込むと、
どこに何が書いてあるか分からなくなり、修正のたびに地獄を見ます。
典型例:ModConvEngine の骨格
' ModConvEngine.bas
Option Explicit
Private Type ConvRule
Enabled As Boolean
SourceSheet As String
SourceCol As Long
TargetSheet As String
TargetCol As Long
TransformName As String
Options As String
End Type
Private Function LoadConvRules() As Variant
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("ConfigConv")
Dim lastRow As Long
lastRow = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row
If lastRow < 2 Then
LoadConvRules = Empty
Exit Function
End If
Dim data As Variant
data = ws.Range(ws.Cells(2, 1), ws.Cells(lastRow, 7)).Value
Dim rules() As ConvRule
ReDim rules(1 To UBound(data, 1))
Dim i As Long
For i = 1 To UBound(data, 1)
rules(i).Enabled = (UCase$(CStr(data(i, 1))) = "Y")
rules(i).SourceSheet = CStr(data(i, 2))
rules(i).SourceCol = ColToNumber(data(i, 3))
rules(i).TargetSheet = CStr(data(i, 4))
rules(i).TargetCol = ColToNumber(data(i, 5))
rules(i).TransformName = CStr(data(i, 6))
rules(i).Options = CStr(data(i, 7))
Next
LoadConvRules = rules
End Function
Public Sub RunConvTool()
Dim rules As Variant
rules = LoadConvRules()
If IsEmpty(rules) Then Exit Sub
SpeedOn
Dim i As Long
For i = LBound(rules) To UBound(rules)
If rules(i).Enabled Then
ApplyConvRule rules(i)
End If
Next i
SpeedOff
End Sub
VBApplyConvRule の中で、Application.Run で TransformName を呼び出します。
JOIN・集計・差分も、同じパターンでモジュールを分けておくと、構造が揃ってとても見通しが良くなります。
共通基盤モジュール:全プロジェクトで使い回す“土台”
ModLogger:ログ専用モジュール
ログ関連は、必ず一つのモジュールにまとめます。
どのモジュールからも同じ LogWrite / LogStart / LogEnd / LogError を呼ぶ形にします。
' ModLogger.bas
Option Explicit
Public Sub LogWrite( _
ByVal level As String, _
ByVal moduleName As String, _
ByVal eventName As String, _
Optional ByVal message As String = "")
Dim ws As Worksheet
On Error Resume Next
Set ws = ThisWorkbook.Worksheets("Log")
On Error GoTo 0
If ws Is Nothing Then
Set ws = ThisWorkbook.Worksheets.Add
ws.Name = "Log"
ws.Range("A1:E1").Value = Array("DateTime", "Level", "Module", "Event", "Message")
End If
Dim nextRow As Long
nextRow = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row + 1
ws.Cells(nextRow, 1).Value = Now
ws.Cells(nextRow, 2).Value = UCase$(level)
ws.Cells(nextRow, 3).Value = moduleName
ws.Cells(nextRow, 4).Value = eventName
ws.Cells(nextRow, 5).Value = message
End Sub
Public Sub LogStart(ByVal moduleName As String, Optional ByVal message As String = "")
LogWrite "INFO", moduleName, "START", message
End Sub
Public Sub LogEnd(ByVal moduleName As String, Optional ByVal message As String = "")
LogWrite "INFO", moduleName, "END", message
End Sub
Public Sub LogError(ByVal moduleName As String, ByVal eventName As String, ByVal errObj As ErrObject)
Dim msg As String
msg = "ErrNumber=" & errObj.Number & ", Description=" & errObj.Description
LogWrite "ERROR", moduleName, eventName, msg
End Sub
VBここを一箇所にまとめておくと、「ログのフォーマットを変えたい」ときも ModLogger だけ直せば済みます。
ModStopWatch:処理時間計測の共通部品
処理時間計測も、専用モジュールに切り出します。
StopWatch クラスと組み合わせて使う形です。
' ModStopWatch.bas
Option Explicit
Public Function ElapsedSeconds(ByVal startTime As Double) As Double
Dim nowT As Double
nowT = Timer
If nowT >= startTime Then
ElapsedSeconds = nowT - startTime
Else
ElapsedSeconds = (86400# - startTime) + nowT
End If
End Function
VBStopWatch クラスは別途クラスモジュールに置きます(前回のテンプレの通り)。
モジュール構成として、「時間計測はここを見る」と決めておくのが大事です。
ModConfigUtil:設定シート系の小物を集約
列記号→列番号変換など、設定シート周りの小物関数はここに集めます。
' ModConfigUtil.bas
Option Explicit
Public Function ColToNumber(ByVal colRef As Variant) As Long
If IsNumeric(colRef) Then
ColToNumber = CLng(colRef)
Else
ColToNumber = Range(CStr(colRef) & "1").Column
End If
End Function
VBJOIN・集計・差分・変換の各エンジンから共通で使うことで、「列の解釈がバラバラ」という事故を防げます。
クラスモジュール:役割がはっきりした“部品”だけをクラス化する
どこまでクラスにするか
全部をクラスにしようとすると、逆に分かりづらくなります。
モジュール構成テンプレとしては、「役割がはっきりしていて、状態を持つものだけクラスにする」がちょうどいいです。
例えば、StopWatch は典型的です。
開始時刻と前ステップ時刻という“状態”を持つ。
Lap を呼ぶたびに内部状態が変わる。
TotalSeconds や GetLogText で“結果”を返す。
こういうものはクラスにするときれいに収まります。
逆に、単なるユーティリティ関数(列変換など)は標準モジュールで十分です。
例題:このモジュール構成テンプレを実務ブックに当てはめる
例えば、「顧客日次処理ブック」を作るとします。
モジュール構成はこうなります。
ModEntry
日次入口(RunDailyJobs)だけを書く。
ModBatchEngine
ConfigBatch を読み、ImportCustomer / ConvCustomer / JoinCustomer / AggCustomer を順に実行。
ModConvEngine
ConfigConv を読み、ノーコード変換ツールとして顧客整形を行う。
ModJoinEngine
ConfigJoin を読み、顧客×マスタJOINを行う。
ModAggEngine
ConfigAgg を読み、顧客集計を行う。
ModLogger
全マクロ共通のログ出力。
ModStopWatch + StopWatch クラス
重い処理の時間計測。
ModConfigUtil
列変換などの共通ユーティリティ。
業務マクロ(ImportCustomer / ConvCustomer / JoinCustomer / AggCustomer)は、
それぞれ「エンジン呼び出し+ログ+エラー処理」だけを書く。
こうしておくと、「どの処理を直したいか」に応じて、見るべきモジュールが自然に決まります。
まとめ:モジュール構成テンプレは「未来の自分への優しさ」
今日のポイントを一言で言うと、「役割ごとにモジュールを分け、名前と中身のルールを決める」です。
入口は ModEntry。
業務フローは ModBatchEngine。
JOIN・集計・差分・変換はそれぞれ専用モジュール。
ログ・時間計測・設定ユーティリティは共通基盤モジュール。
状態を持つ部品だけクラスモジュール。
この“型”を一度決めてしまえば、新しいブックを作るたびに
「今回も同じ構成でいこう」と迷わずに済みます。
もし、今あなたが持っているVBAプロジェクトのスクリーンショットやモジュール名一覧があれば、
それをこのテンプレにどう整理し直せるか、一緒に具体的に組み替えてみるのも面白いですよ。
