はじめに ―― 請求業務の自動化から学ぶ「クラス」の出番
VBAでのクラスモジュールですが、一番の悩みどころは「どういうときにクラスを作ればいいの?」という点です。そして、それと同時に「クラスを作るとしても、何から手を付ければいいのかわからない」と戸惑う人もいます。
この悩みは、初学者だけでなく本職のIT技術者でも多数見られます。この壁にぶつかる理由は、「クラス設計はプログラミングとは関係がない」という本質を知らないからです。
プログラミングの本ではクラスについて、プログラミング言語として説明を中心に書かれています。メソッドやプロパティなどの説明です。しかし、それらはただのプログラミング言語仕様の説明に過ぎません。どういうときにクラスを作ればよいのか、どういうクラスがよいのか、というのはプログラミング言語とは関係がないのです。
どういうクラスを作ればいいのか、というクラス設計は、プログラミングスキルとは別の知識になります。「クラス設計スキル」と言っても構いません。
この記事では、Excelでよく行われる請求業務を例にして、「こういうときにクラスを作るとよい」という考え方と、さらに、どういう順序でクラスの構成を決めればよいか、という考え方を紹介します。
まず最初に、業務を整理する
クラスを作るかどうかの前に、「どういう目的のプログラムを作るのか」を明確にします。これは、実際にどういう業務を行うのかを列挙することから始めます。
例としてExcelが得意な会計処理を題材にします。例えば、以下のような請求書関連の業務を行っているとします。
- 請求先ごとにExcelブックに請求書を保存する。
- 請求書を作成したかチェックする。
これを言葉ごとに分解します。「請求先ごと」「Excelブック」「請求書を保存」「請求書の作成チェック」の4つです。
これらを細かく見ていくと、以下のようになるとします。
- 「請求先ごと」:複数の請求先を管理する必要がある。
- 「Excelブック」:請求書フォーマットには「請求先名」「金額」「支払期日」などが含まれる。
- 「請求書を保存」:請求書ファイルは、請求書IDと請求先名と日付を含むファイル名で保存している。
- 「請求書作成チェック」:請求書を作成しているか確認する。
このような手順で、業務の構成を整理することが出来ます。
どういうときにクラスを作るとよいのか?
ここまでの流れで、ある程度業務を整理できました。
次に、「業務の中心となるデータ」、「データの現在の状態」、「データを扱う処理」があるかを確認します。先の業務の細分化した内容から、以下のように分類できます。
- 「業務の中心となるデータ」は、請求書自体と請求書に記載する金額や支払期日です。
- 「データの現在の状態」は、請求書の作成状況(作成済みか未作成かなど)の状態です。
- 「データを扱う処理」は、請求書Excelブックの作成と、請求書作成チェックです。
ここまでの段階で、データ、状態、処理が明確になっていれば、「クラスを作る」と判断できます。もしこれらの「データ・状態・処理」のいずれかがはっきりしない・無いのであれば、クラス化せずに標準モジュールに通常の関数で書いてしまった方が簡単な場合が多いです。
この請求書関連業務は「データ、状態、処理」が明確になっているため「クラスを作るとよい」と判断できます。
クラスの構成をどう考えればよいのか?
では、先に挙げた「データ、状態、処理」の内容をクラス化します。
一覧にすると、次のようになります。
要素 | 内容 |
---|---|
データ | 請求書ID、請求先名、金額、支払期日 |
状態 | 作成済/未作成 |
処理 | 請求書の作成、状態の更新 |
これをクラスにするとこのような構成になります。
請求書(1件分のデータ):※後述のコードでは請求書の各項目は個別の変数としてローカル変数で定義しています。
├─ 請求書ID
├─ 請求先名
├─ 金額
├─ 支払期日
└─ 作成状態(済/未)
請求書管理クラス(複数の請求書を管理)
├─ 請求書リスト(請求書IDをキーとしたディクショナリ)
├─ メソッド
│ ├─ 請求書IDリスト取得()
│ ├─ 請求書作成(請求書ID)
│ └─ 全請求書作成()
└─ プロパティ
├─ 請求書作成状態取得(請求書ID)
└─ 請求書作成状態更新(請求書ID, 状態)
各構成要素の説明
請求先データ(請求書IDをキーとして、複数の請求書を持つデータ)
請求先ごとの情報を、キー(請求書ID)でアクセスできるように格納します。中身はDictionaryです。
Dictionaryとは、キーと値のペアでデータを扱えるコレクションです。Dictionaryについての詳細は「VBAのDictionaryの使い方(全メソッドとプロパティ網羅)」をご参照ください。
メソッド(動作)
- 請求書IDリスト取得:現在登録されているすべての請求書IDを返します。
- 請求書作成(請求書ID:文字列):指定された請求書IDの請求書を作成します。
- 全請求書作成:登録されているすべての請求書を出力します。
プロパティ(状態の管理)
- 請求書作成状態取得(請求書ID:文字列):指定された請求書の作成状態を取得します。
- 請求書作成状態更新(請求書ID:文字列, 状態:Boolean):作成状態を「作成済(True)」「未作成(False)」に更新します。
業務からクラス設計を行うと、何が良くなるのか?
上のように業務内容からクラス設計までを導き出すと、業務の流れに沿ったままのプログラミングが書けるようになります。
分かりやすいように、上に書いた請求書管理クラスの日本語名のままコードにすると以下のようになります。”CLT001″は仮の請求書IDです。
1 2 3 4 5 6 |
'// 請求書が未作成の場合 If 請求書管理クラス.請求書作成状態取得("CLT001") = "未作成" Then '// 請求書を作成して、作成済みとする Call 請求書管理クラス.請求書作成("CLT001") Call 請求書管理クラス.請求書作成状態更新("CLT001", "作成済") End If |
このコードは、業務担当者が普段から行っている「請求書を作成し、状態を更新する」という作業をそのまま書けています。各プロパティやメソッドの内部処理は、ファイル操作を行ったり、ブックの保存処理を行ったり、といったプログラミングが要求されますが、その部分は業務の流れとは切り離されているため、クラスの中に閉じた状態にできるため、呼び出し側のコードは業務部分のロジックに専念できることになります。
このように、クラスを使って業務ロジックを明確に分離することで、可読性や保守性の高いプログラムになります。
クラスは「業務そのもの」を表現する道具
ここまでの業務の分析からクラス設計までの流れから分かることが、クラスとは単なる「便利機能」ではなく、「業務やデータを表現する道具」だということです。
上の流れで行っているのは「システム分析」や「データ分析」といった言葉で表現されることがありますが、そういう言葉を知らなくても、クラス設計は可能です。そのため、初学者やプログラマーでなくても、クラスモジュールを使ってクラスを作ることは十分可能です。
また、このサイトはExcelVBAを取り扱っていますが、ここで書いているクラス設計の考え方はどのようなプログラミング言語のクラス設計にも通用する「クラス設計スキル」です。本職プログラマーの人でもプログラミング言語については習熟していても、「クラスは業務を細分化して設計していく」ということを知らず、どうやってクラス化すればいいのか分からない、と悩む人は少なくありません。
VBAのクラスモジュールには継承や多態性などの一般的なオブジェクト指向の機能は備わっていないため軽視されることもありますが、クラス設計の本質はそこではありません。継承などはクラス設計の本質的なものではなく、あると便利な機能、というものです。
実際にクラスモジュールに書いた場合のコード例
上の請求書管理クラスを実際にクラスモジュールに書くと以下のような感じになります。
クラスモジュールはInvoiceManager.clsとして作成しています。それを呼び出すテストプログラムを標準モジュールにTestInvoiceManagerとして用意しています。
ここで大事なことは、クラスモジュールを使う側の標準モジュールのコードが、業務で行っている内容の通りの流れのプログラムになっている点です。プロパティやメソッドが複数あり、データや状態もしっかりあるためクラスモジュールのコード自体はそれなりの分量になっていますが、呼び出す側の標準モジュールのテストコードは業務で何を行っているのかを書いているだけです。
なお、コード内でDictionaryを使っていますが、Dictionaryを使う場合は事前にVBA画面のツールメニュー→参照設定を選び、参照設定ダイアログで「Microsoft Scripting Runtime」にチェックを付けます。
Dictionaryの詳細については「VBAのDictionaryの使い方(全メソッドとプロパティ網羅)」をご参照ください。
なお、今回の記事では1つのクラスでの説明を優先させたいためあえて構造体を文字列に変換してDictionaryに格納していますが、より厳密な設計を行いたい場合は構造体をクラスに変更してDictionaryの値として扱う実装もあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
'// InvoiceManager.cls Option Explicit '// 請求書データ構造体 Private Type InvoiceData id As String '// 請求書ID ClientName As String '// 請求先名 Amount As Currency '// 金額 DueDate As Date '// 支払期日 Created As Boolean '// 請求書作成状態(True:作成済み、False:未作成) End Type '// 請求書データコレクション(キー:請求書ID、値:請求書データ構造体) Private Invoices As Scripting.Dictionary '// クラス初期化 Private Sub Class_Initialize() '// 請求書データの領域確保 Set Invoices = New Scripting.Dictionary End Sub '// 請求書を追加 Public Sub AddInvoice(ByVal id As String, ByVal ClientName As String, ByVal Amount As Currency, ByVal DueDate As Date) Dim inv As InvoiceData '// 請求書データ構造体 '// 請求書データの各項目に引数値をセット inv.id = id inv.ClientName = ClientName inv.Amount = Amount inv.DueDate = DueDate inv.Created = False '// 請求書データコレクションに追加 Call Invoices.Add(id, inv.id & vbTab & inv.ClientName & vbTab & CStr(inv.Amount) & vbTab & CStr(inv.DueDate) & vbTab & CStr(inv.Created)) End Sub '// 請求書IDリスト取得 Public Function GetInvoiceIDs() As Variant '// 請求書IDのリストを取得 GetInvoiceIDs = Invoices.Keys End Function '// 請求書作成 Public Sub CreateInvoice(ByVal id As String) '// 請求書IDが存在しない場合は請求書作成を行わない If Not Invoices.Exists(id) Then Exit Sub End If Dim inv As InvoiceData '// 請求書データ構造体 '// 請求書データコレクションから引数の請求書IDに一致する請求書データを取得 Dim data As String data = Invoices(id) Dim v v = Split(data, vbTab) inv.id = v(0) inv.ClientName = v(1) inv.Amount = Val(v(2)) inv.DueDate = CDate(v(3)) inv.Created = CBool(v(4)) '// ここで実際の請求書(Excelファイルなど)を作成する処理を入れる Debug.Print "請求書作成: " & id & ", " & inv.ClientName & ", " & Format(inv.Amount, "Currency") & ", 支払期日: " & inv.DueDate '// 請求書作成状態=作成済み inv.Created = True '// 請求書データコレクションの対象請求書IDの請求書データを更新 data = inv.id & vbTab & inv.ClientName & vbTab & CStr(inv.Amount) & vbTab & CStr(inv.DueDate) & vbTab & CStr(inv.Created) Invoices(id) = data End Sub '// 全請求書作成 Public Sub CreateAllInvoices() Dim id As Variant '// 請求書ID '// 請求書データコレクションの請求書ID単位でループ For Each id In Invoices.Keys '// 請求書の作成を行う Call CreateInvoice(id) Next End Sub '// 請求書作成状態取得 Public Function GetInvoiceCreatedStatus(ByVal id As String) As Boolean '// 引数の請求書IDが請求書データコレクションに存在しない場合 If Not Invoices.Exists(id) Then '// 請求書作成状態取得は未作成として返す GetInvoiceCreatedStatus = False Exit Function End If '// 請求書作成状態取得は請求書データコレクションの対象請求書IDの請求書作成状態を返す Dim data As String data = Invoices(id) Dim v v = Split(data, vbTab) Dim inv As InvoiceData '// 請求書データ構造体 inv.Created = CBool(v(4)) GetInvoiceCreatedStatus = inv.Created End Function '// 請求書作成状態を更新 Public Sub SetInvoiceCreatedStatus(ByVal id As String, ByVal Status As Boolean) '// 引数の請求書IDが請求書データコレクションに存在しない場合 If Not Invoices.Exists(id) Then Exit Sub End If '// 請求書作成状態に引数の状態をセットして、請求書データコレクションを更新 Dim inv As InvoiceData Dim data As String data = Invoices(id) Dim v v = Split(data, vbTab) inv.id = v(0) inv.ClientName = v(1) inv.Amount = Val(v(2)) inv.DueDate = CDate(v(3)) inv.Created = Status data = inv.id & vbTab & inv.ClientName & vbTab & CStr(inv.Amount) & vbTab & CStr(inv.DueDate) & vbTab & CStr(inv.Created) Invoices(id) = data End Sub |
標準モジュールでのテストコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
Option Explicit Sub TestInvoiceManager() Dim mgr As InvoiceManager Set mgr = New InvoiceManager '// 請求書データを追加 Call mgr.AddInvoice("CLT001", "ABC商事", 120000, DateSerial(2025, 7, 10)) Call mgr.AddInvoice("CLT002", "XYZ株式会社", 95000, DateSerial(2025, 7, 15)) Dim id For Each id In mgr.GetInvoiceIDs '// 請求書未作成の場合 If mgr.GetInvoiceCreatedStatus(id) = False Then '// 請求書を作成する Call mgr.CreateInvoice(id) '// 請求書作成済みにする Call mgr.SetInvoiceCreatedStatus(id, True) End If Next End Sub |
まとめ
クラスの設計は、まずは業務の分析から始まります。この分析に漏れがあると、当然クラス設計にも漏れが発生します。
ただ、業務分析結果がそのままクラスに反映されるため、しっかり業務分析とクラス設計を適切に行っておけば、設計段階とプログラミング段階でのズレが少なくなり、早い段階での修正が可能になり、また、後戻りのリスクを減らすことができる利点があります。
VBAではクラスモジュールと標準モジュールで扱いが明確に異なります。標準モジュールをクラスにしようとしてもできません。またクラスモジュールを標準モジュールのように使おうと思ってもできません。このような制約は他のプログラミング言語には無いVBAの利点です。
今後クラスモジュールを使うことが出てきたときには、このVBAの特性を活かして、よいクラス設計をしていただければと思います。