VBAでカプセル化のミスがスタック領域の不足を起こした件

# あらすじ

VBAのコードを書いているとき、カプセル化のためのコードで、
意図せぬ無限再帰をしてしまい、
エラーメッセージ「スタック領域が不足しています。」を
発生させてしまいました。
いわゆるスタックオーバーフローです。

# スタック領域

今回のお題はスタックオーバーフローです。
「スタック領域が不足しています。」と言われたら、
８割の確率で、再帰呼び出しによるスタックオーバーフローを疑っていいと思います。

# 本来のコード

まず、変数`myName`を`Private`にして外部からアクセスできなくしています。
これがカプセル化です。
次に、`Property Get`で`myName`を文章に当てはめて取得できるようにしています。
`myName`に値を登録する場合には`Property Let`を使えるようにしています。

```vb:NameRemember.cls
Private myName As String

Public Property Get GetName() As String
    GetName = "I am " + myName + "."
End Property

Public Property Let SetName(ByVal newName As String)
    myName = newName
End Property
```

# 誤ったコード

以下の誤ったコードでは、
`Property Let`の中で、自分自身に代入しているため、
無限に再帰呼び出しが行われます。

```vb:NameRemember.cls
Private myName As String

Public Property Get GetName() As String
    GetName = "I am " + myName + "."
End Property

Public Property Let Name(ByVal newName As String)
    Name = newName '←ここがミス
End Property
```

# なぜ起こしてしまったのか

意図的に、以下のような再帰呼び出しの無限ループを起こすことも可能です。

```vb:LoopRecurse.bas
Sub Recursion(i)
    i = i + 1
    Call Recursion(i)
End Sub
```

ご覧の通り、上記のサンプルコードは**関数**の無限再帰です。
プロパティで同じことが起きる可能性を警戒していませんでした。

Name、myName、newNameなど、似た名前ばかりなため、
テキトーに書いて直すのを忘れたんですね。
まだまだ経験不足だからかもしれません。

# 対策

とはいえ、経験不足というのは言い訳です。
解決方法を考えましょう。

根本的な発生対策は思いつきません。
もし知っている、思いつく人がいれば、
コメント欄で伝えて下さるとLGTMします。
というかフォローします。

### 取り合えずの対策

細かい低いテストコードを書いて早めに気が付く方法があります。
このテストコードには、[RubberDuck](https://rubberduckvba.com/)を使用しています。

```vb:test.bas
Option Explicit
Option Private Module

'@TestModule
'@Folder("Tests")

Private Assert As New Rubberduck.AssertClass
Private Fakes As New Rubberduck.FakesProvider

'@ModuleInitialize
Private Sub ModuleInitialize()
    'this method runs once per module.
    Set Assert = New Rubberduck.AssertClass
    Set Fakes = New Rubberduck.FakesProvider
End Sub

'@ModuleCleanup
Private Sub ModuleCleanup()
    'this method runs once per module.
    Set Assert = Nothing
    Set Fakes = Nothing
End Sub

'@TestInitialize
Private Sub TestInitialize()
    'This method runs before every test in the module..
End Sub

'@TestCleanup
Private Sub TestCleanup()
    'this method runs after every test in the module.
End Sub

'@TestMethod("Small")
Private Sub TestSetName()
    On Error GoTo TestFail
    'Arrange:
    Dim HumanName As NameRemember
    'Act:
    HumanName.SetName("GOD")

    'Assert:
    Assert.Succeed

TestExit:
    Exit Sub
TestFail:
    Assert.fail "Test raised an error: #" & Err.Number & " - " & Err.Description

End Sub

'@TestMethod("Small")
Private Sub TestGetName()
    On Error GoTo TestFail
    'Arrange:
    Dim HumanName As NameRemember
    Dim TestName As String
    'Act:
    TestName = "GOD"
    HumanName.SetName(TestName)

    'Assert:
    Assert.isEqual HumanName.GetName, "I am " + TestName + "."

TestExit:
    Exit Sub
TestFail:
    Assert.fail "Test raised an error: #" & Err.Number & " - " & Err.Description

End Sub
```

動作として、`Sub TestSetName`は`SetName`が最後まで正常に動作するかを確認しています。
また、`Sub TestGetName`は`GetName`が正常な値を返すかを確認しています。

この方法ではミスを見つけることはできますが、
未然に防ぐことはできません。

### コードスニペット

vscodeなどには、コードスニペットやEmmetといった、
テンプレートを自動挿入してくれる機能が付いています。

これらを使用すると、クラスモジュール内で`Private myName As String`を書いている段階で`Property Get`と`Property Let`の構文が自動挿入されたりします。

以下のサンプルコードのようなレベルが、
１行目を書いた直後に自動挿入されれば、
無限再帰は起こりえないでしょう。

```vb:NameRemember.cls
Private my_Name As String

Public Property Get Get_Name() As String
    Get_Name = my_Name
End Property

Public Property Let Set_Name(ByVal input_Value As String)
    my_Name = input_Value
End Property
```

これらの自動化は、単にキー入力の数を減らすだけではなく、
今回のようなミスを事前に防ぐ効果もあるといえます。
しかし、現状VBE(Visual Basic Editor)にそのような機能はありません。

### 命名規則

また、SetName、GetNameなど、明らかにそれと分かる
命名規則にして習慣化するとミスが防げるかもしれません。

# まとめ

今のところ確実に未然防止する方法はありません。
スタックオーバーフローには気を付けましょう。

**Excelsior!**