ねらい:Excel VBAで「依存性注入(DI風)」を取り入れ、差し替えやすく壊れにくい設計にする
依存性注入(DI)は「使う側が必要な部品を自分で作らず、外から渡してもらう」設計です。VBAにはDIコンテナはありませんが、クラスのコンストラクタ代わりの初期化メソッドやプロパティで依存を渡せば、DI風の効果を得られます。保存先がシート・テーブル・CSVに変わっても、呼ぶ側(サービス)は差し替えだけで動き、テストも楽になります。
重要ポイントの深掘り
VBAでDI風にする要点は「依存を直接newしない」「契約(インターフェース相当)に依存する」「初期化メソッドで外部から渡す」の3つです。これにより、部品の入れ替え・テストダブル(モック)差し替え・運用差異への対応が、最小修正で可能になります。さらに、開始・終了の共通枠と配列I/Oを組み合わせることで、速度・安定性・保守性が同時に向上します。
基本構造:契約を固定し、具体実装を外から渡す
抽象ベースクラスで「契約(読み・書き)」を表現する
VBAに正式なインターフェース構文はありません。そこで抽象ベースクラス(未実装のメソッドがErr.Raiseするクラス)を用意して、呼ぶ側はこの契約に依存します。
' IRepository.cls(抽象:読み込み・保存の契約)
Option Explicit
Public Function LoadAll() As Variant
Err.Raise 9000, , "Not implemented"
End Function
Public Sub SaveAll(ByVal data As Variant)
Err.Raise 9001, , "Not implemented"
End Sub
VB具体実装は「どこから・どう読み書きするか」を担当する
シート版、テーブル(ListObject)版、CSV版など、保存先ごとに具体クラスを用意します。契約の形(配列の入出力)は揃えます。
' SheetRepository.cls
Option Explicit
Private ws As Worksheet
Private anchor As String
Public Sub Init(ByVal target As Worksheet, ByVal topLeft As String)
Set ws = target: anchor = topLeft
End Sub
Public Function LoadAll() As Variant
LoadAll = ws.Range(anchor).CurrentRegion.Value
End Function
Public Sub SaveAll(ByVal data As Variant)
ws.Range(anchor).Resize(UBound(data, 1), UBound(data, 2)).Value = data
End Sub
VB' TableRepository.cls
Option Explicit
Private lo As ListObject
Public Sub Init(ByVal target As ListObject)
Set lo = target
End Sub
Public Function LoadAll() As Variant
LoadAll = lo.DataBodyRange.Value
End Function
Public Sub SaveAll(ByVal data As Variant)
lo.DataBodyRange.Resize(UBound(data, 1), UBound(data, 2)).Value = data
End Sub
Public Function IndexOf(ByVal colName As String) As Long
IndexOf = lo.ListColumns(colName).Index
End Function
VB' CsvRepository.cls(UTF-8で保存・読込)
Option Explicit
Private path As String
Public Sub Init(ByVal csvPath As String)
path = csvPath
End Sub
Public Function LoadAll() As Variant
Dim st As Object: Set st = CreateObject("ADODB.Stream")
st.Type = 2: st.Charset = "UTF-8": st.Open
st.LoadFromFile path
Dim text As String: text = st.ReadText
st.Close: Set st = Nothing
Dim lines() As String: lines = Split(text, vbCrLf)
If UBound(lines) < 0 Then Exit Function
Dim head() As String: head = ParseCsvLine(lines(0))
Dim cols As Long: cols = UBound(head) + 1
Dim arr() As Variant: ReDim arr(1 To UBound(lines) + 1, 1 To cols)
Dim c As Long: For c = 1 To cols: arr(1, c) = head(c - 1): Next
Dim r As Long
For r = 2 To UBound(lines) + 1
If Len(lines(r - 1)) = 0 Then Exit For
Dim rec() As String: rec = ParseCsvLine(lines(r - 1))
For c = 1 To cols: arr(r, c) = IIf(c - 1 <= UBound(rec), rec(c - 1), ""): Next
Next
LoadAll = arr
End Function
Public Sub SaveAll(ByVal data As Variant)
Dim st As Object: Set st = CreateObject("ADODB.Stream")
st.Type = 2: st.Charset = "UTF-8": st.Open
Dim r As Long, c As Long, line As String
For r = 1 To UBound(data, 1)
line = ""
For c = 1 To UBound(data, 2)
Dim s As String: s = Replace(CStr(data(r, c)), """", """""")
line = line & IIf(c > 1, ",", "") & """" & s & """"
Next
st.WriteText line & vbCrLf
Next
st.SaveToFile path, 2
st.Close: Set st = Nothing
End Sub
Private Function ParseCsvLine(ByVal line As String) As String()
Dim res() As String, buf As String, i As Long, inQ As Boolean
ReDim res(0 To 0)
For i = 1 To Len(line)
Dim ch As String: ch = Mid$(line, i, 1)
If ch = """" Then
If inQ And i < Len(line) And Mid$(line, i + 1, 1) = """" Then
buf = buf & """": i = i + 1
Else
inQ = Not inQ
End If
ElseIf ch = "," And Not inQ Then
res(UBound(res)) = buf: buf = "": ReDim Preserve res(0 To UBound(res) + 1)
Else
buf = buf & ch
End If
Next
res(UBound(res)) = buf
ParseCsvLine = res
End Function
VBDI風の注入方法:サービスに依存を「外から渡す」
サービスクラスは契約に依存し、具体はプロパティやInitで受け取る
サービス(業務フロー)は IRepository にだけ依存します。外部から具体Repositoryを渡してもらい、業務処理を実行します。これがDI風の核です。
' EmployeeService.cls
Option Explicit
Private repo As IRepository
Private threshold As Double
Public Sub Init(ByVal repository As IRepository, ByVal th As Double)
Set repo = repository
threshold = th
End Sub
Public Sub RunPass(ByVal wsOut As Worksheet)
On Error GoTo EH
AppEnter "合格判定"
Dim data As Variant: data = repo.LoadAll
RequireHeaders data, Array("EmpNo", "Name", "Dept", "Score")
Dim idxScore As Long: idxScore = IndexByHeader(data, "Score")
Dim rows As Long: rows = UBound(data, 1)
Dim cols As Long: cols = UBound(data, 2)
Dim out() As Variant: ReDim out(1 To rows, 1 To cols + 1)
Dim c As Long: For c = 1 To cols: out(1, c) = data(1, c): Next
out(1, cols + 1) = "Pass"
Dim r As Long
For r = 2 To rows
For c = 1 To cols: out(r, c) = data(r, c): Next
out(r, cols + 1) = IIf(CDbl(data(r, idxScore)) >= threshold, "○", "×")
Next
wsOut.Range("A1").Resize(rows, cols + 1).Value = out
AppLeave
MsgBox "完了(" & rows - 1 & "件)"
Exit Sub
EH:
AppLeave
MsgBox "失敗: " & Err.Description, vbExclamation
End Sub
VB' ModHeaderMap.bas(ヘッダー検証と取得)
Option Explicit
Public Function IndexByHeader(ByVal data As Variant, ByVal headerName As String) As Long
Dim cols As Long: cols = UBound(data, 2)
Dim j As Long
For j = 1 To cols
If StrComp(CStr(data(1, j)), headerName, vbTextCompare) = 0 Then
IndexByHeader = j: Exit Function
End If
Next
Err.Raise 9100, , "ヘッダーが見つかりません: " & headerName
End Function
Public Sub RequireHeaders(ByVal data As Variant, ByVal headers As Variant)
Dim i As Long
For i = LBound(headers) To UBound(headers)
Call IndexByHeader(data, CStr(headers(i)))
Next
End Sub
VB重要ポイントの深掘り
サービスが具体クラスをnewしないことで、保存先の差し替えが容易になります。テスト時にはモック(テスト用のIRepository実装)を渡すことで、入出力なしでロジックだけ検証できます。VBAでも「外から渡す」だけでDIの実用的なメリットが得られます。
入口と注入のパターン:どこで具体を作り、どう渡すか
標準モジュール(Entry)で具体を作り、サービスへ渡す
入口Run_XXXXで実際の保存先に応じたRepositoryをnewし、Initでサービスに注入します。切り替えは1行です。
' ModEntry.bas
Option Explicit
Public Sub Run_Pass_Sheet()
Dim r As New SheetRepository
r.Init Worksheets("Input"), "A1"
Dim svc As New EmployeeService
svc.Init r, 70#
svc.RunPass Worksheets("Output")
End Sub
Public Sub Run_Pass_Table()
Dim r As New TableRepository
r.Init Worksheets("Input").ListObjects("tblEmployees")
Dim svc As New EmployeeService
svc.Init r, 70#
svc.RunPass Worksheets("Output")
End Sub
Public Sub Run_Pass_Csv()
Dim r As New CsvRepository
r.Init ThisWorkbook.Path & "\employees.csv"
Dim svc As New EmployeeService
svc.Init r, 70#
svc.RunPass Worksheets("Output")
End Sub
VBConfigから注入する構成(運用差異をコード変更なしで吸収)
Configの「REPO_TYPE」を読み、対応する具体を選んで注入します。運用現場の切り替えはConfigだけです。
' ModInjector.bas
Option Explicit
Public Function BuildRepositoryFromConfig() As IRepository
Dim t As String: t = GetConfigString("REPO_TYPE") ' "SHEET" / "TABLE" / "CSV"
Select Case UCase$(t)
Case "SHEET"
Dim rs As New SheetRepository
rs.Init Worksheets(GetConfigString("INPUT_SHEET")), GetConfigString("ANCHOR")
Set BuildRepositoryFromConfig = rs
Case "TABLE"
Dim rt As New TableRepository
rt.Init Worksheets(GetConfigString("INPUT_SHEET")).ListObjects(GetConfigString("TABLE_NAME"))
Set BuildRepositoryFromConfig = rt
Case "CSV"
Dim rc As New CsvRepository
rc.Init GetConfigString("CSV_PATH")
Set BuildRepositoryFromConfig = rc
Case Else
Err.Raise 9500, , "未知のREPO_TYPE: " & t
End Select
End Function
VB' ModConfig.bas(キー読み取り)
Option Explicit
Private Function ConfigSheet() As Worksheet: Set ConfigSheet = ThisWorkbook.Worksheets("Config"): End Function
Public Function GetConfigString(ByVal key As String) As String
Dim ws As Worksheet: Set ws = ConfigSheet()
Dim last As Long: last = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
Dim r As Long
For r = 2 To last
If StrComp(CStr(ws.Cells(r, "A").Value), key, vbTextCompare) = 0 Then
GetConfigString = Trim$(CStr(ws.Cells(r, "B").Value))
Exit Function
End If
Next
Err.Raise 900, , "Configキーが見つかりません: " & key
End Function
VBモック注入でテストする:入出力なしでロジック検証
テスト用Repository(モック)を注入し、処理結果を確認する
モックは固定の配列を返し、保存は何もしません。サービスの配列処理だけを安全に検証できます。
' MockRepository.cls
Option Explicit
Public Function LoadAll() As Variant
Dim a(1 To 3, 1 To 4) As Variant
a(1, 1) = "EmpNo": a(1, 2) = "Name": a(1, 3) = "Dept": a(1, 4) = "Score"
a(2, 1) = "000001": a(2, 2) = "山田": a(2, 3) = "Sales": a(2, 4) = 80
a(3, 1) = "000002": a(3, 2) = "佐藤": a(3, 3) = "HR": a(3, 4) = 60
LoadAll = a
End Function
Public Sub SaveAll(ByVal data As Variant)
' 何もしない(テスト用)
End Sub
VB' ModTest.bas
Option Explicit
Public Sub Test_RunPass_WithMock()
Dim m As New MockRepository
Dim svc As New EmployeeService
svc.Init m, 70#
svc.RunPass Worksheets("Output") ' OutputにPass列が出る
End Sub
VB重要ポイントの深掘り
テスト時に外部I/Oを切り離せるのがDIの大きな利点です。モックが返す配列でロジックの期待結果を確認し、速度・安定性を保ちつつ、入出力の不確実性(ネットワーク・ファイルロック)を排除して検証できます。
運用品質の枠を組み合わせる:開始・終了、進捗、ログ
開始・終了の共通枠で「失敗しても戻る」設計にする
開始時に描画・イベント・再計算を止め、終了時に必ず復帰します。サービスの入口で徹底します。
' ModApp.bas
Option Explicit
Public Sub AppEnter(Optional ByVal status As String = "")
Application.ScreenUpdating = False
Application.EnableEvents = False
Application.Calculation = xlCalculationManual
If Len(status) > 0 Then Application.StatusBar = status
End Sub
Public Sub AppLeave()
Application.StatusBar = False
Application.Calculation = xlCalculationAutomatic
Application.EnableEvents = True
Application.ScreenUpdating = True
End Sub
VB進捗・ログはサービスで統一管理する
DoEventsの呼びすぎは遅くなります。1〜5%刻みで間引き表示し、Start/Finish/Errorのログを残すと、現場の不安が減り、調査が速くなります。DIと組み合わせることで、保存先を変えても運用品質は一貫します。
例題の流れ:保存先を切り替えて同じロジックを動かす
シート版からCSV版に切り替えるだけで同じ結果を得る
Run_Pass_Sheet と Run_Pass_Csv をそれぞれ実行します。Outputシートには同じヘッダー+Pass列が出力されます。サービスは契約にしか依存していないため、差し替えの影響は入口1箇所だけです。
ConfigでREPO_TYPEを変更し、コード変更なしで運用切り替えする
ConfigのREPO_TYPEを SHEET→TABLE→CSV と変え、Run_Pass_Config を作って BuildRepositoryFromConfig 経由で注入します。運用現場はConfigのキー変更だけで切り替えできます。
' ModEntryConfig.bas
Option Explicit
Public Sub Run_Pass_Config()
Dim repo As IRepository
Set repo = BuildRepositoryFromConfig()
Dim svc As New EmployeeService
svc.Init repo, CDbl(GetConfigString("THRESHOLD"))
svc.RunPass Worksheets(GetConfigString("OUTPUT_SHEET"))
End Sub
VB深掘り:DI風設計で避けられる落とし穴
具体クラスに直接依存して差し替え不能になる問題
サービスが SheetRepository を直接newすると、保存先変更のたびにサービスを書き換える必要が出ます。契約(IRepository)に依存し、具体は外から渡す形にすれば、差し替えは入口だけで済みます。
テストが外部I/Oに引きずられる問題
外部の状態(ファイル、ネットワーク、列順)に依存すると、テストが不安定になりがちです。モック注入すれば、配列ベースのロジックだけを短時間で繰り返し検証できます。
依存が循環して壊れやすくなる問題
サービス→Repository→サービスのような循環を作らないこと。依存は一方向に保ち、責務を混ぜない(Repositoryに業務ルールを書かない、サービスに入出力詳細を持ち込まない)ことで、修正範囲が自然に限定されます。
導入手順:今日からDI風にする最短ルート
ステップ1:契約(IRepository)を作る
抽象ベースクラスに LoadAll/SaveAll を定義し、戻り値は「ヘッダー付き二次元配列」に統一します。
ステップ2:保存先ごとの具体Repositoryを1つ作る
最初はシート版(CurrentRegion)で十分。配列I/Oに統一して速度を確保します。
ステップ3:サービスを契約に依存させ、Initで注入する
サービスで具体をnewしない。入口で具体を作り、Init経由で渡します。
ステップ4:モックを用意してロジックをテストする
固定配列を返すMockRepositoryで、入出力なしの検証を行います。
ステップ5:Configからの注入に拡張する
REPO_TYPEやシート名・テーブル名・CSVパスをConfigへ置き、BuildRepositoryFromConfigで選択・注入します。
まとめ:VBAでも「外から渡す」だけでDIのメリットは十分に得られる
依存性注入(DI風)は、保存先・連携先・運用差異の変更を「入口の差し替えだけ」に局所化し、サービスとロジックを無傷で保ちます。VBAでは、抽象ベースクラスの契約、Init/プロパティ注入、Config選択、モックテストの4点で実用的に運用できます。
