Excel VBAのインターフェース継承を試す

Excel VBAには一応クラスの機能がありオブジェクト志向でプログラムを作成することもできます.さて,今日はImplementsキーワードを利用したインターフェース継承を試してみます.インターフェース継承をざっくりと説明すると,クラスモジュールを「扱う側」の処理を共通化することに利用できる考え方となるように思います.多相性の実現が可能となると言えるかも知れません.

実装例として,ある関数で記述される数値の配列をクラス化し,それをグラフとして描画する処理を共通化してみます.そこで,以下のようなものを実装してみます.

  • グラフを描画するための関数 (DrawGraph())
  • 上記関数に与えるインタフェースを定義したクラス(抽象クラスiGraphData)
  • 抽象クラスを継承し,データを保持するクラス(正弦波クラスcSinusoidal,直線クラスcLinear)

まず,グラフを描画するのに必要なデータを考えると以下のようなものがありそうです.

  • データの数とデータ
  • 横軸と縦軸の名前
  • 関数名

これらを取得するためのプロパティを定義した抽象クラスを作れば良さそうです.例えば以下のようにします.

' iGraphData.bas
Option Explicit
' データの数を取得する
Public Property Get nD() As Long
End Property
' i番目のXを取得する
Public Property Get XV(tIdx As Long) As Double
End Property
' i番目のYを取得する
Public Property Get V(tIdx As Long) As Double
End Property
' 横軸の名称を取得する
Public Property Get strXVName() As String
End Property
' 縦軸の名称を取得する
Public Property Get strVName() As String
End Property
' 関数の名称を取得する
Public Property Get strFName() As String
End Property

次に上記のようなクラスを与えて,グラフとして描画する関数DrawGraphを以下のように記述します.

' DrawGraph.bas
Option Explicit
' グラフを描画する関数
Public Sub DrawGraph(tSheet As String, tGD As iGraphData)
    Dim i As Long
        
    ' シートを作成する
    With ThisWorkbook
        .Sheets.Add After:=.Sheets(.Sheets.Count)
        .Sheets(.Sheets.Count).Name = tSheet
    End With
    
    With ThisWorkbook.Worksheets(tSheet)
        ' 前に出す
        .Activate
        
        ' データを書き出す
        For i = 0 To tGD.nD - 1
            .Cells(i + 1, 1) = tGD.XV(i)
            .Cells(i + 1, 2) = tGD.V(i)
        Next
    
        ' グラフを描画する
        .Shapes.AddChart.Select
        With ActiveChart
            '.HasTitle = True
            .ChartType = xlLine
            
            .Axes(xlCategory, xlPrimary).HasTitle = True
            .Axes(xlCategory, xlPrimary).AxisTitle.Characters.Text = tGD.strXVName
            .Axes(xlValue, xlPrimary).HasTitle = True
            .Axes(xlValue, xlPrimary).AxisTitle.Characters.Text = tGD.strVName
            
            .SetSourceData Source:=Range("A1")
            With .SeriesCollection(1)
                .Values = Range(Cells(1, 2), Cells(1 + tGD.nD - 1, 2))
                .XValues = Range(Cells(1, 1), Cells(1 + tGD.nD - 1, 1))
                .MarkerStyle = xlMarkerStyleCircle
                .Name = tGD.strFName
                .MarkerStyle = xlMarkerStyleNone ' マーカーを消す
            End With
            
            .Parent.Top = Range("C2").Top
            .Parent.Left = Range("C2").Left
        End With
    End With
End Sub

やっていることは横軸の値と縦軸の値を1列目と2列目に書き出し,線グラフとして描画するものです.

さて,どちらかと言えばここからが本題です.クラスiGraphDataは関数DrawGraphとのデータのやり取りを記述するもので,何の処理も記述していませんから,数値データを保持するための具体的なクラスとしてcSinusoicalを実装します.

' cSinusoical.bas
Option Explicit

Private mNData As Long
Private mValue() As Double
Private mXValue() As Double
Private mScale As Double
Private mTheta0 As Double
Private mIsCalculated As Boolean

' インターフェース継承
Implements iGraphData

' データの数を取得する
Public Property Get iGraphData_nD() As Long
    If mIsCalculated Then
        iGraphData_nD = mNData
    End If
End Property

' i番目のXを取得する
Public Property Get iGraphData_XV(tIdx As Long) As Double
    If mIsCalculated Then
        iGraphData_XV = mXValue(tIdx)
    End If
End Property

' i番目のXを取得する
Public Property Get iGraphData_V(tIdx As Long) As Double
    If mIsCalculated Then
        iGraphData_V = mValue(tIdx)
    End If
End Property
    
' 横軸の名称を取得する
Public Property Get iGraphData_strXVName() As String
    iGraphData_strXVName = "Theta"
End Property

' 縦軸の名称を取得する
Public Property Get iGraphData_strVName() As String
    iGraphData_strVName = "Y"
End Property

' 関数の名称を取得する
Public Property Get iGraphData_strFName() As String
    iGraphData_strFName = "Sinusoidal Curve"
End Property

' コンストラクタ
Private Sub Class_Initialize()
    mIsCalculated = False
End Sub

' デストラクタ
Private Sub Class_Terminate()
End Sub

' 関数値を計算する
Public Sub Calc(tXVmin As Double, tXVMax As Double, tDiv As Long, _
                tScale As Double, tTheta0 As Double)
    Dim i As Long

    ' 領域確保
    ReDim mValue(tDiv - 1) As Double
    ReDim mXValue(tDiv - 1) As Double
    
    mScale = tScale
    mTheta0 = tTheta0
    mNData = tDiv
    
    ' 計算を実行する
    For i = 0 To mNData - 1
        mXValue(i) = tXVmin + i * (tXVMax - tXVmin) / tDiv
        mValue(i) = mScale * Sin(mXValue(i) + mTheta0)
    Next
    
    mIsCalculated = True
End Sub

このクラスは以下の関数値を計算し配列として保持するものです.
y = A\sin\left(\theta - \theta_0\right)
同様に以下の直線の関数値を計算して配列として保持するクラスcLinearを実装します.
y = Ax + b

' cLinear.bas
Option Explicit

Private mNData As Long
Private mValue() As Double
Private mXValue() As Double
Private mAValue As Double
Private mBValue As Double
Private mIsCalculated As Boolean

' インターフェース継承
Implements iGraphData

' データの数を取得する
Public Property Get iGraphData_nD() As Long
    If mIsCalculated Then
        iGraphData_nD = mNData
    End If
End Property

' i番目のXを取得する
Public Property Get iGraphData_XV(tIdx As Long) As Double
    If mIsCalculated Then
        iGraphData_XV = mXValue(tIdx)
    End If
End Property

' i番目のXを取得する
Public Property Get iGraphData_V(tIdx As Long) As Double
    If mIsCalculated Then
        iGraphData_V = mValue(tIdx)
    End If
End Property
    
' 横軸の名称を取得する
Public Property Get iGraphData_strXVName() As String
    iGraphData_strXVName = "X"
End Property

' 縦軸の名称を取得する
Public Property Get iGraphData_strVName() As String
    iGraphData_strVName = "Y"
End Property

' 関数の名称を取得する
Public Property Get iGraphData_strFName() As String
    iGraphData_strFName = "Linear Function"
End Property

' コンストラクタ
Private Sub Class_Initialize()
    mIsCalculated = False
End Sub

' デストラクタ
Private Sub Class_Terminate()
End Sub

' 関数値を計算する
Public Sub Calc(tXVmin As Double, tXVMax As Double, tDiv As Long, _
                tAvalue As Double, tBvalue As Double)
    Dim i As Long

    ' 領域確保
    ReDim mValue(tDiv - 1) As Double
    ReDim mXValue(tDiv - 1) As Double
    
    mAValue = tAvalue
    mBValue = tBvalue
    mNData = tDiv
    
    ' 計算を実行する
    For i = 0 To mNData - 1
        mXValue(i) = tXVmin + i * (tXVMax - tXVmin) / tDiv
        mValue(i) = mAValue * mXValue(i) + mBValue
    Next
    
    mIsCalculated = True
End Sub

最後に呼び出し側である関数ExecDrawGraphを以下のように実装します.

' Main.bas
Option Explicit

Public Sub ExecDrawGraph()
    Dim cSin As cSinusoidal
    Dim cLin As cLinear
    
    Set cSin = New cSinusoidal
    Call cSin.Calc(0, 2 * 3.14, 100, 1.5, -3.14 / 2)
    Call DrawGraph("sin_curve", cSin)
    
    Set cLin = New cLinear
    Call cLin.Calc(0, 10, 100, 3, 2)
    Call DrawGraph("linear_func", cLin)
End Sub

これらを実行するとシートsin_curveに正弦波の関数が,linear_funcに直線の関数が描画されます.

内部処理がそれぞれ微妙に異なる二つのクラスcSinusoidalcLinearについてグラフを描画する関数を共通化できたことが分かります.この程度の例では何が利点かは分かりづらいのですが,クラスモジュールと継承を使うことは以下の利点があります.

  • データとデータを扱う手段をクラスとしてまとめ込める
  • インターフェース継承によってクラスを「扱う側」の処理を共通にできる

データに対する処理が複雑化した場合,似たような処理に共通の実装を用いるにはクラスモジュールを使うべきでしょう.重要なのはインターフェースを継承したクラスの内部処理がどのように実装されていても良いという点です.今回の例ではcSinusoidalcLinearは双方ともデータを配列で保持していましたが,ファイルやシートに記述されたデータを読み出すクラスであっても,データ列が二次元であればiGraphDataを継承することで関数DrawGraph()によってグラフを描画できます.

ちなみに,cSinusoidalcLinearの中身がほとんど同じであるわけですが,実装継承ができればこの辺りもかなり綺麗になるように思います.とは言え,VBAの仕様上それはできないようなので仕方がないのかも知れません.