ねらい:クラスモジュールで「型」を作り、VBAを一段強くする
VBAは手軽ですが、値やRangeが行き交うだけだと複雑化しがち。クラスモジュールは「自分専用の型(モノ)を作る」仕組みです。社員、注文、ルール、タイマーなどを”モノ”として扱うと、責務が明確になり、テストや再利用が楽になります。初心者でも貼って動かせるコードで、重要点を深掘りして解説します。
- 狙い: データ+処理をひとまとまりにして、見通しと再利用性を高める
- 要点: プロパティ(状態)+メソッド(振る舞い)+初期化/後始末で”モノ化”
- 重要ポイント(深掘り):
- カプセル化: 内部の実装を隠し、外からはプロパティ・メソッドだけで扱う
- コンストラクタ的初期化:
Class_InitializeとClass_Terminateを使い、作成/破棄時の準備・片付けを自動化 - イベント:
WithEventsでExcelのイベントを「クラスに閉じ込めて」扱うと、スッキリ
基本:社員クラスで「データ+処理」をひとまとめに
クラスモジュール(名前: CEmployee)
' CEmployee.cls
Option Explicit
Private pEmpNo As String
Private pName As String
Private pDept As String
Private pHireDate As Date
' 生成/破棄イベント(必要に応じて)
Private Sub Class_Initialize()
pEmpNo = ""
pName = ""
pDept = ""
pHireDate = 0
End Sub
Private Sub Class_Terminate()
' 後始末が必要ならここへ(ファイルクローズなど)
End Sub
' --- プロパティ(外から読み書きできる状態) ---
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 = v
End Property
Public Property Get HireDate() As Date
HireDate = pHireDate
End Property
Public Property Let HireDate(ByVal v As Date)
If v <= DateSerial(1900, 1, 1) Then Err.Raise 1002, , "入社日が不正"
pHireDate = v
End Property
' --- メソッド(振る舞い) ---
Public Function SeniorityYears() As Long
If pHireDate = 0 Then SeniorityYears = 0 Else SeniorityYears = DateDiff("yyyy", pHireDate, Date)
End Function
Public Function ToArray() As Variant
Dim a(1 To 1, 1 To 4) As Variant
a(1, 1) = pEmpNo
a(1, 2) = pName
a(1, 3) = pDept
a(1, 4) = pHireDate
ToArray = a
End Function
VB呼び出し例(標準モジュール)
Sub Example_EmployeeClass()
Dim e As New CEmployee
e.EmpNo = "000123"
e.Name = "山田太郎"
e.Dept = "営業"
e.HireDate = DateSerial(2018, 4, 1)
Debug.Print e.Name & "(勤続 " & e.SeniorityYears & " 年)"
End Sub
VB- 重要深掘り:
- 検証をプロパティで担う: 不正値は入れさせない。データ品質をクラスで保証。
- メソッドは「そのモノの仕事」だけ: 勤続年数など、社員が知っていることを社員クラスに。
コレクション管理:複数件を「リスト」として扱う
クラスモジュール(名前: CEmployeeList)
' 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 Sub Remove(ByVal empNo As String)
On Error Resume Next
items.Remove empNo
On Error GoTo 0
End Sub
Public Function To2DArray() As Variant
If items.Count = 0 Then Exit Function
Dim r As Long: r = items.Count
Dim a() As Variant: ReDim a(1 To r + 1, 1 To 4)
' ヘッダー
a(1, 1) = "社員番号": a(1, 2) = "氏名": a(1, 3) = "部署": a(1, 4) = "入社日"
' 中身
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.HireDate
Next
To2DArray = a
End Function
VB使い方(標準モジュール)
Sub Example_EmployeeList()
Dim list As New CEmployeeList
Dim e1 As New CEmployee, e2 As New CEmployee
e1.EmpNo = "000101": e1.Name = "佐藤": e1.Dept = "総務": e1.HireDate = DateSerial(2020, 1, 10)
e2.EmpNo = "000102": e2.Name = "鈴木": e2.Dept = "開発": e2.HireDate = DateSerial(2019, 7, 1)
list.Add e1: list.Add e2
Dim arr As Variant: arr = list.To2DArray
Worksheets("Output").Range("A1").Resize(UBound(arr, 1), UBound(arr, 2)).Value = arr
End Sub
VB- 重要深掘り:
- 「1件」と「複数件」を分ける: CEmployee(1件)+CEmployeeList(複数)。型が明確で迷わない。
- To2DArrayで一括書き出し: Rangeへの書き戻しが高速・簡潔。
データ読み込みの責務分離:リーダークラス
UIやロジックに読み方を混ぜないために「リーダー」を用意。
クラスモジュール(名前: CEmployeeReader)
' CEmployeeReader.cls
Option Explicit
Public Function ReadFromSheet(ByVal ws As Worksheet) As CEmployeeList
Dim lastRow As Long: lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
Dim list As New CEmployeeList
Dim r As Long
For r = 2 To lastRow
Dim e As New CEmployee
e.EmpNo = CStr(ws.Cells(r, "A").Value)
e.Name = CStr(ws.Cells(r, "B").Value)
e.Dept = CStr(ws.Cells(r, "C").Value)
If IsDate(ws.Cells(r, "D").Value) Then e.HireDate = CDate(ws.Cells(r, "D").Value)
list.Add e
Next
Set ReadFromSheet = list
End Function
VB使い方(標準モジュール)
Sub Example_ReadEmployees()
Dim rd As New CEmployeeReader
Dim list As CEmployeeList
Set list = rd.ReadFromSheet(Worksheets("Input"))
Debug.Print "件数:", list.Count
Dim e As CEmployee: Set e = list.Find("000101")
If Not e Is Nothing Then Debug.Print e.Name
End Sub
VB- 重要深掘り:
- 読み方が変わってもReaderだけ修正: マッピングや列順変更に強い。
- ロジックは「クラスを前提」に書ける: シート知らずのきれいなコードに。
Excelイベントの”閉じ込め”:WithEventsで安全運用
シート変更やブックイベントをクラスに閉じ込めると、散らばらずに管理できます。
クラスモジュール(名前: CSheetWatcher)
' CSheetWatcher.cls
Option Explicit
Public WithEvents TargetSheet As Worksheet
Private Sub TargetSheet_Change(ByVal Target As Range)
If Not Intersect(Target, TargetSheet.Range("A2:A1000")) Is Nothing Then
' A列が変わったらB列に「長さ」を書く例
Dim c As Range
For Each c In Intersect(Target, TargetSheet.Range("A2:A1000"))
TargetSheet.Cells(c.Row, "B").Value = Len(CStr(c.Value))
Next
End If
End Sub
VB初期化(標準モジュール)
Dim watcher As CSheetWatcher
Sub InitWatcher()
Set watcher = New CSheetWatcher
Set watcher.TargetSheet = Worksheets("Input")
MsgBox "監視を開始しました。"
End Sub
VB- 重要深掘り:
- イベントをクラスに閉じ込める: ロジックが散らばるのを防ぎ、切り替えや停止も容易。
- WithEventsは参照が必須: グローバル(モジュールレベル)に保持し、
Setで対象を割り当てる。
ビジネスルールの独立:ルールクラスで切り替え容易
判定ルールはコード直書きせず、クラス化すると差し替えが簡単。
クラスモジュール(名前: CScoreRule)
' CScoreRule.cls
Option Explicit
Private pThreshold As Double
Public Property Let Threshold(ByVal v As Double)
pThreshold = v
End Property
Public Function IsPass(ByVal score As Double) As Boolean
IsPass = (score >= pThreshold)
End Function
VB利用(標準モジュール)
Sub Example_Rule()
Dim rule As New CScoreRule
rule.Threshold = 70
Debug.Print rule.IsPass(75) ' True
rule.Threshold = 80
Debug.Print rule.IsPass(75) ' False
End Sub
VB- 重要深掘り:
- ルールは”差し替え”が本質: 将来「重み付き」「係数」などに変更しても、ルールクラスだけ差し替え。
- テスト容易: ルール単体の挙動を検証できる。
ファクトリ(生成役)で初期化の複雑さを隠す
クラスモジュール(名前: CEmployeeFactory)
' CEmployeeFactory.cls
Option Explicit
Public Function FromRow(ByVal ws As Worksheet, ByVal r As Long) As CEmployee
Dim e As New CEmployee
e.EmpNo = CStr(ws.Cells(r, "A").Value)
e.Name = CStr(ws.Cells(r, "B").Value)
e.Dept = CStr(ws.Cells(r, "C").Value)
If IsDate(ws.Cells(r, "D").Value) Then e.HireDate = CDate(ws.Cells(r, "D").Value)
Set FromRow = e
End Function
VB- 重要深掘り:
- 「どう作るか」を一本化: 生成手順を集中管理して、散らばる初期化を撲滅。
- 呼び出し側はシンプル:
Set e = factory.FromRow(ws, r)の一行で生成。
例題で練習(貼って試せる)
- 例1(基本クラス): CEmployeeを作成→Example_EmployeeClassでプロパティとメソッドを体験。
- 例2(リスト): CEmployeeListで複数管理→Outputに一括書き出し。
- 例3(リーダー): CEmployeeReaderでInputから読み込み→Findで検索。
- 例4(イベント): CSheetWatcherを初期化→Input!A列変更でB列へ自動反映。
- 例5(ルール): CScoreRuleのしきい値変更→合否判定が切り替わる。
実務の落とし穴と対策(ここが肝)
- 落とし穴1:クラスを作っても”責務”が曖昧
- 対策: 1クラス1責務。データモデル、リスト、読み書き、ルール、イベントの役割を分ける。
- 落とし穴2:Range操作をクラスに混ぜる
- 対策: 読み書きはReader/Writer/Factoryへ。モデルクラスはセルを知らない。
- 落とし穴3:初期化/後片付け漏れ
- 対策:
Class_Initialize/Class_Terminateで必ず準備・解放を管理(ファイル・接続など)。
- 対策:
- 落とし穴4:WithEventsの参照切れ
- 対策: モジュールレベルで保持し、対象割り当てを明示。
Set Nothingで停止を管理。
- 対策: モジュールレベルで保持し、対象割り当てを明示。
- 落とし穴5:使い回しができない設計
- 対策: 入出力を値/配列ベースに。業務ロジックは純粋関数的に。
スターター手順(最短導入)
- データモデルをクラス化(CEmployeeのように状態+検証+簡単な振る舞い)。
- リストクラスで複数管理(検索・配列変換・件数)。
- Reader/Factoryで読み込みを分離(列マッピングをここに集約)。
- WithEventsクラスでイベントを閉じ込める(監視・トリガー)。
- ビジネスルールをクラス化(しきい値・判定などを差し替え可能に)。
