このシリーズでは、Excel VBAで本格的な将棋アプリケーションを開発する過程を通して、VBAにおけるオブジェクト指向プログラミングの様々なテクニックを解説しています。第6弾となる今回は、将棋の盤面操作において非常に頻繁に利用される「位置」を表すクラスを、VBAの強力な機能の一つである「デフォルトインスタンス」として定義し、そのメリットと実装方法、そして実務における活用術を深掘りします。この変更は、コードの簡潔性、可読性、そしてメンテナンス性を劇的に向上させるための重要な一歩となるでしょう。
概要
Excel将棋の開発において、駒の移動、盤面の状態判断、合法手判定など、あらゆる処理で「盤上の位置」を扱うことになります。通常、位置を表現するために、筋(列)と段(行)の2つのプロパティを持つ「位置クラス」を作成し、必要に応じて`Set objPos = New clsPos`のようにインスタンスを生成して利用します。しかし、この「位置」はアプリケーション全体で共通の概念であり、特定の操作中に一時的に利用されることが多いため、毎回インスタンスを生成・破棄するのは冗長になることがあります。
そこで本稿では、「clsPos(位置クラス)」をVBAの「デフォルトインスタンス」として定義することで、`New`キーワードを使わずにクラス名自体で直接プロパティやメソッドにアクセスできるように変更します。これにより、グローバル変数を減らしつつ、よりオブジェクト指向的なアプローチで、一時的な位置情報を扱うコードを劇的に簡潔に記述することが可能になります。これは、VBAにおけるオブジェクト指向プログラミングの理解を深め、より洗練されたコードを書くための重要なステップと言えるでしょう。
詳細解説
デフォルトインスタンスとは何か
VBAにおける「デフォルトインスタンス」とは、クラスモジュールに設定された特別なプロパティ`VB_PredeclaredId = True`によって実現される機能です。このプロパティを`True`に設定すると、VBAはそのクラスモジュールに対して、アプリケーション起動時、またはそのクラスのメンバー(プロパティやメソッド)に初めてアクセスされた時に、自動的に単一のインスタンスを生成し、そのインスタンスをクラス名自体で直接参照できるようにします。
具体的には、通常であれば`Set obj = New ClassName`のように明示的にインスタンスを生成してから`obj.Property`とアクセスするところ、デフォルトインスタンス化されたクラスでは、`ClassName.Property`のように、クラス名自体をオブジェクトインスタンスとして扱って直接アクセスできるようになります。これは、あたかもそのクラスがグローバルなオブジェクトであるかのように振る舞うことを意味します。
将棋の「位置クラス(clsPos)」にこれを適用するメリットは多大です。例えば、特定の駒が移動可能かチェックする際、移動元の位置と移動先の位置を一時的に格納し、その位置が盤面内であるか、他の駒と衝突しないかなどを判断する必要があります。従来であれば、以下のように記述するでしょう。
Dim fromPos As clsPos
Set fromPos = New clsPos
fromPos.Suji = 5
fromPos.Dan = 7
Dim toPos As clsPos
Set toPos = New clsPos
toPos.Suji = 5
toPos.Dan = 6
If fromPos.IsValid() And toPos.IsValid() Then
‘ 処理
End If
これをデフォルトインスタンス化した`clsPos`で、一時的な「現在の位置」や「参照位置」として利用すると、コードは次のように簡潔になります。(ただし、複数の位置情報を同時に保持する用途には向きません。後述の実務アドバイスを参照ください。)
‘ fromの位置を設定し、その位置に対する処理を行う
clsPos.Suji = 5
clsPos.Dan = 7
If clsPos.IsValid() Then
Debug.Print “移動元は有効な位置です。”
End If
‘ toの位置を設定し、その位置に対する処理を行う
clsPos.Suji = 5
clsPos.Dan = 6
If clsPos.IsValid() Then
Debug.Print “移動先は有効な位置です。”
End If
このように、`New`キーワードによるインスタンス生成の記述が不要になり、コードの記述量が減り、可読性が向上します。また、グローバル変数を定義することなく、アプリケーション全体で共有される「一時的な位置情報」を提供できるため、グローバル変数の乱用を防ぎ、よりオブジェクト指向的な設計を促進します。
実装上の注意点
デフォルトインスタンス化されたクラスを扱う際には、いくつかの重要な注意点があります。
1. **シングルトンではない**:
`VB_PredeclaredId = True`が設定されたクラスは、クラス名で直接アクセスできる単一のインスタンスがVBAによって自動的に管理されますが、これは厳密な意味でのシングルトンパターンとは異なります。なぜなら、開発者が明示的に`Set obj = New ClassName`と記述すれば、別の新しいインスタンスを生成することが可能だからです。デフォルトインスタンスはあくまで、クラス名で直接アクセスできる「既定のインスタンス」を提供するものです。したがって、複数の位置情報を同時に独立して保持したい場合は、依然として`New`キーワードを使って個別のインスタンスを生成する必要があります。デフォルトインスタンスは、あくまで「現在の操作対象となる一時的な位置情報」や「位置に関するユーティリティメソッドのコンテナ」として利用するのが適切です。
2. **`Class_Initialize`と`Class_Terminate`の挙動**:
デフォルトインスタンスの`Class_Initialize`イベントは、そのクラスのメンバーに初めてアクセスされた時、またはアプリケーション起動時にVBAが自動的にインスタンスを生成する際に一度だけ実行されます。その後、アプリケーションが終了するまで、このインスタンスはメモリに残り続けます。`Class_Terminate`イベントは、アプリケーションが終了する際に呼び出されます。これは、アプリケーション全体で共通して利用される初期設定やリソース解放のロジックを記述するのに適しています。
3. **プロパティとメソッドの設計**:
デフォルトインスタンスとして利用するクラスは、その性質上、グローバルな状態を持つことになります。そのため、プロパティやメソッドの設計には特に注意が必要です。状態を変更するプロパティやメソッドは、その変更がアプリケーション全体に影響を与える可能性があることを常に意識する必要があります。今回の`clsPos`のように、一時的な位置情報を設定・取得するユーティリティとして使う分には問題ありませんが、安易に複雑な状態を持たせると、予期せぬ副作用を生む可能性があります。
4. **デバッグの観点**:
デフォルトインスタンスは、`New`キーワードを使わないため、どこでインスタンスが生成されたのかがコード上では分かりにくいことがあります。デバッグ時には、`Class_Initialize`イベントにブレークポイントを設定することで、インスタンスが最初に生成されるタイミングを把握できます。
これらの点を理解した上で、デフォルトインスタンスを適切に活用することで、VBAアプリケーションの設計品質を向上させることができます。
サンプルコード
以下に、`clsPos`クラスをデフォルトインスタンスとして実装し、標準モジュールから利用する例を示します。
クラスモジュール: `clsPos`
このクラスモジュールは、「筋(Suji)」と「段(Dan)」という2つのプロパティを持ち、将棋盤上の位置を表します。
**重要**: `clsPos`クラスモジュールのプロパティウィンドウで、`PredeclaredId`プロパティを`True`に設定してください。
‘ クラスモジュール名: clsPos
‘ プロパティウィンドウで「PredeclaredId」を「True」に設定することを忘れないでください。
Option Explicit
‘ 筋(列)を表すプライベート変数
Private m_Suji As Long
‘ 段(行)を表すプライベート変数
Private m_Dan As Long
‘ === プロパティの実装 ===
‘ 筋プロパティのSetアクセサー
Public Property Let Suji(ByVal v As Long)
m_Suji = v
End Property
‘ 筋プロパティのGetアクセサー
Public Property Get Suji() As Long
Suji = m_Suji
End Property
‘ 段プロパティのSetアクセサー
Public Property Let Dan(ByVal v As Long)
m_Dan = v
End Property
‘ 段プロパティのGetアクセサー
Public Property Get Dan() As Long
Dan = m_Dan
End Property
‘ === メソッドの実装 ===
‘ 位置を設定するメソッド
‘ デフォルトインスタンスとして利用する際に、まとめて位置を設定できると便利です。
Public Sub SetPosition(ByVal Suji As Long, ByVal Dan As Long)
Me.Suji = Suji
Me.Dan = Dan
End Sub
‘ 位置をクリアするメソッド(必要に応じて)
Public Sub ClearPosition()
m_Suji = 0
m_Dan = 0
End Sub
‘ 現在の位置が将棋盤の範囲内であるかを判定するメソッド
‘ (将棋盤は1~9筋、1~9段と仮定)
