はじめに
Excel VBA で「マクロ使っているよ」「VBA書いているよ」という方は少なくないものの、標準モジュールは使っていても「クラスはよく分からない」「標準モジュールで全部できるし」という方が多いように見受けられます。(筆者の観測範囲)
確かに「その通り!」と思いますが、一方で「クラスって便利なんだよ!」「確かに標準モジュールだけでも出来るけど、すっきりとして読みやすいコードが書けるよ!」ということを伝えたいです。
そんなわけで、以前より、データをひとまとめにして扱う方法としてクラスを使う方法を紹介してきましたが、今回は、Let と Get の使いどころについて紹介したいと思います。
モジュールが変換を担当するパターン
これを次のようなクラスとして表現したいとします。Excel上のデータに加えて「年齢」と「社内電話か携帯電話か」を判断したいという要望があったのでプロパティを追加したとします。
Public Id As String ' ID
Public name As String ' 名前
Public Department As String ' 部署
Public Birthdate As Date ' 生年月日
Public TelephoneNumber As String ' 電話番号
Public Age As Long ' 年齢
Public TelephoneType As String ' 社内電話か携帯電話か
年齢は生年月日から判断できます。社内かケータイかの判断はここでは「0120-」で始まっていたら社内、そうでなければ携帯と判断することとします。
' 指定行の従業員データを取得する
Private Function GetEmployeeData(rowNum As Long) As EmployeeClass
Dim employeeData As EmployeeClass
With ThisWorkbook.Worksheets(EMPLOYEE_SHEET_NAME)
Set employeeData = New EmployeeClass
' ここで各セルから値を変数にセット
employeeData.Id = .Cells(rowNum, EMPLOYEE_ID_COLUMN_NUM)
employeeData.name = .Cells(rowNum, EMPLOYEE_NAME_COLUMN_NUM)
employeeData.Department = .Cells(rowNum, EMPLOYEE_DEPARTMENT_COLUMN_NUM)
employeeData.Birthdate = .Cells(rowNum, EMPLOYEE_BIRTHDATE_COLUMN_NUM)
employeeData.TelephoneNumber = .Cells(rowNum, EMPLOYEE_TELNUMBER_COLUMN_NUM)
' 携帯電話ならば Trueを返す
If IsCellPhoneNumber(employeeData.TelephoneNumber) Then
employeeData.TelephoneType = "携帯電話"
Else
employeeData.TelephoneType = "社内電話"
End If
' 生年月日と現在時刻から年齢を判断
employeeData.Age = DateDiff("yyyy", CDate(employeeData.Birthdate), Now)
End With
Set GetEmployeeData = employeeData
End Function
' ケータイ番号か確認する
Private Function IsCellPhoneNumber(number As String) As Boolean
If InStr(1, number, "0120-") = 1 Then
IsCellPhoneNumber = False
Else
IsCellPhoneNumber = True
End If
End Function
懸念点と解決策
上記のソースでやりたいことは実現できています。正しく動作します。
しかしちょっと煩雑だと感じてしまいます。ただでさえ.Cells()が続いて長いのに、if文まで出てきてややこしいです。
またこうも思います。「いやいや、生年月日が分かるなら年齢ぐらいEmployeeClassさん自身で判断してくださいよ。なぜ読み込み時にモジュールさんに変換をやらせているんですか?」と。
employeeData.Birthdate = .Cells(rowNum, EMPLOYEE_BIRTHDATE_COLUMN_NUM)
employeeData.TelephoneNumber = .Cells(rowNum, EMPLOYEE_TELNUMBER_COLUMN_NUM)
ここの Birthdate とTelephoneNumber を入力するだけでAge とTelephoneType にも自動で入ってほしいと思います。つまりその計算をEmployeeClass自身が担当するようにするのです。そう役割分担すれば、EmployeeClassを使う側がその計算を意識しないで済みます。
その役割分担をLetとGetで実現します。
LetとGetの使い方
さて、ここでまずは Let と Get の使い方を書いてみます。
生年月日を示すBirthdateをLetとGetを使ったパターンに置き換えます。
Public Birthdate As Date ' 生年月日
Private myBirthdate As Date ' 生年月日
'Birthdateの値を変更
Public Property Let Birthdate(ByVal inputValue As Date)
myBirthdate = inputValue
End Property
'Birthdateの値を取得
Public Property Get Birthdate() As Date
Birthdate = myBirthdate
End Property
一行で済むコードが十数行にまで膨れ上がっています。ややこしいですね…。
Letにて、入力値を内部変数 myBirthdate に保存して、Getにて、内部変数を myBirthdate から取得して外に出ていきます。イメージ的にはこんな感じ?
一行で済んでいたコードが十数行に膨らんでややこしく感じますが、こうすることで、入力時と出力時に動作を追加できるようになります。
LetとGetの使い所
入力時と出力時に動作を追加できるようになると、クラスに役割を追加できるようになります。例えば、Birthdate入力時に年齢を計算して保存するのは次のようになります。
Private myBirthdate As Date ' 生年月日
Public Age As Long ' 年齢
'Birthdateの値を変更
Public Property Let Birthdate(ByVal inputValue As Date)
myBirthdate = inputValue
' 生年月日と現在時刻から年齢を判断
Age = DateDiff("yyyy", CDate(inputValue), Now)
End Property
'Birthdateの値を取得
Public Property Get Birthdate() As Date
Birthdate = myBirthdate
End Property
これで Let を呼び出したときに、Ageプロパティが変更されます。
Ageプロパティの問題点
しかし、Ageは 現在 Public なので、外から変更できてしまいます。たとえば生年月日を1990/4/1 と入力して、せっかく年齢が29歳と入っているのに、その後10歳に上書きする、というようなことが出来てしまいます。
これはバグを生む要因になりますから、Ageは読み取りは出来ても書き込みは出来ない Read Onlyにしたいプロパティです。
読み取り専用プロパティ、書き込み専用プロパティを実現
Ageプロパティを読み取り専用にしたいのですが、これを実現するためにいったんAgeもLet と Getの方に置き換えます。
Private myBirthdate As Date ' 生年月日
Private myAge As Long ' 年齢
'Birthdateの値を変更
Public Property Let Birthdate(ByVal inputValue As Date)
myBirthdate = inputValue
' 生年月日と現在時刻から年齢を判断
' Age = DateDiff("yyyy", CDate(inputValue), Now) を次のように置き換え
myAge = DateDiff("yyyy", CDate(inputValue), Now)
End Property
'Birthdateの値を取得
Public Property Get Birthdate() As Date
Birthdate = myBirthdate
End Property
'Ageの値を変更
Public Property Let Age(ByVal inputValue As Long)
myAge = inputValue
End Property
'Ageの値を取得
Public Property Get Age() As Long
Age = myAge
End Property
ここでAgeの値を変更するのは Letですから、Letを削除します。
Private myBirthdate As Date ' 生年月日
Private myAge As Long ' 年齢
'Birthdateの値を変更
Public Property Let Birthdate(ByVal inputValue As Date)
myBirthdate = inputValue
' 生年月日と現在時刻から年齢を判断
'Age = DateDiff("yyyy", CDate(inputValue), Now) を次のように置き換え
myAge = DateDiff("yyyy", CDate(inputValue), Now)
End Property
'Birthdateの値を取得
Public Property Get Birthdate() As Date
Birthdate = myBirthdate
End Property
'Ageの値を取得
Public Property Get Age() As Long
Age = myAge
End Property
ここでコンパイルをすると、Ageの書き込みは出来ないと怒られます。
これで Age を読み取り専用(Read Only)プロパティに出来ました。
なお、ここまで年齢は生年月日の入力時に判定していますが、これだとAgeプロパティを使用した時にその年齢が変わる可能性があります。Ageプロパティを使用したい時に年齢を判断したいパターンにしたいので、Let から Get の時に動作を変更します。
Private myBirthdate As Date ' 生年月日
'Birthdateの値を変更
Public Property Let Birthdate(ByVal inputValue As Date)
myBirthdate = inputValue
End Property
'Birthdateの値を取得
Public Property Get Birthdate() As Date
Birthdate = myBirthdate
End Property
'Ageの値を取得
Public Property Get Age() As Long
' 生年月日と現在時刻から年齢を判断
Age = DateDiff("yyyy", CDate(myBirthdate), Now)
End Property
ここで読み取り専用のプロパティが出来ましたが、逆に書き込み専用プロパティにしたい場合は、Getの方を削除します。
- 読み取り専用プロパティを実現するには、Getだけにする(Letを削除する)
- 書き込み専用プロパティを実現するには、Letだけにする(Getを削除する)
クラス内で変換を完結させるパターン
LetとGetを使用することで、プロパティの書き込み時や読み込み時に、自分の好きな動作をさせることができるようになりました。これを使用して、これまでモジュール内で行っていた作業を書き換えてみます。
電話番号関連もクラス内で完結させたいので、IsCellPhoneNumber() もクラス内に持ってきます。
Public Id As String ' ID
Public Name As String ' 名前
Public Department As String ' 部署
Private myBirthdate As Date ' 生年月日
Private myTelephoneNumber As String ' 電話番号
Private myTelephoneType As String ' 社内かケータイか
'Birthdateの値を変更
Public Property Let Birthdate(ByVal inputValue As Date)
myBirthdate = inputValue
End Property
'Birthdateの値を取得
Public Property Get Birthdate() As Date
Birthdate = myBirthdate
End Property
'Ageの値を取得
Public Property Get Age() As Long
' 生年月日と現在時刻から年齢を判断
Age = DateDiff("yyyy", CDate(myBirthdate), Now)
End Property
'TelephoneNumberの値を変更
Public Property Let TelephoneNumber(ByVal inputValue As String)
myTelephoneNumber = inputValue
' 携帯電話ならば Trueを返す
If IsCellPhoneNumber(myTelephoneNumber) Then
myTelephoneType = "携帯電話"
Else
myTelephoneType = "社内電話"
End If
End Property
'TelephoneNumberの値を取得
Public Property Get TelephoneNumber() As String
TelephoneNumber = myTelephoneNumber
End Property
'TelephoneTypeの値を取得
Public Property Get TelephoneType() As String
TelephoneType = myTelephoneType
End Property
' ケータイ番号か確認する
Private Function IsCellPhoneNumber(number As String) As Boolean
If InStr(1, number, "0120-") = 1 Then
IsCellPhoneNumber = False
Else
IsCellPhoneNumber = True
End If
End Function
こうすることによって、このEmployeeClassクラスを使用するときには、必要な箇所の入力だけで済み、すっきりと分かりやすいコードになります。
' 指定行の従業員データを取得する
Private Function GetEmployeeData(rowNum As Long) As EmployeeClass
Dim employeeData As EmployeeClass
With ThisWorkbook.Worksheets(EMPLOYEE_SHEET_NAME)
Set employeeData = New EmployeeClass
' ここで各セルから値を変数にセット
employeeData.Id = .Cells(rowNum, EMPLOYEE_ID_COLUMN_NUM)
employeeData.Name = .Cells(rowNum, EMPLOYEE_NAME_COLUMN_NUM)
employeeData.Department = .Cells(rowNum, EMPLOYEE_DEPARTMENT_COLUMN_NUM)
employeeData.Birthdate = .Cells(rowNum, EMPLOYEE_BIRTHDATE_COLUMN_NUM)
employeeData.TelephoneNumber = .Cells(rowNum, EMPLOYEE_TELNUMBER_COLUMN_NUM)
End With
Set GetEmployeeData = employeeData
End Function
まとめ
- モジュールでじゃなくてクラス内で完結させることで、すっきりとした読みやすいコードを書くことができます
- LetとGetを使うことで、書き込み時や読み込み時にいろんな操作が可能になります
- Getだけにして読み込み専用プロパティにしたり、Letだけにして書き込み専用プロパティにしたりできます