VBA クラスでの Let と Get の使いどころについて

# はじめに
　Excel VBA で「マクロ使っているよ」「VBA書いているよ」という方は少なくないものの、標準モジュールは使っていても「クラスはよく分からない」「標準モジュールで全部できるし」という方が多いように見受けられます。（筆者の観測範囲）
　確かに「その通り！」と思いますが、一方で「クラスって便利なんだよ！」「確かに標準モジュールだけでも出来るけど、すっきりとして読みやすいコードが書けるよ！」ということを伝えたいです。

　そんなわけで、以前より、[データをひとまとめにして扱う方法](https://qiita.com/saeki4n/items/12f3ccbafbbd50bba616)としてクラスを使う方法を紹介してきましたが、今回は、Let と Get の使いどころについて紹介したいと思います。

# モジュールが変換を担当するパターン
　次のようなExcelデータが存在するとします。
![従業員データ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/505df54d-627f-c415-4260-7e13397bd5aa.png)

　これを次のようなクラスとして表現したいとします。Excel上のデータに加えて「年齢」と「社内電話か携帯電話か」を判断したいという要望があったのでプロパティを追加したとします。

```vb:EmployeeClass.cls
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-」で始まっていたら社内、そうでなければ携帯と判断することとします。

```vb:EmployeeModuleのGetEmployeeDataを修正
' 指定行の従業員データを取得する
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さん自身で判断してくださいよ。なぜ読み込み時にモジュールさんに変換をやらせているんですか？」と。

```vb:
        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を使ったパターンに置き換えます。

```vb:Before
Public Birthdate        As Date     ' 生年月日
```
```vb: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

```
　一行で済むコードが十数行にまで膨れ上がっています。ややこしいですね…。
　Letにて、入力値を内部変数 myBirthdate に保存して、Getにて、内部変数を myBirthdate から取得して外に出ていきます。イメージ的にはこんな感じ？
![letget.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/d4c3b931-e368-b108-d098-5973a55a8997.png)

　一行で済んでいたコードが十数行に膨らんでややこしく感じますが、こうすることで、入力時と出力時に動作を追加できるようになります。

# LetとGetの使い所

　入力時と出力時に動作を追加できるようになると、クラスに役割を追加できるようになります。例えば、Birthdate入力時に年齢を計算して保存するのは次のようになります。

```vb:生年月日を入力したら年齢を自動で計算
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歳に上書きする、というようなことが出来てしまいます。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/1d745da6-bf4b-ae8b-a853-c017e2dd979e.png)
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/93e0b4f3-d3fd-8f1c-bfc8-7117612f693a.png)

　これはバグを生む要因になりますから、Ageは読み取りは出来ても書き込みは出来ない Read Onlyにしたいプロパティです。

## 読み取り専用プロパティ、書き込み専用プロパティを実現

　Ageプロパティを読み取り専用にしたいのですが、これを実現するためにいったんAgeもLet と Getの方に置き換えます。

```vb: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を削除します。

```vb: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の書き込みは出来ないと怒られます。
![getOnly.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/75c772a8-7a07-ab26-84e5-385f6b6d4f56.png)

　これで Age を読み取り専用(Read Only)プロパティに出来ました。
　なお、ここまで年齢は生年月日の入力時に判定していますが、これだとAgeプロパティを使用した時にその年齢が変わる可能性があります。Ageプロパティを使用したい時に年齢を判断したいパターンにしたいので、Let から Get の時に動作を変更します。

```vb:Age参照時に年齢を計算
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() もクラス内に持ってきます。

```vb:EmployeeClass.clsを修正
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クラスを使用するときには、必要な箇所の入力だけで済み、すっきりと分かりやすいコードになります。

```vb:EmployeeModuleのGetEmployeeDataを修正
' 指定行の従業員データを取得する
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だけにして書き込み専用プロパティにしたりできます

# 参考資料
[VBAで生年月日から年齢を求める](https://vbabeginner.net/vba%E3%81%A7%E7%94%9F%E5%B9%B4%E6%9C%88%E6%97%A5%E3%81%8B%E3%82%89%E5%B9%B4%E9%BD%A2%E3%82%92%E6%B1%82%E3%82%81%E3%82%8B/)