[VBA] クラスを利用するメリットと方法について & 簡単なサンプル(1)

概要

この記事について

VBAにおけるクラスっていまいちマイナーで使いにくい存在だと思っている。
理由はいくつかあって、

  • Javaなどと違ってクラスを使わなければ何も書けないという言語ではなく、
    書こうと思えばクラスを使わなくても手続き型で全て書けてしまう
  • クラスの持つ機能が後発の言語に比べて弱いので(後述)、あえて使うメリットが見出しにくい。
  • モジュールにPublicとして書けば何もせずとも呼び出せる関数でも、クラスの関数の場合はクラスのインスタンスを生成してからでないと利用できない。

ただ、
クラスを利用したほうがずっと安全で、保守性も高く、バグも少なくコーディングできる場面ももちろん存在するため、
この記事においてクラスを利用するメリットと方法についてを記したい。
また、クラスを利用した簡単なサンプルも合わせて記載したい。

説明のために作成したExcelファイルとソースコードはこちらでダウンロードできます。

クラスを利用する方法

クラスとは?

クラスとは、
利用するデータ(変数・定数)と、そのクラスが持つ処理内容(関数)をひとつの箱(クラスモジュール)にまとめたもの。

といっても、無作為に適当な変数や関数を一緒にまとめれば良いというものではなく、
そのクラスが指す一つの存在を定め、その存在に属するものとしての変数や関数でなければならない。

クラスの例

例えば、
ヒトという存在をクラスと定めれば、
ヒトは目、口、胴体などのデータ(変数)と、
走る、食べる、喋るなどの処理内容(関数)を持つ。

MSOffice風に言えば、
ExcelのRangeオブジェクトは
Addressプロパティ(参照範囲)やRowプロパティ(行の数値)、Valueプロパティ(セルの値)などの変数を持ち、
Selectメソッド(範囲にカーソルを合わせる)や、
AutoFitメソッド(範囲の行幅、列幅を自動調整する)などの関数を持つ、
典型的なクラスのオブジェクトである。

また、ある一定の機能を持つモノという抽象的な概念もクラスとしてまとめ得る。
例えば、ExcelのRangeオブジェクトを取込み、
表のように、罫線を引き、
ヘッダーの強調を行い、
小計列の合計をセルに書き込むという一連の処理を担当するモノが挙げられる。
これを、TableCreaterと命名しよう。

存在 変数・定数 関数
ヒト

胴体
走る
食べる
喋る
Excel
Rangeオブジェクト
Address
Row
Value
Select
AutoFit
TableCreater 対象のRangeオブジェクト
小計列の列番号
ヘッダーセルの色
罫線を引く
ヘッダーの強調のためのスタイル変更を行う
小計から合計を計算し出力する

クラスを利用するメリット

安全なコーディングができる

クラスに宣言する変数、定数は
基本的にはPrivateの適用範囲(スコープ)で宣言し、
クラス内部の関数から用いるため、
不用意な呼出しで値が影響を受けることを防ぐことができる(そもそも呼出しをするとコンパイルエラーになる)

また、外部から変数の値などを参照したい場合や変数の値を設定したい場合は、
それ専用の関数として、
Porperty Getステートメント、Porperty Let(変数の型がオブジェクトの場合はSet)ステートメントを用いた関数を実装し、
それを通して外部とやりとりを行うようにすればよい。
これを カプセル化 という。

例えば、次のようなパーセント格納変数とPorperty Let関数は、
パーセンテージとして0~100の値のみ許容し、それ以外の場合はNullを代入する。
このように、値の取得や設定の際になんらかのフィルタやチェックを行い、安全性を高めることができる。

 1Private percentVal As Variant
 2
 3'**************************
 4'*Setter
 5'**************************
 6Public Property Let percent(ByVal v As Long)
 7    If v < 0 Or 100 < v Then percentVal = Null: Exit Property
 8    percentVal = v
 9End Property
10

保守性の高いコーディングができる

クラスの関数が、引数や内部で宣言した変数や定数などのみを極力使用するように
設計しておけば、
他のクラスやモジュールの変数・関数を利用したり利用されたりする頻度が減り、
互いのコードの結合性を低くすることができる。
(これを 疎結合 という)。

そうすれば、関数の処理変更や新しい関数の追加などでの影響範囲の大部分を
クラス内に留めることができるため、
思わぬ影響で想定外の動作をするリスクを低減することができる。

また、あるクラスにはそのクラスの指す対象に関係したコードしか書かれないようにしておけば、コードの見通しが大きく改善し、誰が見ても分かりやすいコードが書けるようになる。

決まりきった処理を必ず実行するようにでき、コーディングのミスが減る

VBAのクラスには、クラスのオブジェクト生成・破棄時に必ず実行される関数がある。
生成時は初期化処理であるClass_Initialize()、
破棄時は終了処理であるClass_Terminate()が実行される。

クラス内部でOutlookのMailアイテムやAccessのRecordSetなどを扱う場合、
外部の関数でこのクラスのオブジェクトを生成した際や破棄した際に必ず行うべき決まりきった処理をClass_InitializeやClass_Terminateに記載しておけば、
コードを書く際にその処理を忘れたり間違った処理を書いたりすることを防ぐことができる。

例えば、

  • OutlookのMailアイテムを扱うクラスの場合は、
    クラスのオブジェクトの生成時にOutlookアプリケーションも同時に生成したいし、
    クラスのオブジェクトの破棄時に新規作成あるいは編集したMailアイテムをDisplayメソッドにより見える状態にしたい。
    もし可視化を忘れてしまうと、バックグラウンドで生成されたメールにユーザはおそらく気づかない。

  • AccessのRecordSetを扱うクラスの場合は、 クラスのオブジェクトの生成時にAccessデータベースとのコネクションを設定する処理を実行したい。
    また、クラスのオブジェクトの破棄時にRecordSetをCloseし、それ以上の接続が必要なければデータベースとの接続もCloseしても良いかもしれない(「そのデータで何をしたいか」に依存するけれども、一例として)。
    もしCloseを忘れてしまうと、ずっとそのデータベースにつなぎ続けることになり、不本意な結果を生むかもしれない。

※VBAのクラスの持つ機能は、後発の言語に比べて弱い※

ただし、VBAのクラスにも弱点がある。
VBAのクラス機能には残念ながら後発の言語に比べて癖があり、機能面でも後塵を拝する面が多少ある。

  • Class_Initializeは引数を持つことができない。
    そのため、生成時に同時にクラス変数に値を代入したりすることができない。
  • クラスの継承という概念が無い(継承については→→→ [1] [2]
  • staticな変数や関数を持つことができない。
    すなわち、クラスに記述した変数や関数は、すべて、一度クラスをオブジェクト化しないと利用できない。

これらのような不便さがあるのが弱点だと思う。

VBAって今後もメンテされたり機能拡張されたりするんでしょうかね?
Microsoftは、より機能の高いPythonやJavascriptをもって、
Officeの搭載言語をVBから入れ替えるみたいな話も小耳に挟んだので
なんとも言えないですが。。。

ともあれ、VBAはオブジェクト指向を理解する入り口としては優れたツールかと思っています。

VBAにおけるクラスの利用方法の一例

作成環境

Windows10
MSOffice 2016

i. VBEにおいてクラスモジュールを作成する

上記のTableCreaterの作成を例にしたいと思う。

VBE(VBAの開発画面)において
挿入タブからクラスモジュールを選択し、
作成する。

1Option Explicit
2
3'**************************
4'*TableCreater
5'**************************
6

ii. 変数・定数を宣言する

変数として、
対象のRangeオブジェクト
小計列の列番号
ヘッダーセルの色
を宣言する。

同時にProperty Let関数とProperty Set関数を記述する。

 1Option Explicit
 2
 3'**************************
 4'*TableCreater
 5'**************************
 6
 7
 8'定数
 9Private Const HEADER_COLOR = 15917529 'ヘッダーセルの色
10
11'変数
12Private myRange As Range '範囲
13Private myColumnSubTotal As Long '小計列番号
14
15
16'******************************************************************************************
17'*getter/setter
18'******************************************************************************************
19
20
21Public Property Set Range(ByVal pRng As Range)
22    Set myRange = pRng
23End Property
24
25
26Public Property Let ColumnSubTotal(ByVal num As Long)
27    '範囲変数が設定されていない状態で呼出は禁止
28    If myRange Is Nothing Then Err.Raise 1000, , "範囲が設定されていません。"
29    '引数が範囲オブジェクトの列番号より外の列の番号であればエラー
30    If num < myRange.EntireColumn(1).Column Or myRange.EntireColumn(myRange.EntireColumn.Count).Column < num Then Err.Raise 1001, , "列番号の指定が不正です。"
31    '値を設定
32    myColumnSubTotal = num
33End Property
34
35

iii. Class_Initialize、Class_Terminateを記述する

通常はここで初期化処理・終了処理のClass_Initialize、Class_Terminateを記述する。

ただ、TableCreaterクラスでは特にやることはない。

 1Option Explicit
 2
 3'**************************
 4'*TableCreater
 5'**************************
 6
 7
 8'~~略~~
 9
10'******************************************************************************************
11'*関数名    :Class_Initialize
12'*機能      :
13'*引数      :
14'******************************************************************************************
15Private Sub Class_Initialize()
16    
17    '定数
18    Const FUNC_NAME As String = "Class_Initialize"
19    
20    '変数
21    
22    On Error GoTo ErrorHandler
23    
24    '本クラスでは特にやることはない。
25
26ExitHandler:
27
28    Exit Sub
29    
30ErrorHandler:
31
32    MsgBox "エラーが発生したため、マクロを終了します。" & _
33           vbLf & _
34           "関数名:" & FUNC_NAME & _
35           vbLf & _
36           "エラー番号:" & Err.Number & vbNewLine & _
37           Err.Description, vbCritical, "TableCreater"
38        
39    GoTo ExitHandler
40        
41End Sub

iv. 関数を記述する

クラスが持つべきそれぞれの処理内容を一つずつの関数として記述する。

TableCreaterの持つべきクラスとしては以下のようになる

  • 罫線を引く
  • ヘッダーの強調のためのスタイル変更を行う
  • 小計から合計を計算し出力する
  1Option Explicit
  2
  3'**************************
  4'*TableCreater
  5'**************************
  6
  7
  8'~~略~~
  9
 10
 11'******************************************************************************************
 12'*関数名    :drawLines
 13'*機能      :罫線を引く
 14'*引数      :
 15'*戻り値    :True > 正常終了、False > 異常終了
 16'******************************************************************************************
 17Public Function drawLines() As Boolean
 18    
 19    '定数
 20    Const FUNC_NAME As String = "drawLines"
 21    
 22    '変数
 23    
 24    On Error GoTo ErrorHandler
 25
 26    drawLines = False
 27    
 28    '範囲変数が設定されていない状態で呼出は禁止
 29    If myRange Is Nothing Then Err.Raise 1000, , "範囲が設定されていません。"
 30    
 31    '罫線を引く
 32    myRange.Borders.LineStyle = xlContinuous
 33
 34    drawLines = True
 35    
 36ExitHandler:
 37
 38    Exit Function
 39    
 40ErrorHandler:
 41
 42    MsgBox "エラーが発生したため、マクロを終了します。" & _
 43           vbLf & _
 44           "関数名:" & FUNC_NAME & _
 45           vbLf & _
 46           "エラー番号:" & Err.Number & vbNewLine & _
 47           Err.Description, vbCritical, "TableCreater"
 48        
 49    GoTo ExitHandler
 50        
 51End Function
 52
 53
 54'******************************************************************************************
 55'*関数名    :setStyleForHeader
 56'*機能      :ヘッダーの強調のためのスタイル変更を行う
 57'               ヘッダーは与えられた範囲の一行目のセルとする
 58'*引数      :
 59'*戻り値    :True > 正常終了、False > 異常終了
 60'******************************************************************************************
 61Public Function setStyleForHeader() As Boolean
 62    
 63    '定数
 64    Const FUNC_NAME As String = "setStyleForHeader"
 65    
 66    '変数
 67    
 68    On Error GoTo ErrorHandler
 69
 70    setStyleForHeader = False
 71    
 72    '範囲変数が設定されていない状態で呼出は禁止
 73    If myRange Is Nothing Then Err.Raise 1000, , "範囲が設定されていません。"
 74    
 75    'スタイル変更
 76    With myRange.Rows(1)
 77        '範囲の一行目のセルの色を変更
 78        .Interior.color = HEADER_COLOR
 79        '範囲の一行目を太字にする
 80        .Font.Bold = True
 81        '範囲の一行目を中央揃えにする
 82        .HorizontalAlignment = xlCenter
 83    End With
 84        
 85    
 86    setStyleForHeader = True
 87    
 88    
 89ExitHandler:
 90
 91    Exit Function
 92    
 93ErrorHandler:
 94
 95    MsgBox "エラーが発生したため、マクロを終了します。" & _
 96           vbLf & _
 97           "関数名:" & FUNC_NAME & _
 98           vbLf & _
 99           "エラー番号:" & Err.Number & vbNewLine & _
100           Err.Description, vbCritical, "TableCreater"
101        
102    GoTo ExitHandler
103        
104End Function
105
106
107
108
109'******************************************************************************************
110'*関数名    :calcTotalFromSubTotal
111'*機能      :小計列から合計を計算し出力する
112'*引数      :
113'*戻り値    :True > 正常終了、False > 異常終了
114'******************************************************************************************
115Public Function calcTotalFromSubTotal() As Boolean
116    
117    '定数
118    Const FUNC_NAME As String = "calcTotalFromSubTotal"
119    
120    '変数
121    Dim sumVal As Long
122    Dim cell As Range
123    Dim subTotalOrder As Long
124    
125    On Error GoTo ErrorHandler
126
127    calcTotalFromSubTotal = False
128    
129    '範囲変数が設定されていない状態で呼出は禁止
130    If myRange Is Nothing Then Err.Raise 1000, , "範囲が設定されていません。"
131    '小計列が設定されていない状態で呼出は禁止
132    If myColumnSubTotal = 0 Then Err.Raise 1002, , "小計列が設定されていません。"
133        
134    '小計列が範囲の列の何番目であるかを算出
135    subTotalOrder = myColumnSubTotal - myRange(1).Column + 1
136        
137    '合計値計算(ヘッダー列は含まない)
138    For Each cell In myRange.Columns(subTotalOrder).Cells.Offset(1).Resize(myRange.Columns(subTotalOrder).Cells.Offset(1).Cells.Count - 1)
139        '数値の場合のみ加算
140        If IsNumeric(cell.Value) Then sumVal = sumVal + cell.Value
141    Next cell
142    If sumVal = 0 Then GoTo TruePoint
143    
144    '合計値を小計列の最下端セルの下のセルに記入
145    With myRange.Columns(subTotalOrder).Rows(myRange.Columns(subTotalOrder).Cells.Count).Offset(1)
146        .Value = sumVal
147        '合計ラベルセル
148        With .Offset(, -1)
149            'ラベルを記入
150            .Value = "合計"
151            '合計ラベルセルと合計セルに罫線を引く
152            .Resize(, .Columns.Count + 1).Borders.LineStyle = xlContinuous
153        End With
154        
155    End With
156       
157TruePoint:
158       
159    calcTotalFromSubTotal = True
160    
161ExitHandler:
162
163    Exit Function
164    
165ErrorHandler:
166
167    MsgBox "エラーが発生したため、マクロを終了します。" & _
168           vbLf & _
169           "関数名:" & FUNC_NAME & _
170           vbLf & _
171           "エラー番号:" & Err.Number & vbNewLine & _
172           Err.Description, vbCritical, "TableCreater"
173        
174    GoTo ExitHandler
175        
176End Function
177

ここまででクラスのコーディングは完了。
データ、動作を併せ持つ「テーブルを作成するモノ」の雛形であるクラスができた。

v. 外部の関数からクラスを利用する。

このように2種類のテンプレの表データが記載されている「原本」シートが
ある。

原本

それぞれのテンプレデータをもとに、
新しいシートに表を作成するプログラムを作成した。
この関数はTableCreaterを用いてなるべくシンプルに記述している。

 1'******************************************************************************************
 2'*関数名    :TestTemplateA
 3'*機能      :原本のテンプレAについて、TableCreaterを用いて表を作成する
 4'               作成場所:新規シート
 5'*引数      :
 6'******************************************************************************************
 7Public Sub TestTemplateA()
 8    
 9    '定数
10    Const FUNC_NAME As String = "TestTemplateA"
11    
12    '変数
13    Dim ws As Worksheet
14    Dim objTableCreater As tableCreater
15    
16    On Error GoTo ErrorHandler
17    
18    With ThisWorkbook
19        '新規シート作成
20        Set ws = .Worksheets.Add(, .Worksheets(.Worksheets.Count))
21        ws.Name = "テンプレA_" & Format(Now, "yyyymmddhhnnss")
22        
23        '原本よりテンプレをコピー
24        ws.Range(ws.Cells(2, 2), ws.Cells(9, 4)).Value = .Worksheets("原本").Range(.Worksheets("原本").Cells(2, 2), .Worksheets("原本").Cells(9, 4)).Value
25        
26        'TableCreaterをオブジェクト化
27        Set objTableCreater = New tableCreater
28        '範囲と小計列を設定
29        Set objTableCreater.Range = ws.Range(ws.Cells(2, 2), ws.Cells(9, 4))
30        objTableCreater.ColumnSubTotal = 4
31        
32        '罫線を引く 異常終了時はExitHandler(終了処理)に移行
33        If Not objTableCreater.drawLines Then GoTo ExitHandler
34         
35        'ヘッダーの強調のためのスタイル変更を行う 異常終了時はExitHandler(終了処理)に移行
36        If Not objTableCreater.setStyleForHeader Then GoTo ExitHandler
37        
38        '小計から合計を計算 異常終了時はExitHandler(終了処理)に移行
39        If Not objTableCreater.calcTotalFromSubTotal Then GoTo ExitHandler
40        
41        '列幅の調整
42        ws.Range(ws.Cells(2, 2), ws.Cells(9, 4)).EntireColumn.AutoFit
43        
44    End With
45    
46
47ExitHandler:
48    
49    '変数を解放
50    Set objTableCreater = Nothing
51    Set ws = Nothing
52    
53    Exit Sub
54    
55ErrorHandler:
56
57    MsgBox "エラーが発生したため、マクロを終了します。" & _
58           vbLf & _
59           "関数名:" & FUNC_NAME & _
60           vbLf & _
61           "エラー番号:" & Err.Number & vbNewLine & _
62           Err.Description, vbCritical, "TableCreater"
63        
64    GoTo ExitHandler
65        
66End Sub

これを実行すると、このようになる。

テーブル作成後

vi. クラスの利用により得られたもの

TableCreaterを用いることで、
ある程度は見通しの良いコードになっているかと思う。

objTableCreater.で呼び出されるテキストを含むセンテンスは、
テーブル作成関連の意味を持つとわかる。

また、TableCreaterの関数は引数を必要とせず、
クラス内部の変数の利用で完結しているため、
コードを見たときにどんな動作をしているかが把握しやすいと思う。

終わりに

TableCreaterサンプルとソースコードについて

こちらのリンクをご参照ください。

記事の続き

TableCreaterの例だと、カプセル化の恩恵や初期化処理・終了処理を必ず実行できることの恩恵が十分には表現できなかったように思える。

続きの記事を作成しました。
[VBA] クラスを利用するメリットと方法について & 簡単なサンプル(2)

関連記事

comments powered by Disqus

Translations: