Excel VBA 逆引き集 | Repositoryパターン

Excel VBA
スポンサーリンク
  1. ねらい:Excelで「読み書きの責務」を分離し、壊れないVBAへ
    1. 重要ポイントの深掘り
  2. Repositoryパターンの基本構造と役割
    1. インターフェースで「契約」を固定する意義
    2. 具体Repositoryが「どこから・どう」読み書きするかを担当する
  3. シート版Repository:CurrentRegionで一括読み書き
    1. シートから読み取り、シートへ書き戻す具体実装
    2. 重要ポイントの深掘り
  4. テーブル版Repository:ListObject列名で堅牢化する
    1. 列名でIndexを動的取得し、順序変更に強くする実装
    2. 重要ポイントの深掘り
  5. CSV版Repository:UTF-8で安定保存・読込
    1. CSVへ書く・CSVから読む具体実装
    2. 重要ポイントの深掘り
  6. ヘッダー名でマッピングする補助(列崩れに強くする)
    1. ヘッダーから列番号を動的に引くユーティリティ
    2. 重要ポイントの深掘り
  7. サービス層からRepositoryを「差し替え可能」に呼ぶ
    1. 契約に依存して業務処理を記述する
    2. 重要ポイントの深掘り
  8. 例題で確認:保存先を差し替えても同じ結果を得る
    1. シートとCSVを切り替えて同じPass列を出す
    2. 列順を変えてもヘッダー検証で安全に止める
  9. 実務の落とし穴と対策を先に押さえる
    1. 契約が揺れて呼ぶ側が壊れる問題の回避
    2. 速度と安定性の両立
    3. エラー時の復帰とログ
  10. 導入手順の道筋と拡張の作法
    1. まず抽象契約を決めてから具体リポジトリを追加する
    2. ヘッダー名マッピングを併用して堅牢性を高める
    3. さらに拡張したい場合の方向性
  11. まとめ:保存先の違いを「Repositoryにだけ」閉じ込める

ねらい:Excelで「読み書きの責務」を分離し、壊れないVBAへ

Repositoryパターンは「どこから読み、どこへ書くか」の責務をクラスに閉じ込め、業務ロジックやUIから切り離す設計です。これにより、データの保存先がシートでもテーブルでもCSVでも、ロジック側のコードは変えずに入れ替えられます。初心者でも扱えるように、インターフェース、具体リポジトリ、マッピング、例題まで貼ってそのまま使える形で解説します。

重要ポイントの深掘り

Repositoryは「入出力の窓口」です。業務ロジック(合格判定、集計など)は配列やエンティティを扱い、Excelのセル参照を持ちません。保存先や列順が変わるのは現場では日常です。入出力の変更はRepositoryだけ直す構造にしておくと、壊れにくく、テスト可能になり、速度も出ます。


Repositoryパターンの基本構造と役割

インターフェースで「契約」を固定する意義

呼ぶ側(サービス・ロジック)は「読む・書く」の契約に依存し、具体的な保存先を知りません。実装はあとで差し替え可能にします。VBAには正式なInterface構文がありませんが、「抽象ベースクラス」で同等の契約を表現できます。

' IEmployeeRepository.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

具体Repositoryが「どこから・どう」読み書きするかを担当する

SheetRepository、TableRepository(ListObject)、CsvRepositoryなどを用意して、契約に従って配列を返し、配列を保存します。呼ぶ側は実装の違いを意識しません。


シート版Repository:CurrentRegionで一括読み書き

シートから読み取り、シートへ書き戻す具体実装

最短で動かしたい場合の標準型です。ヘッダー行を含む表を二次元配列で扱います。

' EmployeeSheetRepository.cls
Option Explicit
Private ws As Worksheet
Private topLeft As String

Public Sub Init(ByVal targetWs As Worksheet, ByVal anchor As String)
    Set ws = targetWs
    topLeft = anchor
End Sub

Public Function LoadAll() As Variant
    LoadAll = ws.Range(topLeft).CurrentRegion.Value
End Function

Public Sub SaveAll(ByVal data As Variant)
    ws.Range(topLeft).Resize(UBound(data, 1), UBound(data, 2)).Value = data
End Sub
VB

重要ポイントの深掘り

CurrentRegionは「空行・空列で囲まれた塊」を一発で取得でき、初心者にとって扱いやすい入口です。列順変更に弱い場面では、次章の「ヘッダー名でのマッピング」を併用して堅牢性を上げます。


テーブル版Repository:ListObject列名で堅牢化する

列名でIndexを動的取得し、順序変更に強くする実装

ListObjectを使うと、列名ベースで安全に読み書きできます。業務で列追加・並べ替えが起きても壊れにくくなります。

' EmployeeTableRepository.cls
Option Explicit
Private lo As ListObject

Public Sub Init(ByVal targetLo As ListObject)
    Set lo = targetLo
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

重要ポイントの深掘り

列名参照を使うだけで「列順が変わると壊れる」問題が激減します。前提が崩れたら入口でFail Fast(すぐ停止)させる設計にすると、事故調査が短時間で済みます。


CSV版Repository:UTF-8で安定保存・読込

CSVへ書く・CSVから読む具体実装

現場で配布・連携の定番がCSVです。UTF-8固定とエスケープを部品化して、文字化けや引用符の揺れを防ぎます。

' EmployeeCsvRepository.cls
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)
    Dim r As Long, c As Long, cols As Long
    If UBound(lines) < 0 Then Exit Function
    Dim head() As String: head = ParseCsvLine(lines(0)): cols = UBound(head) + 1

    Dim arr() As Variant: ReDim arr(1 To UBound(lines) + 1, 1 To cols)
    For c = 1 To cols: arr(1, c) = head(c - 1): Next
    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()
    ' 簡易CSV解析(ダブルクォート対応・入れ子なし)
    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
VB

重要ポイントの深掘り

CSVの仕様差でハマりがちなのは文字コードと引用符です。Repository内で統一すると、呼ぶ側は「配列を渡す/受け取る」だけで済み、安定運用できます。


ヘッダー名でマッピングする補助(列崩れに強くする)

ヘッダーから列番号を動的に引くユーティリティ

配列先頭行をヘッダーとして扱い、列名でIndexを取得します。

' 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

重要ポイントの深掘り

ヘッダー検証を「入口で」行うのが肝です。列順が変わっても追従でき、想定外なら早期停止できます。Repositoryが列順に依存する場合でも、検証があるだけで事故を大幅に減らせます。


サービス層からRepositoryを「差し替え可能」に呼ぶ

契約に依存して業務処理を記述する

サービスは保存先を知りません。契約(LoadAll/SaveAll)に基づいて、読み→ロジック→書き戻しを行います。

' ModService.bas
Option Explicit

Public Sub Run_EmployeePass_Sheet()
    Dim repo As New EmployeeSheetRepository
    repo.Init Worksheets("Input"), "A1"
    ProcessPass repo, Worksheets("Output"), 70#
End Sub

Public Sub Run_EmployeePass_Table()
    Dim repo As New EmployeeTableRepository
    repo.Init Worksheets("Input").ListObjects("tblEmployees")
    ProcessPass repo, Worksheets("Output"), 70#
End Sub

Public Sub Run_EmployeePass_Csv()
    Dim repo As New EmployeeCsvRepository
    repo.Init ThisWorkbook.Path & "\employees.csv"
    ProcessPass repo, Worksheets("Output"), 70#
End Sub

Private Sub ProcessPass(ByVal repo As Object, ByVal wsOut As Worksheet, ByVal th As Double)
    Dim data As Variant: data = repo.LoadAll
    RequireHeaders data, Array("EmpNo", "Name", "Dept", "Score")

    Dim rows As Long: rows = UBound(data, 1)
    Dim cols As Long: cols = UBound(data, 2)
    Dim idxScore As Long: idxScore = IndexByHeader(data, "Score")

    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)) >= th, "○", "×")
    Next

    wsOut.Range("A1").Resize(rows, cols + 1).Value = out
End Sub
VB

重要ポイントの深掘り

この形なら「保存先を変えたい」要求に対して、サービスはメソッド1行の差し替えで対応できます。ロジックの配列処理は共通で、Repositoryは入出力だけを担当。これがRepositoryパターンの本質的なメリットです。


例題で確認:保存先を差し替えても同じ結果を得る

シートとCSVを切り替えて同じPass列を出す

Run_EmployeePass_Sheet と Run_EmployeePass_Csv をそれぞれ実行します。Outputシートに同じ形の「Pass」列が現れることを確認してください。入力元が違っても、ロジックと出力は同じです。

列順を変えてもヘッダー検証で安全に止める

Inputの列を入れ替えて実行します。RequireHeadersが不足を検知すれば早期停止し、列名に基づいてIndexを取る実装なら継続して正しく処理されます。これが「列崩れ耐性」の実務的効果です。


実務の落とし穴と対策を先に押さえる

契約が揺れて呼ぶ側が壊れる問題の回避

契約(LoadAll/SaveAll)の引数・戻り値を固定します。戻すのは「二次元配列(ヘッダー含む)」に統一すると、ロジックの再利用性が高まります。型やサイズが変わるなら、バージョン番号や追加の契約ドキュメントを先頭コメントに残してください。

速度と安定性の両立

Repository内ではセル往復を禁止し、必ず一括読み(CurrentRegion/DataBodyRange)と一括書き戻し(Resize代入)にします。ロジック側の配列処理と合わせて、桁違いに速くなり、フリーズしづらくなります。

エラー時の復帰とログ

サービス側で AppEnter/AppLeave とログ記録(Start/Finish/Error)を必ず通す型にしておくと、失敗時でも環境復帰し、調査が容易になります。Repositoryは「入出力の詳細」で失敗しやすい場所なので、例外メッセージに対象ファイル・シート名を含めると現場が助かります。


導入手順の道筋と拡張の作法

まず抽象契約を決めてから具体リポジトリを追加する

最初に IEmployeeRepository(抽象)で「LoadAll/SaveAll」の契約を定義し、シート版を作ります。次にテーブル版、CSV版を追加して差し替えできることを確認します。契約が固いほど、拡張で壊れません。

ヘッダー名マッピングを併用して堅牢性を高める

配列先頭行にヘッダーを必ず載せ、IndexByHeader/RequireHeadersで検証します。列追加・並べ替えの事故を事前に防げます。

さらに拡張したい場合の方向性

外部DB(ADO)のRepositoryを追加したい場合も同じ契約で構いません。LoadAllはレコードセットを二次元配列へ変換、SaveAllはバルクインサートへ差し替え。呼ぶ側は変更不要です。


まとめ:保存先の違いを「Repositoryにだけ」閉じ込める

Repositoryパターンの力は、入出力の変更を局所化し、ロジックを無傷で保つことです。シート、テーブル、CSV、DB——どれへでも差し替えられる「読み書きの窓口」を用意して、業務ロジックは配列で純粋に書く。これがExcel VBAで長く使える設計の土台になります。

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