VBAでインタフェースを使ってみた

インタフェースを用いたポリモーフィズムをやってみた

立山秀利さんが著書の中で使っていた音楽プレーヤのたとえが私にとっては一番分かりやすかったので、それをVBAでやってみる。

方針としては、

  1. 「RecordPlayer」クラス、「CDPlayer」クラス、「MP3Player」クラスの3つのクラスを作る
  2. 3つのクラスに共通する「音楽を再生する」という機能を「play」メソッドとして括り出す
  3. 「IMusicPlayer」インタフェースに「play」メソッドを定義する
  4. 「RecordPlayer」、「CDPlayer」、「MP3Player」の各クラスで「IMusicPlayer」インタフェースを実装する
  5. 「RecordPlayer」、「CDPlayer」、「MP3Player」の各クラスで「play」メソッドを独自に定義する

これでポリモーフィズムが実現できるはず。

イメージとしては、

音楽を再生する方法はどうあれ、ともかく「play」と命令すればそれぞれのオブジェクトがそれぞれのやり方で音楽を再生する

といったところか。

インタフェースの準備

リスト1 インタフェース「IMusicPlayer」
'クラスモジュール'
'オブジェクト名「IMusicPlayer」'
Option Explicit
'Fields'
Private Name_ As String
'Accessor'
Public Property Get Name() As String
  Name = Name_
End Property
'Methods'
Public Sub play()
End Sub

「Name」というプロパティ(フィールド)を持たせていること、あとは「play」というメソッドを持たせているだけ。

特に、メソッドについては、名前を定義しているだけで処理の中身は空っぽ。

具体的な処理は、このインタフェースを実装する各クラスで独自に定義するのだから、これでいいのだ(ですよね?)。

各クラスでのインタフェースの実装

リスト2-1 「RecordPlayer」クラス
'クラスモジュール'
'オブジェクト名「RecordPlayer」'
Option Explicit
Implements IMusicPlayer    '……(1)'
'Fields'
Private IMusicPlayer_Name_ As String    '……(2)'
'Accessor'
Public Property Get IMusicPlayer_Name() As String    '……(3)'
  IMusicPlayer_Name = IMusicPlayer_Name_
End Property
'Methods'
Public Sub init(ByVal n As String)
  IMusicPlayer_Name_ = n    '……(4)'
End Sub

Public Sub IMusicPlayer_play()    '……(5)'
  Debug.Print "レコードを再生するぜ~♪" & vbCrLf & _
              "針が飛ぶから、暴れるんじゃねーぞw"
End Sub

ほとんど通常のクラスモジュールと同じ書き方なんだけど、至る所に「IMusicPlayer」というインタフェース名が出てくるところがポイント。

まず(1)、

Implements IMusicPlayer

宣言セクションでこう書く。変なの。

(2)と(3)、フィールド・アクセサの設定も、

Private IMusicPlayer_Name_ As String
Public Property Get IMusicPlayer_Name() As String
  IMusicPlayer_Name = IMusicPlayer_Name_
End Property

「Name」というフィールド(プロパティ)名にいちいち「IMusicPlayer」をスネーク記法で付けないといけない。これも何だかなー。

(4)の

IMusicPlayer_Name_ = n

では、擬似コンストラクタのinitメソッドの引数を受け取っている。当然ここにも「IMusicPlayer」が……。

あと、(5)のメソッド名の定義、

Public Sub IMusicPlayer_play()

にもやっぱり「IMusicPlayer」……。

仕様とはいえ、何だか美しくないんだよなー……。

あと、「CDPlayer」、「MP3Player」についても、やり方は全く同じなので、リストだけ載っけときます。

リスト2-2 「CDPlayer」クラス
'クラスモジュール'
'オブジェクト名「CDPlayer」'
Option Explicit
Implements IMusicPlayer
'Fields'
Private IMusicPlayer_Name_ As String
'Accessor'
Public Property Get IMusicPlayer_Name() As String
  IMusicPlayer_Name = IMusicPlayer_Name_
End Property
'Methods'
Public Sub init(ByVal n As String)
  IMusicPlayer_Name_ = n
End Sub

Public Sub IMusicPlayer_play()
  Debug.Print "CDを再生するぜ~♪"
End Sub
リスト2-3 「MP3Player」クラス
'クラスモジュール'
'オブジェクト名「MP3Player」'
Option Explicit
Implements IMusicPlayer
'Fields'
Private IMusicPlayer_Name_ As String
'Accessor'
Public Property Get IMusicPlayer_Name() As String
  IMusicPlayer_Name = IMusicPlayer_Name_
End Property
'Methods'
Public Sub init(ByVal n As String)
  IMusicPlayer_Name_ = n
End Sub

Public Sub IMusicPlayer_play()
  Debug.Print "MP3を再生するぜ~♪"
End Sub

長ったらしくなってすまん。

まあ、これで3つのクラス全てに「IMusicPlayer」インタフェースを実装したことになる。

(「実装」の使い方、これで合ってるんだろうか……?)

ポリモーフィズムをやってみる

次のコードで各種音楽プレーヤを使ってみる。

Option Explicit
Public Sub musicPlayerTest()
  'インスタンス用変数を準備'    '……(1)'
  Dim cdp As CDPlayer
  Dim rp As RecordPlayer
  Dim mp3p As MP3Player
  'インスタンス生成&擬似コンストラクタ発動'    '……(2)'
  Set rp = New RecordPlayer
  rp.init "レコードプレーヤー1号"
  Set cdp = New CDPlayer
  cdp.init "CDプレーヤ1号"
  Set mp3p = New MP3Player
  mp3p.init "ストロングマシン1号"
  'インタフェース型配列に各インスタンスを格納'    '……(3)'
  Dim iMPlayer(0 To 2) As IMusicPlayer    '……(4)'
  Set iMPlayer(0) = rp    '……(5)'
  Set iMPlayer(1) = cdp
  Set iMPlayer(2) = mp3p
  'Nameプロパティの出力とplayメソッドの実行'    '……(6)'
  Dim i As Integer
  For i = 0 To 2
    With iMPlayer(i)
      Debug.Print .Name    '……(7)'
      .play    '……(8)'
    End With
  Next
End Sub

(1)の後の3行

Dim cdp As CDPlayer
Dim rp As RecordPlayer
Dim mp3p As MP3Player

で各音楽プレーヤ用の変数を準備。

(2)の後の6行では、たとえば「RecordPlayer」クラスの場合、

Set rp = New RecordPlayer
rp.init "レコードプレーヤー1号"

こんなふうに、インスタンスを生成した後、擬似コンストラクタinitメソッドでNameプロパティを設定している。

「CDPlayer」にしても「MP3Player」にしてもやっていることは同じ。

で、(3)からは、(4)の

Dim iMPlayer(0 To 2) As IMusicPlayer

インタフェース「IMusicPlayer」型の配列変数を用意して、

(5)からの

Set iMPlayer(0) = rp
Set iMPlayer(1) = cdp
Set iMPlayer(2) = mp3p

でそれぞれのクラスのインスタンスを配列に格納している。

後は、(6)の後の7行、

Dim i As Integer
For i = 0 To 2
  With iMPlayer(i)
    Debug.Print .Name    '……(7)'
    .play    '……(8)'
  End With
Next

で、配列の各要素について、(7)で「Name」プロパティを表示させ、(8)でplayメソッドを実行している。

このとき、配列「iMPlayer」の各要素は、それぞれ別々のクラスのインスタンスなのに、全て同じ名前でプロパティやメソッドが呼び出せているというところがミソ。

実行結果

f:id:akashi_keirin:20170619050311j:plain

ほれ。同じメソッド名で呼び出したにもかかわらず、それぞれのクラスがそれぞれのやり方でplayメソッドを実行していることが分かる。

おわりに

正直、まだインタフェースの使いどころについてはピンときていないが、実際にコードを書いてみると、思ったより簡単だった。やっぱり、実際にコードを書いて動かしてみるというのが大事なんだなあ。

……と、ここまで書いてきてから気づいたんだが、

initメソッドもまとめてしまったらよかったんじゃね?

……orz

f:id:akashi_keirin:20170619050303j:plain

ちなみに、クラス側でちゃんとメソッドを置かないと、こんなふうに叱られる。

@akashi_keirin on Twitter