t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

VBA インターフェースとは、プロパティやメソッドの存在を保証する規格のようなもの

プログラミングの学習途中で、いくつか難しい概念にぶつかる。
インターフェースもその一つだと思う。このインターフェースは、フォームのボタンなどの、ユーザーインターフェースのことではない。
クラスモジュールなどで作ったオブジェクトに適用するインターフェースのことである。
VBAにもインターフェースの機能が備わっているが、あまり知られていない。

さて、そもそもインターフェースとは何なのか。
インターフェースとは一言でいえば「規格」のようなものである。

世の中はさまざまな規格のおかげで成り立っている。
たとえば先日シェーバーの電池が切れたので単4型の乾電池を買ったが、規格のおかげで悩むことはない。Panasonicでも東芝でも富士通でも、単4であれば好きなメーカーの好きな電池を買えばよい。単4乾電池を名乗るからにはすべて円筒型で、長さ44.5mm、直径10.5mmで1.5ボルトである。そのように規格で定められており、どれを買ってもシェーバーは正常に動く。

プログラミングにおけるインターフェースもこの「規格」と同じように、なにかを保証する働きをする。
規格に準拠した製品が必ず規格で定められた要素を満たしているように、インターフェースに準拠したオブジェクトは、必ずインターフェースで定められたプロパティ、メソッドを持っている。
つまりオブジェクトを使用するプログラマーは、オブジェクトの詳細を知らなくてもインターフェースさえ知っていればメインコードを書くことができる。

まずはインターフェース=UIというイメージを捨てよう

ユーザーインターフェースという言葉が有名になってしまったので、インターフェースが規格のようなものだと言われてもピンと来ないかもしれない。

そこでまずインターフェースという言葉を正確にとらえるため、単語を分解して意味を考えてみよう。

Interは「中間・相互・間」という意味の接頭辞である。
たとえばインターネットはネットとネットの相互のつながり。これまで小さな網(もう)だったネットワークが相互につながったものである。
インターナショナルは国際的なという意味であるが、これも国と国の間(inter nation)に形容詞を作るalをくっつけたもの。

feceは「顔」という意味のほかに、「面(めん)」という意味がある。物体の表面とか、そういう面だ。

インターフェースは、面と面の間、つまり境界面のことだといえる。

ユーザーインターフェースは、ソフトウェアと人間が接する境界面。
キーボードやマウスをインターフェースと呼ぶのも同じで、パソコンと人間が接する境界面だから。
それで、オブジェクトとオブジェクトを利用するコードが接する境界面も、インターフェースと呼ばれる。

規格も、境界面で本領を発揮する。
先ほどの乾電池の例でいえば、シェーバーと単四乾電池の境界面を定めているのが規格である。電池の規格だけ決まっていても、電池ボックスが好き勝手な形状だったらカッチリはまらない。

インターフェースが規格のようなものだというのも、そういうことである。

具体的な使い方

インターフェースを作るには(1)任意の名前でクラスモジュールを作成し、(2)InstancingプロパティをPublicNotCreatableに設定し、(3)メソッドやプロパティを作成する。
f:id:t-hom:20161029195701p:plain

ただし、インターフェースが決めるのは、どのようなメソッド、プロパティを持っているかと、その引数、戻り値のみなので、プロパティやメソッドの中身は書かず、空っぽにしておく。

作成したインターフェースを使用するには、(4)任意の名前でクラスモジュールを作成し、(5)InstancingプロパティはPrivateのまま変更せず、(6)コードの先頭にImplements インターフェース名と書く。
f:id:t-hom:20161029200350p:plain

クラスモジュールに「Implements インターフェース名」と書くと、「そのインターフェースを実装します!」と宣言したことになる。いわば「私は○○という規格に準拠している」と公言するようなものである。
宣言したからにはVBAコンパイラによる監査も入る。つまりインターフェースで定めたメソッドやプロパティを実装しないままコンパイルするとコンパイルエラーになるのだ。
このままデバッグメニューからコンパイルすると、以下のようなエラーが確認できる。
f:id:t-hom:20161029200613p:plain
※このエラーメッセージはちょっとおかしい。IWritableとWriteLineが逆じゃないかと思う。

インターフェースで定義したメソッドはそのまま書いてもコンパイルは通らず、インターフェース名とアンダーバーに続けて書く必要がある。

Private Sub IWritable_WriteLine(str As String)

End Sub

なお、インターフェース側のWriteLineはPublicで宣言したが、ここではPrivateで良い。
手入力しても良いが、Implements IWritableを書いた時点で、オブジェクトボックスで選択できるようになり、IWritableを選択すると自動で上記のコードが入る。
f:id:t-hom:20161029201006p:plain
インターフェースに複数プロシージャがある場合はオブジェクトボックスで選択した後、右のプロシージャボックスで選択すれば良い。

さて、これでコンパイルは通るようになるが、中身がないと何も起きない。
Class1のIWritable_WriteLineメソッドにDebug.Print strを追記しよう。

Private Sub IWritable_WriteLine(str As String)
    Debug.Print str
End Sub

それでは、メインプロシージャでこれらを使用したコードを書いてみる。

Sub hoge()
    Dim W As IWritable    '(1)
    Set W = New Class1   '(2)
    W.WriteLine "Hello"     '(3)
    W.WriteLine "Good Bye"
End Sub

(1)変数Wを、インターフェース型として宣言する。
(2)インターフェース型の変数Wに、Class1オブジェクトをセットする。
(3)インターフェースのWriteLine命令を実行すると、実際にはClass1オブジェクトのIWritable_WriteLineが実行される。

同じインターフェースを実装したオブジェクトは、同じように取り扱うことができる。

試しにシートモジュール「Sheet1」に以下のコードを書いてみよう

Implements IWritable
Private Sub IWritable_WriteLine(str As String)
    Dim r As Range
    Set r = Cells(Rows.Count, 1).End(xlUp)
    If r <> "" Then Set r = r.Offset(1, 0)
    r.Value = str
End Sub

標準モジュールのコードは以下のように変更する。

Sub hoge()
    Dim W As IWritable
    Set W = Sheet1
    W.WriteLine "Hello"
    W.WriteLine "GoodBye"
End Sub

実行結果はこのようになった。
f:id:t-hom:20161029204746p:plain

メインコードで変更したのは、変数WにセットするオブジェクトをClass1からSheet1に変えただけである。
オブジェクトの挙動は大きく変わったが、呼び出す側は何も悩むことなく、WriteLineメソッドを使用できる。なぜならSheet1もIWritableインターフェースを実装している以上、必ずWriteLineメソッドを持っていることがコンパイラによって保証されるからだ。

シェーバーがどのメーカーの電池でも正常に動作するのと同じである。

なお、Object型やVariant型を使えばインターフェースを使わなくても同じことができるが、コンパイラによるチェックの恩恵を受けることができない。

関連記事

インターフェースを活用したサンプルは以下の記事に書いているので参考までに。
thom.hateblo.jp
thom.hateblo.jp
thom.hateblo.jp

以下はインターフェースは使ってないけれど、最後のほうにインターフェース活用のヒントあり。
thom.hateblo.jp

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。