Excel VBA 逆引き集 | クラスによるレイヤー設計

Excel VBA
スポンサーリンク
  1. ねらい:クラスで「レイヤー」を分け、壊れにくく拡張しやすいVBAへ
    1. 重要ポイントの深掘り
  2. レイヤーの全体像と依存の向き
    1. レイヤー構成の俯瞰
    2. 依存の向きを固定する理由
  3. ドメイン層の設計とコード(例:社員+合格判定)
    1. 1件を表すドメインクラス(CEmployee)
    2. 集合を扱うドメインクラス(CEmployeeList)
    3. 重要ポイントの深掘り
  4. リポジトリ層の設計とコード(例:シートから読み、シートへ書く)
    1. シート→ドメインへの読み取り(CEmployeeRepository)
    2. 重要ポイントの深掘り
  5. アプリサービス層の設計とコード(業務フローの司令塔)
    1. 間に立って「開始→読み→業務→出力→記録」を束ねる(CEmployeeService)
    2. 重要ポイントの深掘り
  6. UI層(入口)の設計とコード(Run_XXXXに統一)
    1. 標準モジュールからサービスを呼ぶ入口
    2. 重要ポイントの深掘り
  7. インフラ補助(設定・ログ・進捗・開始終了)
    1. 設定とログの最小クラスまたはモジュール
    2. 重要ポイントの深掘り
  8. 例題シナリオ:社員データの合格判定をレイヤーで組む
    1. 流れの確認
    2. 動作確認の小テスト
  9. 拡張の指針と落とし穴の回避
    1. レイヤー拡張の指針
    2. よくある落とし穴の予防
  10. 導入手順:今日からクラスレイヤーを適用する最短ルート
    1. 段階的な置き換えの道筋
    2. 成果の確認方法
  11. まとめ:クラス×レイヤーで「直す場所が分かる」VBAへ

ねらい:クラスで「レイヤー」を分け、壊れにくく拡張しやすいVBAへ

業務VBAは、UI・入出力・業務ロジック・設定などが混ざりがちで、少しの変更で全体が壊れます。クラスによるレイヤー設計は、役割ごとに境界を作り、変更の影響範囲を小さく保つための型(構造)です。初心者でも扱えるように、レイヤーを4段(UI/アプリサービス/リポジトリ/ドメイン)に分け、貼って動くコードと例題で解説します。

重要ポイントの深掘り

レイヤーの境界は「依存の向き」を守ることで効力を発揮します。UI→アプリサービス→ドメイン・リポジトリの一方向に限定し、逆流(ドメインがセルを触る、UIが直接シートへ書く)を禁止すると、列順変更や業務ルールの改訂にも強くなります。クラスは「状態と責務」を持てるため、妥当性チェックや処理を適切な場所へ閉じ込められます。


レイヤーの全体像と依存の向き

レイヤー構成の俯瞰

UI(Run_XXXXやフォーム)から始まり、アプリサービス(業務フローをまとめるクラス)がドメイン(業務の対象を表すクラス)を扱い、シートやファイルの読み書きはリポジトリ(入出力担当クラス)が担います。設定やログはインフラ(補助クラス)として横から提供します。

依存の向きを固定する理由

UIはアプリサービスへ、アプリサービスはドメインとリポジトリへ依存します。ドメインは業務ルールだけを扱い、Excelのセル参照を持ちません。リポジトリは「どこから読み書きするか」だけに責務を限定します。これにより、業務ルールの変更はドメイン層だけ、列やブックの変更はリポジトリ層だけ修正すれば済みます。


ドメイン層の設計とコード(例:社員+合格判定)

1件を表すドメインクラス(CEmployee)

社員1件の状態・妥当性・簡単な振る舞いを閉じ込めます。セルに触れず、値だけを扱います。

' CEmployee.cls
Option Explicit

Private pEmpNo As String
Private pName As String
Private pDept As String
Private pScore As Double

Public Property Get EmpNo() As String: EmpNo = pEmpNo: End Property
Public Property Let EmpNo(ByVal v As String)
    If Len(v) <> 6 Or v Like "*[!0-9]*" Then Err.Raise 1001, , "社員番号は6桁の数字"
    pEmpNo = v
End Property

Public Property Get Name() As String: Name = pName: End Property
Public Property Let Name(ByVal v As String): pName = Trim$(v): End Property

Public Property Get Dept() As String: Dept = pDept: End Property
Public Property Let Dept(ByVal v As String): pDept = Trim$(v): End Property

Public Property Get Score() As Double: Score = pScore: End Property
Public Property Let Score(ByVal v As Double)
    If v < 0 Or v > 100 Then Err.Raise 1002, , "スコアは0~100"
    pScore = v
End Property

Public Function IsPass(ByVal threshold As Double) As Boolean
    IsPass = (pScore >= threshold)
End Function
VB

集合を扱うドメインクラス(CEmployeeList)

複数件を検索・配列化できるようにします。Excelへは配列で渡せる形を用意します。

' CEmployeeList.cls
Option Explicit

Private items As Collection

Private Sub Class_Initialize(): Set items = New Collection: End Sub

Public Sub Add(ByVal emp As CEmployee): items.Add emp, emp.EmpNo: End Sub

Public Function Count() As Long: Count = items.Count: End Function

Public Function Find(ByVal empNo As String) As CEmployee
    On Error Resume Next
    Set Find = items(empNo)
    On Error GoTo 0
End Function

Public Function ToArrayWithPass(ByVal threshold As Double) As Variant
    If items.Count = 0 Then Exit Function
    Dim a() As Variant: ReDim a(1 To items.Count + 1, 1 To 5)
    a(1, 1) = "EmpNo": a(1, 2) = "Name": a(1, 3) = "Dept": a(1, 4) = "Score": a(1, 5) = "Pass"
    Dim i As Long
    For i = 1 To items.Count
        Dim e As CEmployee: Set e = items(i)
        a(i + 1, 1) = e.EmpNo
        a(i + 1, 2) = e.Name
        a(i + 1, 3) = e.Dept
        a(i + 1, 4) = e.Score
        a(i + 1, 5) = IIf(e.IsPass(threshold), "○", "×")
    Next
    ToArrayWithPass = a
End Function
VB

重要ポイントの深掘り

ドメインは「セルを知らない」ことが肝心です。セル参照が混入するとテスト不能・変更に弱くなります。妥当性チェックはプロパティで受け止め、異常値は早期に弾くことで、下流の不具合と調査コストを大幅に下げられます。


リポジトリ層の設計とコード(例:シートから読み、シートへ書く)

シート→ドメインへの読み取り(CEmployeeRepository)

列名に依存しないなら列番号、堅牢にするならテーブル列名でIndexを取得してマッピングします。

' CEmployeeRepository.cls
Option Explicit

Public Function ReadFromSheet(ByVal ws As Worksheet) As CEmployeeList
    Dim last As Long: last = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
    Dim list As New CEmployeeList
    Dim r As Long
    For r = 2 To last
        Dim emp As New CEmployee
        emp.EmpNo = CStr(ws.Cells(r, "A").Value)
        emp.Name  = CStr(ws.Cells(r, "B").Value)
        emp.Dept  = CStr(ws.Cells(r, "C").Value)
        If IsNumeric(ws.Cells(r, "D").Value) Then emp.Score = CDbl(ws.Cells(r, "D").Value)
        list.Add emp
    Next
    Set ReadFromSheet = list
End Function

Public Sub WritePassToSheet(ByVal ws As Worksheet, ByVal list As CEmployeeList, ByVal threshold As Double)
    Dim arr As Variant: arr = list.ToArrayWithPass(threshold)
    If IsEmpty(arr) Then Exit Sub
    ws.Range("A1").Resize(UBound(arr, 1), UBound(arr, 2)).Value = arr
End Sub
VB

重要ポイントの深掘り

リポジトリは「読み方・書き方」の責務だけに集中します。列順が変わる可能性があるなら、ListObjectの列名からIndexを取る方針にすると壊れにくくなります。読み取りで型チェックを挟み、ドメインへわたす前に最低限の正規化を行うのが安全です。


アプリサービス層の設計とコード(業務フローの司令塔)

間に立って「開始→読み→業務→出力→記録」を束ねる(CEmployeeService)

開始・終了の枠、設定、ログ、進捗をまとめて面倒を見ます。UI側はこのサービスだけ呼びます。

' CEmployeeService.cls
Option Explicit

Private threshold As Double

Public Sub Init(ByVal th As Double)
    threshold = th
End Sub

Public Sub RunPassProcess(ByVal wsIn As Worksheet, ByVal wsOut As Worksheet)
    On Error GoTo EH
    AppEnter "合格判定"
    LogInfo "Start", "EmployeePassProcess"
    
    Dim repo As New CEmployeeRepository
    Dim list As CEmployeeList
    Set list = repo.ReadFromSheet(wsIn)
    
    repo.WritePassToSheet wsOut, list, threshold
    
    LogInfo "Finish", "Count=" & list.Count
    AppLeave
    MsgBox "完了(" & list.Count & "件)"
    Exit Sub
EH:
    LogError "EmployeePassProcess", Err.Number & " - " & Err.Description
    AppLeave
    MsgBox "失敗: " & Err.Description, vbExclamation
End Sub
VB

重要ポイントの深掘り

アプリサービスは「一番上」から見た業務フローをクラスに閉じ込めます。開始・終了・ログ・設定・進捗など、どの業務でも共通な処理をここで統一すれば、UIは薄く、ドメインは純粋に保てます。サービスが太るなら、サブサービス(集計、エクスポート等)に分割して責務を再整理します。


UI層(入口)の設計とコード(Run_XXXXに統一)

標準モジュールからサービスを呼ぶ入口

UIは短く、サービスの公開メソッドを呼ぶだけにします。ショートカットキーやボタンはこのRun_XXXXへ割り当てます。

' ModServiceEntry.bas
Option Explicit

Public Sub Run_EmployeePass()
    Dim svc As New CEmployeeService
    svc.Init GetConfigNumber("THRESHOLD")
    svc.RunPassProcess Worksheets(GetConfigString("INPUT_SHEET")), _
                       Worksheets(GetConfigString("OUTPUT_SHEET"))
End Sub
VB

重要ポイントの深掘り

入口統一(Run_XXXX)は、運用手順書をシンプルにし、障害対応の導線を揃えます。UIで個別にセルへ触るのは厳禁です。すべての操作はサービス経由にすると、後からログや進捗、エラー復帰を一括で制御できます。


インフラ補助(設定・ログ・進捗・開始終了)

設定とログの最小クラスまたはモジュール

設定はキーで読み、ログはStart/Finish/Errorの3点を残します。クラス化しても構いませんが、最初は標準モジュールで十分です。

' 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
' 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
Public Function GetConfigNumber(ByVal key As String) As Double
    Dim s As String: s = GetConfigString(key)
    If Not IsNumeric(s) Then Err.Raise 901, , "数値ではありません: " & key & "=" & s
    GetConfigNumber = CDbl(s)
End Function
VB
' ModLog.bas
Option Explicit
Public Sub LogInfo(ByVal action As String, ByVal detail As String): WriteLog "INFO", action, detail: End Sub
Public Sub LogError(ByVal action As String, ByVal detail As String): WriteLog "ERROR", action, detail: End Sub
Private Sub WriteLog(ByVal level As String, ByVal action As String, ByVal detail As String)
    Dim ws As Worksheet
    On Error Resume Next
    Set ws = ThisWorkbook.Worksheets("Log")
    If ws Is Nothing Then Set ws = ThisWorkbook.Worksheets.Add: ws.Name = "Log"
    On Error GoTo 0
    Dim r As Long: r = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row + 1
    ws.Cells(r, 1).Value = Format(Now, "yyyy-mm-dd HH:NN:SS")
    ws.Cells(r, 2).Value = level
    ws.Cells(r, 3).Value = action
    ws.Cells(r, 4).Value = detail
End Sub
VB

重要ポイントの深掘り

設定外出しは「本番差異」に耐えるための基礎です。ログは「事実の記録」で、後から辿れる安心を作ります。開始・終了の枠(AppEnter/AppLeave)は、エラー時でも必ず復帰させる業務品質の支えです。


例題シナリオ:社員データの合格判定をレイヤーで組む

流れの確認

Configシートに INPUT_SHEET、OUTPUT_SHEET、THRESHOLD を用意します。Run_EmployeePass を実行すると、サービスが起動し、リポジトリで読み→ドメインで判定→配列で生成→リポジトリで書き戻し→ログ記録→完了通知の順で進みます。列が変わればリポジトリだけ、合格ロジックが変わればドメインだけ修正すればよく、影響範囲が最小化されます。

動作確認の小テスト

ドメイン単体でIsPassが正しく動くか、CEmployeeList.ToArrayWithPassが期待の形かをImmediateへ出力して確認します。サービス全体はRun_EmployeePassで実行して、件数と出力形を確かめます。


拡張の指針と落とし穴の回避

レイヤー拡張の指針

フォーム(UI)を追加する場合でも、フォームはサービスの公開メソッドを呼ぶだけにし、入力値の妥当性はフォーム側で最小限に、業務ルールはドメインで扱う方針を守ります。外部出力(CSVやSQL)を増やす場合は、対応するリポジトリクラス(CCsvRepository、CSqlRepository)を追加し、サービスから差し替え可能にします。

よくある落とし穴の予防

ドメインからRangeを参照しない、サービスにセル処理を持ち込まない、リポジトリに業務ルールを書かない、という三禁則を徹底します。どれかを破ると境界が崩れ、修正が難しくなります。イベント処理は「イベント監視クラス」を別に作り、サービスを呼ぶだけにすると、暴走や再帰を避けられます。


導入手順:今日からクラスレイヤーを適用する最短ルート

段階的な置き換えの道筋

まずはドメイン(CEmployee)とリポジトリ(CEmployeeRepository)を作って、既存の処理をサービスクラス(CEmployeeService)へ移します。入口Run_XXXXを用意し、Config・ログ・開始終了を組み込みます。次に集合クラス(CEmployeeList)を導入し、配列書き戻しへ置換します。最後に、列名参照やCSV出力などのリポジトリ拡張を行い、業務フロー全体をクラスから呼ぶ形へ統一します。

成果の確認方法

ドメインの単体テスト、サービスの統合テスト(Run_XXXX)、ログ出力の確認で安全性を確かめます。列順やしきい値を変えても壊れないかを試し、修正範囲が小さく保たれていることを確認すると、レイヤー設計が効果を発揮していると分かります。


まとめ:クラス×レイヤーで「直す場所が分かる」VBAへ

クラスによるレイヤー設計は、役割と境界を明確にし、変更の影響を最小化します。UIは薄く、サービスが業務フローを束ね、ドメインがルールを保ち、リポジトリが入出力を担う。この形にすると、遅さ・壊れやすさ・引き継ぎの難しさが一気に改善します。

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