実装の継承は、サブクラスとスーパークラスとの間で「~は~である」という関係が成り立つときだけ使うべきです。たとえばこの例では、図1に示したように「ビーグル(beagle)は犬(dog)である」と言うことができます。このような「~は~である」という条件が成立するかぎり、実装の継承はコードの再利用を実現する手段として役に立ちます。特に、共通の動作をするクラスを多数含むアプリケーションでは、実装の継承は非常に価値があります。複数のクラスに共通点がある場合に、その共通部分をスーパークラスに格上げできます。たとえば、いったん作成したCDogクラスを、「~は犬である」という条件を満たすCBeagle、CTerrier、CBoxerなどの任意のクラスに拡張できます。CDogクラスの状態と動作を定義するために記述したコードは、ほかの多くのクラスで再利用できます。

Ted Pattison
Microsoft Corporation
1999年1月


  • 本稿は『Programming Distributed Applications with COM & Microsoft Visual Basic(Ted Pattison著、Microsoft Press刊、ISBN# 1-57231-961-5)』からの抜粋です。
  • 要約:
    Microsoft(r) Visual Basic(r)のプログラマを対象に、インターフェイス ベース プログラミングに関する次の項目について説明しています(印刷時19ページ)。

はじめに
クラス、オブジェクト、およびクライアント
実装の継承
ポリモーフィズムとは?
実装の継承に関する問題
Visual Basicでのインターフェイスの使用
なぜインターフェイスを使うのか?
ユーザー定義インターフェイスを持つアプリケーションの設計
おわりに

はじめに

インターフェイス ベース プログラミングについて理解することは決してやさしいことではありません。このプログラミングのスタイルを正しく評価するためには、コードの記述に関する従来の習慣や直感的な判断をすべて捨て去り、過去10年間にわたって進化してきたオブジェクト指向プログラミングとコンピュータ技術について振り返ってみる必要があります。ダーウィンの進化論風にいえば、インターフェイスのおかげで現代のソフトウェア デザインが生き残りのためにどのように適応できるようになったのかを検証するということです。かつてない激しい変動を見せる生産環境の中で生き残っていくためには、ソフトウェアが3つの重要な特徴である「再利用性」、「保守性」、そして「拡張性」を備えていなければなりません。本稿では、インターフェイス ベース プログラミングの概略を示すとともに、これらの3つの特徴について説明します。

コンポーネント オブジェクト モデル(COM)は、インターフェイス ベース プログラミングの概念が土台となっています。インターフェイスの概念がなければCOMの存在は無意味であるといっても過言ではないでしょう。読者がVisual Basicのプログラマで、COMが実際にどのように動作しているのかを理解しようとしているのであれば、オブジェクト指向のソフトウェア デザインにおいてインターフェイスがいかに価値のある存在であり、なぜそれほどまでに価値があるのかについて十分な時間を費やして理解すべきです。インターフェイス ベース プログラミングを理解すれば、今までよりもずっとレベルの高いCOMプログラムを作れるようになり、その結果としてMicrosoft Transaction Server(MTS)などの分散環境に対応したCOMベース アプリケーションを作成できるようになります。

インターフェイス ベース プログラミングはCOMの領域のさらに外側に位置するものであり、公開インターフェイスを実装から分離するという考え方に基づいたプログラミングの原則です。インターフェイス ベース プログラミングはC++やSmalltalkなどの言語を通じて多くの技術者によって開発されましたが、その中で技術者たちは、独立のインターフェイスを使うことでソフトウェアの保守や拡張が容易になり、特に大規模なアプリケーションでその有効性が高いことを知りました。Javaを開発した技術者たちもインターフェイス ベース プログラミングの良さに気づき、その結果としてインターフェイス ベース プログラミングをサポートする機能を自分たちの言語にそのまま取り入れています。

インターフェイスは、オブジェクト指向プログラミングにおけるコードの再利用に関する問題の多くを解決します。本稿ではこれらの問題のいくつかについて解説します。特に、従来のOOPに準拠したスタイルでプログラムを作成していると、クラスの定義に依存した柔軟性のないクライアントプログラムができあがってしまうことがあります。このような依存性があると、クライアント コードを修正しないかぎりクラスの保守や拡張が非常に難しくなってしまう可能性があり、時がたつにつれてオブジェクト コードの改良が非常に困難になるか、または不可能になってしまいます。また、OOPの一般的な言語機能である、実装の継承に関する問題もいくつか存在します。実装の継承は非常に強力な機能ですが、しばしば誤った使い方をされているようです。実装の継承についても同じような依存性の問題に陥りやすい傾向があり、そのためにアプリケーションの保守性や拡張性が犠牲になっています。Visual Basicは実装の継承をサポートしていませんが、本稿では実装の継承に関する問題をいくつか取り上げ、その長所と欠点について解説するとともに、このような問題を解決するためにインターフェイス ベース プログラミングが誕生したことを明らかにします。

Visual Basic Version 5.0では、ユーザー定義インターフェイスの定義と実装を行うためのサポートが追加されています。本稿では、Visual Basicアプリケーションでのインターフェイスの使い方を紹介します。インターフェイスの使用に関する基本的な説明をした後に、インターフェイス ベース プログラミングをより強力なものにする「ポリモーフィズム」と「実行時型検査」を実現する方法を紹介します。

クラス、オブジェクト、およびクライアント

インターフェイスを理解する上で最初に直面する課題は、インターフェイス ベース プログラミングが解決しようとしている問題が何であるかを調べることです。これらの問題の多くは、クラスと、それを使用するクライアントとの結び付きに関係があります。次のような問いを考えてみましょう。「クライアントとクラス定義とはどのような関係にあるか?」「クラスの恩恵を受けるためにクラスに関してクライアントが知らなければならないことは何か?」「プログラマがクラスのメソッドやプロパティを使ってコードを書くときに、クライアントの中でどのような依存関係が生まれるか?」 一般的なオブジェクト指向プログラミングでは、クライアントはクラスからオブジェクトのインスタンスを生成します。ふつう、クライアントはNew演算子の後にクラス名を指定してオブジェクトを作成します。オブジェクトを作成したら、クライアントは公開されているプロパティとメソッドのセットに変数を通じてアクセスすることで、そのオブジェクトを使用します。この場合、変数はクラス ベース参照になります。次に紹介する簡単な例では、クラスの型に基づく変数を使ってオブジェクトの公開メンバーにアクセスしています。

  Dim Dog As CDog
Set Dog  = New CDog
' access a property
Dog.Name = "Snoopy"
' invoke a method
Dog.Bark

この例では、クラス ベース参照によって、dogというオブジェクトのインスタンス化とそのオブジェクトとのやり取りが可能になっています。クライアントとオブジェクトとのやり取りは、公開されているプロパティとメソッドのセットを通じて行われます。このセットのことをオブジェクトの「公開インターフェイス」とよびます。クラスの作成者は、必ずこの公開インターフェイスを使ってオブジェクトの機能をクライアントに公開します。オブジェクトが便利な理由もここにあるわけですが、注意すべきことは、公開インターフェイスから提供されるメソッド名とプロパティ名はクライアントにハードコードされているということです。つまり、このようにしてクライアントに埋め込まれた依存コードをこのクラスの将来のバージョンにおいても使えるようにするためには、同じメンバーを引き続き残しておく必要があります。

クラスを使う利点の1つに、クラスによってコードの再利用が可能になるという点があります。一度作成したクラスはアプリケーションの様々な部分で利用できます。そのため、アプリケーションの中で余分なコードを省略または削除できるようになり、コードの保守も容易になります。クラスの中で非公開にしたプロパティやメソッドは、自由に変更または削除できます。公開のメソッドであっても、呼び出しの構文を変えないかぎり実装内容の変更ができます。クラス内のメソッドの実装が改良されても、そのクラスを使用するクライアントはそのままのコードで恩恵を享受できます。

既存のクラス定義を変えるときは、公開のメソッドやプロパティにアクセスするための呼び出し構文を変更してはいけません。構文を変更してしまうと、本来のクラス定義に基づいて作成したクライアント コードが、その構文に依存した部分を使用できなくなる恐れがあります。公開インターフェイスを変えなければ、クライアント コードを修正することなくクラスに変更を加えて改良できます。

上の段落の要点を言いかえると、「クラスの公開インターフェイスの中でいったん公開してしまったプロパティやメソッドのシグネチャは、変更や削除ができない」ということになります。つまり、プロジェクトの最初の段階において、公開インターフェイスを適切に設計し、クラスの変更や改良にかかわらずその原則を守る必要があります。そうすれば、オブジェクトコードを改良したり拡張したりする場合にもクライアント コードを書き直す必要がなくなり、クラスの保守作業もバグの修正とメソッドの実装の改良だけになります。

このように、公開インターフェイスの既存のメンバーを保守する上で守るべき規則はごく平凡ですが、クラスに新しい機能を追加するときには果たしてどの程度の柔軟性があるのでしょうか。またどのようにすれば将来のバージョンでオブジェクトを安全に拡張できるのでしょうか。新しい公開のメソッドやプロパティをクラスに追加すること自体は簡単であり、かつ安全に行えます。従来のクライアントも以前と同じように動作します。ただし、オブジェクトの新機能は利用できません。しかし、クラスの変更後に作成したクライアントは、公開インターフェイスに追加されたメンバーを利用できます。つまり、生産環境の中では時間をかけてオブジェクトを安全に改良できるということになります。

クラスの設計において問題が起きるのは、既存のクライアントが利用できなくなるような形で公開メソッドのシグネチャを変更する場合です。ふつう、このようなことは最初に決定したクラス設計に不備があり、後になってそれに気づいたという場合に起こります。たとえば、dogというクラスで犬がころがり回る動作を実行するメソッドを考えてみましょう。次のRollOver()は16ビットの整数パラメータを持つメソッドとして定義されており、クライアントはメソッドを起動するたびにころがり回数を指定できます。

  ' method defined in CDog class
Public Sub RollOver(Rolls As Integer)
  ' implementation
End Sub
' client hard-codes calling syntax
Dim Dog As CDog, Rolls As Integer
Set Dog = New CDog
Rolls = 20000
Dog.RollOver Rolls

仮に、最初の設計段階において、dogオブジェクトの変化をきちんと予想できていなかったとしたら、どうなるでしょうか。たとえば、32 KBの整数の上限値を超えるころがり回数を要求した場合はどうなるでしょう。クライアントが値50,000を指定してメソッドを起動しようとしたらどうなるでしょう。大きな値に対処するためにはパラメータの型をlong整数に変更する必要がありますが、このような変更は設計上の大きな問題を招くことになります。つまり、新しいクライアントプログラムは32ビット整数を要求しますが、ここに示したような古いクライアントプログラムはすでに16ビット整数に依存したコードになってしまっているのです。

ここでは2つの解決手段が考えられます。1つは、メソッドのシグネチャを変更した後、それを呼び出すクライアント コードをすべて書き直す方法です。もう1つは、コードはそのまま残し、本来の設計上の制限に対処する方法です。これまで見てきたように、クラスの設計に不備があるとクライアント コードを修正しなければならなくなったり、オブジェクトの拡張ができなくなったりします。

クラスの公開インターフェイスの設計を完璧にしてからそのクラスに対してクライアント コードを記述すればよい、という解決法はすぐに思いつきます。しかし、そのようなことはクラス設計の熟練者であっても常に可能であるとはかぎりません。現実世界で一定不変なものをモデルにしたクラスであれば、熟練した設計者は長く使用できる堅牢なデザインを考案できるかもしれません。しかしほとんどの場合、外的な変化によってオブジェクトの公開インターフェイスの必要条件がどのような影響を受けるかについて、設計者が予測を立てることは困難です。アプリケーションのクラスを作成する際、急速に変動するビジネス環境で何年もの間動作し続けることを想像することはできても、それでは実際に何が必要になるのかを予測することはおそらくできないでしょう。ビジネス モデルが絶えず変動しているのであれば、クラスもまたその動きに追随していかなければなりません。拡張性のあるオブジェクトが必要となる理由もそこにあります。

ソフトウェアの設計者たちは、「クラス ベース参照」を利用するとクライアントとクラスとの間に依存性のあるコードが生じてしまうことに気づきました。しかし、設計上の原則を守り、将来の必要条件をあらかじめ予測しておけば、このような依存性が保守性や拡張性に及ぼす影響を小さくできます。公開インターフェイスの中では永久使用を前提とするのでないかぎり、メソッドやプロパティを定義してはいけません。熟練した設計者は、データ プロパティをすべて非公開にし、オブジェクトの状態へのアクセス手段を公開メソッドとして提供します。このようにすると、どのクライアントもクラスの実際のデータ配置に依存しなくなります。どのメソッドを公開するかについては常識の範囲内で決定します。非公開にしたメンバーは、クラスの実装が進化する過程で変更または削除できます。もちろん、メンバーのすべてを非公開にしてしまうとそのクラスは無益なものになってしまいます。クラス ベース参照を使って設計を行う場合は、常にこのようなトレードオフが伴います。

実装の継承

OOPの機能の多くは、高水準のコードの再利用をプログラマに提供することを目的としています。C++やSmalltalk、Javaなどの言語は、一般に「実装の継承」とよばれている機能を提供します。この機能は、一般的なオブジェクト指向プログラミングにおいてコードの再利用を実現するために考案された数多くの手段のうちの1つです。中には、実装の継承を提供する言語こそ真のオブジェクト指向言語とみなすべきである、と主張する意見もあり、ソフトウェア業界や学界で熱い議論が交わされています。ここではそのような議論はさておき、優れた機能を持つ実装の継承について、その利点と問題点を中心に話を進めます。

実装の継承では、あるクラスを、別のクラスのコードを再利用するように定義します。再利用される側のクラスを「スーパークラス」とよび、再利用する側のクラスを「サブクラス」とよびます。現バージョンのVisual Basicでは実装の継承をサポートしていません。このため、ここではJavaの例を挙げて実装の継承がどのようなものなのかを示すことにします。次のようなCDogというJavaクラスを調べてみましょう。

  // super class
class CDog
{
  // DOG STATE
  public String Name;
  // DOG BEHAVIOR
  public void Bark()
    {/* method implementation */}
  public void RollOver(int Rolls)
    {/* method implementation */}
}

クラスCDogには1個のプロパティと2つのメソッドがあります。ここでは、それぞれのメソッドが価値の高い実装内容で定義されていると仮定します。クラスの状態や動作は実装の継承によって再利用できます。CDogはスーパークラスとして使います。CDogを拡張したサブクラスは、クラスのプロパティおよびメソッドの両方の実装を継承します。次のJavaコードは実装の継承に必要な構文を示したものです。

  // sub class
class CBeagle extends CDog
{
  // BEAGLE STATE
  // Name property is inherited
  // A color property is added
  Public String Color;
  // BEAGLE BEHAVIOR
  // implementation of RollOver() is inherited
  // implementation of Bark() is overridden
  public void Bark()
    {/* CBeagle-specific implementation */}
  // CBeagle extends CDog by adding a new method
  public void FetchSlippers()
    {/* CBeagle-specific implementation */}
}

スーパークラスCDogから拡張されたサブクラスCBeagleは、CDogの既存のプロパティとメソッドの実装をすべて継承します。つまり、CBeagleはCDogで定義されている状態および動作のすべてを再利用できるわけです。その後、CBeagleに対してたとえば**Bark()**などの既存のメソッドをオーバーライドしたり、**FetchSlippers()**などの新しいメソッドを追加したりすることで、CDogを拡張できます。サブクラスの定義に新しいプロパティを追加することも可能です。

**図1:**実装の継承により、あるクラスが別のクラスのコードを再利用できます。サブクラスはスーパークラスのプロパティおよびメソッドの実装を継承し、メソッドのオーバーライドや、新しいプロパティまたはメソッドの追加によって拡張します

図2は、継承の階層を図示したもので、アプリケーションの中で使われている様々なクラスの相互関係を示しています。ここに示した階層は比較的単純なものですが、さらに複雑な階層を作ることもできます。たとえば、最上位のクラスとしてCAnimalを置き、順にCMammal、CDog、CTerrier、CScottieというように階層を拡張できます。このように継承の階層は大規模で複雑なものになり得ます。実際に使うコードの階層が5レベル以上になることはよくあります。

**図2:**アプリケーションの中のスーパークラスとサブクラスの関係を示す継承の階層

実装の継承を正しく利用すれば、コードの保守の面でも優れた威力を発揮します。スーパークラス内のメソッドの実装を改良すれば、継承の階層でそれ以下にあるすべてのクラスが自動的にその恩恵を受けます。たとえばCAnimalクラスのバグが修正されれば、ほかの多数のクラスもそれによって改良されることになります。継承の階層が大規模で複雑になってくると、最上位のクラスに加えられる変更が、その配下にある多数のクラスに大きな影響を及ぼすことがあります。このことは、1か所を変更しただけで多数のオブジェクト型の動作がその影響を受けることを意味しています。

ポリモーフィズムとは?

ここまでは、実装の継承によってメソッドの実装がどのように暗黙のうちに再利用されるのかについて説明し、実装の継承によってコードの重複がなくなり保守性が向上することを示しました。ここでは、実装の継承がもたらすOOPのもう1つの機能である「ポリモーフィズム」について解説します。ポリモーフィズムは、オブジェクト指向プログラミングにおいて最も重要な概念であるともいわれています。オブジェクトは様々なクラスから作成され、公開している動作も多種多様ですが、ポリモーフィズムのおかげでクライアントはそれらの様々なオブジェクトを共通の方法で扱うことができます。

C++やJavaなどの言語では、実装の継承を利用してポリモーフィズムを実現できます。たとえば、スーパークラス参照を使って、サブクラスのインスタンスに接続してメソッドを起動できます。図3は、クライアントがCDog参照を使用して、型の異なる3つのオブジェクトとやり取りする様子を示したものです。CDogから派生したサブクラスの型は、それぞれCDog参照と互換性があります。したがって、クライアントはCDog参照を使用して、CBeagle、CRetriever、またはCBoxerの型を持つオブジェクトとやり取りできます。

**図3:**スーパークラス参照を使ってサブクラスのインスタンスとやり取りすることでポリモーフィズムを実現できます。クライアントはCDog参照を使用して、CDogと互換性のあるすべてのオブジェクトとやり取りできます

クライアントでは、CDogクラスから拡張されたすべてのクラスが必ず**Bark()**メソッドの実装を提供するという前提のもとに、**Bark()メソッドを使用できます。クライアントは、サブクラスがCDogで実装されているBark()の定義をそのまま使用しているか、または独自の実装によってこのメソッドをオーバーライドしているかどうかにかかわりなく、CDogクラスで定義されている呼び出し構文に従って、単純にメソッドを起動します。しかし、サブクラスごとにBark()**の実装を独自に提供すれば、同じ要求に対してそれぞれのオブジェクト型が独自の方法で応答できます。次のJavaコードを見てみましょう。

  // method accepts any CDog-compatible object
Public void MakeDogBark(CDog Dog)
{
  // different objects can respond differently
  Dog.Bark()
}

このメソッドを、CBeagleオブジェクトを使って起動した場合と、CTerrierオブジェクトを使って起動した場合とでは、その結果がまったく異なることが予想されます。クライアントは、どのメソッドを呼び出すべきかは知っていますが、実行される**Bark()**メソッドがどのように実行されるのかはまったく知りません。呼び出し構文はコンパイル時に厳密に定義されますが、実際のメソッドの実装は実行時まで決まりません。ポリモーフィズムとは、このような実装コードの「動的バインディング」の考えに基づいた概念であり、従来の「静的バインディング」と対をなす概念です。動的バインディングはある程度の制御された「不確かさ」を提供し、それによってポリモーフィズムが威力を発揮します。アプリケーションの開発者はいわば「差し替え互換の」オブジェクトをもとにアプリケーションを作成できます。たとえば、何千行ものクライアント コードであっても、それがCDogクラスの公開インターフェイスを対象にして書かれたものであれば、CBeagleオブジェクトをCTerrierやCBoxerなどのオブジェクトに簡単に差し替えることができます。クライアント コードにはCDogに対する依存性はありますが、CDogから派生したクラスに対する依存性はありません。したがって、このような変更を加えてもクライアント コードに与える影響はごくわずかか、あるいはまったくありません。

実装の継承に関する問題

ここまで、実装の継承がもたらす2つの大きな利点である、メソッドの実装が暗黙のうちに再利用されることと、ポリモーフィズムについて説明しました。しかし、実装の継承に伴う潜在的な問題のいくつかについてはまだ触れていません。残念ながら、実装の継承はサブクラスとスーパークラスとの結び付きが非常に強いために、アプリケーションで利用した場合にクラス ベース参照と同じような依存性の問題に陥りやすい傾向にあります。

カプセル化を適切に利用すれば、実装の詳細をクライアントから隠し、クライアント コードを修正することなくクラスの実装の詳細を自由に変更できます。実装の継承の問題は、公開でないメンバーのカプセル化ができなくなるということです。実装の継承の機能を持つ言語は、アクセスのレベルとして「public(公開)」と「private(非公開)」のほかに、「protected(保護)」というレベルを提供しています。protectedを指定した、すなわち保護されたプロパティやメソッドは、クライアントからは隠され、サブクラスからはアクセスできます。したがって、サブクラスはクライアントからはアクセスできない実装の詳細にアクセスできます。スーパークラスの保護されたプロパティとメソッドの名前をサブクラスにハードコードすると、柔軟性のない依存コードの層がもう1つ生まれます。

実装の継承は、「ホワイトボックス型再利用」とよばれる開発の考え方の一例として挙げられます。この考え方に基づいて作成されたアプリケーションは、しばしば継承階層内のクラスどうしの結び付きが非常に強くなってしまうことがあります。保護されたプロパティやメソッドをサブクラスで使用してしまうと、その中に組み込んだ依存コードを修正しないかぎり、スーパークラスのシグネチャの変更や削除ができなくなります。大きな継承階層を持つアプリケーションでは、このためにアプリケーションの柔軟性が失われます。階層の最上位のクラスを変更するために、多数のサブクラスを変更しなければならなくなります。アプリケーションによっては、階層の最上位のメソッドのシグネチャやプロパティの型を変更するために、その配下に連なる数十あるいは数百というクラスを修正しなければならなくなる場合もあります。逆に、公開または保護されている主要なスーパークラスのインターフェイスをそのまま固定してしまうと、普通はそのアプリケーションが改良できなくなることになります。

簡単なクラスを設計する場合であっても、プロパティやメソッドにprotectedのアクセスを与えるかどうかを決めるときは、十分検討しなければなりません。実装の継承を利用しつつ適切な設計を行うためには、柔軟性のないスーパークラスの生成を防ぐための高度な技術や原則が必要になります。将来そのクラスを別のサブクラスによって拡張するかどうかをあらかじめ決めておくことが必要です。クラスを拡張する予定がある場合は、実装の詳細をカプセル化する際に、クライアントから実装を分離しておくことも重要ですが、それらをサブクラスから分離することも同様に重要です。

このように説明すると実装の継承が不便なもののように思われるかもしれませんが、決してそうではなく、開発対象が適切であれば強力な機能になります。実装の継承は、比較的規模の小さい開発に最も適しています。アプリケーションの必要条件に合わせていくらでも大きな継承階層を作成すると、ごく一部の熟練したオブジェクト指向設計者だけしか管理できなくなってしまいます。

C++やSmalltalkが初めて導入されたとき、OOPの普及に努めていた人たちは、実装の継承こそがコードの再利用を実現する究極の技術であるとして過大な評価をしていました。その結果、「ホワイトボックス型再利用」に伴うクラスどうしの結び付きの問題を理解していなかった設計者たちは、この機能の使い方を誤ってしまったのです。実装の継承を不用意に利用した結果、ここ10年余りのあいだに、進化する能力のない大規模なシステムが数多く作られてしまいました。しかし、実装の継承が小規模な開発に最も適していることを理解していた熟練開発者たちは、大規模なシステムにおいても再利用を実現するようなより柔軟な方法がないか探し続けました。特に、規模の比較的大きいシステムにおいて拡張性を損わずに再利用を実現する手段について模索を続けました。彼らは、より規模が大きく、進化型の設計に対応できるような、オブジェクト指向に基づく再利用のしくみを求めていました。こうした努力の結果、インターフェイス ベース プログラミングと「オブジェクト合成」とよばれる開発方式が誕生したのです。

インターフェイスの実装からの分離

オブジェクト合成は、クラスどうしの強い結び付きがなるべく起こらないようにコードの再利用を実現する方法の1つです。オブジェクト合成は「ブラックボックス型再利用」という考え方に基づいています。この考え方では、クラスの実装の詳細がクライアントに公開されることがありません。クライアントは、利用できる一連のリクエストだけを知っており、オブジェクトの応答方法の詳細は知りません。つまり、「何」を要求できるのかは知っていますが、「どのようにして」応答するのかは知らないのです。

ブラックボックス型再利用という概念は、インターフェイスと実装を完全に分離するという考え方に基づいています。つまり、インターフェイスを独立のものとして扱うわけです。インターフェイスは独自の定義を持つ独立したデータ型です。これは、クラス定義の一環として公開インターフェイスを定義する従来のOOPが進化した形と考えることができます。

このような説明では漠然としていてよくわからないという読者もいると思います。「インターフェイスとは一体何なのか?」と自問する方もいるかもしれません。インターフェイスとはソフトウェアを書くためのまったく新しい手法であるため、残念ながらその明確な定義を示して中心となる概念を説明することは困難です。インターフェイスを説明する方法は様々です。構文はすぐに覚えられるので、インターフェイスの定義、実装、使用もすぐにできるでしょう。しかし、インターフェイスがソフトウェアの設計に与える潜在的な影響を一般のプログラマが理解することははるかに困難です。インターフェイスによる設計方法を理解するには、通常は数か月から数年かかります。

最も基本的なレベルでいえば、「インターフェイスは公開されたメソッドシグネチャの集まり」です。インターフェイスは、論理的に関係のある一連のクライアント リクエストに対する呼び出し構文を定義します。ただし、インターフェイスではメソッドのシグネチャは定義できますが、その中に実装やデータ プロパティを含めることはできません。クラスとそれを使用するクライアントとの間に間接的な層を設けることで、クラスとクライアントを分離します。したがって、インターフェイスを有益に利用するためには、インターフェイスを1つ以上のクラスによって実装することが必要になります。クラスを使ってインターフェイスを実装すれば、クライアントはそのクラスに基づくオブジェクトを作成し、インターフェイス参照を通じてオブジェクトとやり取りできます。

インターフェイスからはオブジェクト参照を作成できますが、オブジェクトそのものは作成できません。オブジェクトはインターフェイスが提供できないデータ プロパティやメソッドの実装を必要とします。したがって、インターフェイスがオブジェクト参照しか作成できないということには合理的な意味があります。インターフェイスは作成可能な実体ではないため、「インターフェイスは抽象データ型」です。一方、オブジェクトのインスタンスは作成可能なクラスからのみ生成できますが、このような作成可能な実体のことを「具象データ型」とよびます。

設計上の観点からいえば、「インターフェイスは一種の取り決め」であるといえます。インターフェイスを実装しているクラスは、そのクラスに基づいて生成されるオブジェクトが特定の動作をサポートしていることを保証します。より具体的にいうと、クラスはインターフェイスが定義している個々のメソッドに対応する実装を必ず提供しなければならないのです。クライアントは、インターフェイス参照を通じてオブジェクトとやり取りするときに、インターフェイスで定義されているメソッドごとにオブジェクトが必ず適切な応答を返すという前提のもとに動作できます。

同じインターフェイスを複数のクラスで実装することもできます。インターフェイスの呼び出し構文の定義は厳密ですが、各メソッドの意味付けは緩やかです。そのため各クラスの作成者は、メソッドごとの適切なオブジェクト動作を決める際に、ある程度の自由度があります。たとえば、IDogインターフェイスで**Bark()というメソッドを定義した場合、各クラスの作成者は同じBark()リクエストに対して「犬がほえる」という概念に従っているかぎり、異なる応答を返すことができます。たとえば、CBeagleクラスではCTerrierやCBoxerとは異なるBark()**を実装できます。つまり、「インターフェイスはポリモーフィズムを実現するためのしくみを提供する」ことになります。インターフェイスは、差し替え互換のオブジェクトに基づいてアプリケーションを構築できるという点では、実装の継承に似ています。しかし、インターフェイスの場合は、実装の継承やホワイトボックス型再利用で発生するクラスどうしの強い結び付きを起こさずに、差し替えの互換性を実現します。

継承の2つの形体

継承とは、2つの実体のあいだで成り立つ「~は~である」という関係をモデルとしたオブジェクト指向の概念です。本稿ではこれまで「実装の継承」という言葉を使い、より一般的な「継承」という言葉を使っていませんでしたが、これは「~は~である」という関係を活用する方法としては、スーパークラスをサブクラスで拡張するというのは1つの方法にすぎないからです。クラスの中でインターフェイスを実装するときも、この「~は~である」という関係を利用することができるのです。たとえば、CBeagleクラスがIDogインターフェイスを実装する場合、「ビーグルは犬である」という文が成り立ちます。したがって、IDogと互換性のあるオブジェクトを必要とする任意の箇所で、CBeagleオブジェクトを使用できます。

インターフェイス ベース プログラミングは、継承のもう1つの形体である「インターフェイスの継承」に基づいています。つまり、継承ではメソッドの実装の再利用が必ずしも必要ではないことになります。継承の唯一の真の要件は、サブクラスのインスタンスが、継承元の基本型と互換性を持っていることです。継承元の基本型は、クラスか、または定義済みのインターフェイスのどちらかになります。どちらの場合でも、基本型参照を使って各種の型のオブジェクトとやり取りできます。すなわち、どちらの形体の継承でもポリモーフィズムの実現が可能だということです。

どちらの継承の形体も、ともにポリモーフィズムを提供するという点では同じですが、カプセル化の方法は互いにまったく異なります。実装の継承はホワイトボックス型再利用に基づいており、サブクラスは拡張元クラスの詳細を必要なだけ知ることができます。このため、サブクラスではスーパークラスのメソッドの実装やデータ プロパティを暗黙のうちに再利用できることになります。実装の継承は、状態や動作の再利用という点ではインターフェイスの継承よりもはるかに優れています。しかし、この再利用には代償が伴います。ホワイトボックス型再利用によってカプセル化ができなくなり、大規模な設計ではスケーラビリティに限界が生じます。

一方、ブラックボックス型再利用という言葉が示すとおり、「インターフェイスの継承はカプセル化の概念を強制します」。クラス内部で実装の詳細を厳密にカプセル化することによって、よりスケーラブルなアプリケーションの設計が可能になります。インターフェイス ベース プログラミングは、ホワイトボックス型再利用に関する問題の多くを解決します。しかし、このようなプログラミングのスタイルを正しく評価するためには、代償も大きいが恩恵のほうがさらに大きいという事実を受け入れなければなりません。この点が多くのプログラマの悩むところです。

クラスでインターフェイスを実装するときは、メソッドのセットを提供する必要があります。サブクラスの作成者は、インターフェイスを実装する場合に必ず追加のコードを書かなければなりません。この作業は実装の継承と比較してはるかに大変な作業のように思えます。クラスから継承する場合は必要な作業のほとんどがすでに完了していますが、インターフェイスから継承する場合にはまだたくさんの作業が残っています。直感的には、実装の継承は手軽であり、インターフェイスの実装は手間がかかるという印象を受けます。しかし、手軽なものを選択したいという欲求を抑えて、インターフェイスというより高いレベルを選択しなければなりません。インターフェイスの継承が実装の継承よりも優れている最も重要な点は、アプリケーションの拡張性を阻む原因となるクラスどうしの強い結び付きが起こりにくいという点です。

Visual Basicでのインターフェイスの使用

Visual Basic 5.0は、ユーザー定義インターフェイスを初めてサポートしたVisual Basicのバージョンです。Visual Basicのプロジェクトでは、次の3つの必須手順を実行することで、インターフェイス ベース プログラミングの恩恵を受けることができます。

  1. インターフェイスを定義する。
  2. 作成可能な1つまたは複数のクラスでインターフェイスを実装する。
  3. クライアントの中でインターフェイス参照を使ってオブジェクトとやり取りする。

このように、アプリケーションにインターフェイスを追加するための基本的な手順は非常に簡単です。またインターフェイスを使うことで、アプリケーションの設計にポリモーフィズムを加えることができます。ここでは簡単な例をとおして、これらの手順の実行に必要なVisual Basicの構文を示します。

Visual Basicでは、標準のクラス モジュールを使ってカスタムのインターフェイスを定義します。Visual BasicのIDE(統合開発環境)でインターフェイスを定義するための専用のエディタが利用できればよいのですが、残念ながら現バージョンではインターフェイス作成用のエディタは付属していません。ここではクラス モジュール エディタを使って、インターフェイス定義とインターフェイス クラスの両方を作成します。

新しいインターフェイスを定義するには、既存のプロジェクトに新しいクラス モジュールを追加して、適切な名前を付けるだけです。たとえば犬の動作を表現するインターフェイスを作成しようとするのであれば、IDogまたはitfDogという名前を付けます。この2つの名前の付け方はVisual Basicの開発者たちのあいだで最も一般的に使われている命名規則です。また、作業中のVisual BasicプロジェクトがActiveX(r) DLLまたはActiveX EXEのどちらかの場合には、クラス モジュールのインスタンス生成プロパティをPublicNotCreatableに設定してください。インターフェイスは抽象的なデータ型を表すため、この設定が必要です。標準EXEプロジェクトでは、クラス モジュールにインスタンス生成プロパティはありません。

インターフェイスを定義するには、公開メソッドのセットに対する呼び出し構文を作成します。インターフェイスの中にこれらのメソッド用の実装コードを含めてはいけません。必要なのはシグネチャの定義だけです。要するに、ここではメソッドが何を行うかではなく、クライアントがメソッドをどのように呼び出すかを定義するのです。次の例は、Visual Basicのクラス モジュールで定義したIDogインターフェイスの例です。

  ' (class module IDog.cls)
' IDog expresses behavior of a dog object
Public Property Get Name() As String
End Property
Public Property Let Name(ByVal Value As String)
End Property
Public Sub Bark()
End Sub
Public Sub RollOver(ByVal Rolls As Integer)
End Sub

最初に目につくのが、End Sub、End Function、またはEnd Propertyといった記述です。Visual Basicでインターフェイスを宣言するときは、これらのEndステートメントを各メソッドのシグネチャの最後に記述します。これらは実際にはインターフェイス定義の中で特に意味を持っていません。ふつう、キーワードEndはメソッドの実装の終わりを示します。これはVisual BasicのIDEに特有の表記法であり、Visual Basicのクラス モジュールを使ってクラスとインターフェイスの両方を定義する際に生じる、あまり好ましくない副作用です。おそらく将来のバージョンのVisual Basicでは、End Sub、End Function、またはEnd Propertyを必要としないインターフェイス定義専用のモジュール型が提供されるでしょうが、今のところは我慢するしかありません。

もう1つ重要な点は、このインターフェイスがメソッドのほかに論理プロパティを使用できるという点です。論理プロパテイが実際にはデータ プロパティではなくメソッドのセットであると考えれば、これは合理的です。クライアントは、前出のインターフェイスで定義したNameという論理プロパティを標準のデータ プロパティとまったく同様に使用できます。ただし、このプロパティはProperty Let/Property Getというメソッドのペアとして実装する必要があります。

ここで次のことを考えてみましょう。なぜインターフェイスにデータ メンバーを含めることができないのか?これは、インターフェイスはクラスとは違い、オブジェクトの作成に使われることが決してないからです。インターフェイスの目的はクラスの実装の詳細をカプセル化することにあります。オブジェクトのデータ配置は、クラス定義の中でカプセル化しなければならない最も重要な詳細の1つです。仮にインターフェイスに実際のデータ メンバーが含まれていたとしたら、クライアントがそれらのデータ メンバーに依存することになってしまいます。依存性が好ましくないことについてはすでに説明したとおりです。

インターフェイスにデータ プロパティを含めることはできませんが、Visual Basicではインターフェイスの定義に使っているクラス モジュールの中で、次のように記述することでデータ プロパティを定義できます。

  Public Name As String

ただし、クラス モジュール内でデータ プロパティを定義すると、データ プロパティをインターフェイス定義として使うときに、Visual Basicはそれらのプロパティを論理プロパティとして自動的に再定義します。インターフェイスの作成時には、Visual Basicの持つこの機能がとても便利です。こうして定義したばかりのNameプロパティも、インターフェイスを実装するすべてのクラスの中でProperty LetProperty Getが必要です。また、インターフェイスを実装してもクラス定義のデータ配置は影響を受けないことに注意してください。このインターフェイスを実装するすべてのクラスには、犬の名前を物理的に格納するための非公開のデータ プロパティがなければなりません。

インターフェイス定義を作成したら、次にそのインターフェイスを実装する具象クラスを作成します。2つ目のクラス モジュールをプロジェクトに追加し、適切な名前を付けます。たとえば、IDogインターフェイスを実装するCBeagleという具象クラスを作成できます。クラス モジュールの先頭でキーワードImplementsを使います。ステートメントは次のようになります。

  Implements Idog

クラス モジュールにこの行を追加したら、インターフェイス内のすべてのメソッドと論理プロパティに対応する実装を、クラス モジュールの中に用意する必要があります。実装がすべてそろっているかどうかはVisual Basicのコンパイラがチェックします。すべての実装をそろえないとコードはコンパイルできません。たとえば、IDogインターフェイスの**Bark()**メソッドの実装では、次のような定義が必要になります。

  Private Sub IDog_Bark()
  ' implementation
End Sub

Visual Basicによるインターフェイスのマッピングでは、各メソッドの実装において、インターフェイス名の後にアンダスコアとメソッド名を付けた名前を使うことが要求されます。Visual Basicは、特定のインターフェイスが使用されると、この独自の構文を使用して、オブジェクトへのエントリポイントを作成します。Visual Basicコンパイラを使う場合も、インターフェイス内の個々のメソッドや論理プロパティについて、同様の名前を付ける必要があります。これにより、クラスから作成されるオブジェクトが、各インターフェイスのメンバーに対するエントリポイントを提供することが保証されます。

幸い、クラス モジュールの先頭でキーワードImplementsを使えば、Visual BasicのIDEを利用してメソッドの実装用のプロシージャ スタブを簡単に作成できます。クラス モジュールのエディタ ウィンドウにはウィザード バーがあり、その中に2つのドロップダウン コンボ ボックスがあります。図4のように、左側のコンボ ボックスでインターフェイス名を選択し、右側のコンボ ボックスでメソッド名を選択すると、メソッドの実装用のスケルトンを簡単に生成できます。

**図4:**ユーザー定義インターフェイスの実装でウィザード バーを利用すると、プロシージャのスケルトンを簡単に作成できます

  Implements IDog
Private Name As String
Private Property Let IDog_Name(ByVal Value As String)
  Name = Value
End Property
Private Property Get IDog_Name() As String
  IDog_Name = Name
End Property
Private Sub IDog_Bark()
  ' implementation
End Sub
Private Sub IDog_RollOver(ByVal Rolls As Integer)
  ' implementation
End Sub

このサンプル コードは、IDogインターフェイスを実装するCBeagleクラスの実装の一部分です。ウィザード バーが生成するメソッドの実装にはPrivateが指定されます。つまり、これらのメソッドの実装はCBeagle参照を使用するクライアントでは利用できません。これらはIDogインターフェイスを使用するクライアントだけが利用できます。また前出のコードは、CBeagleクラスの中で非公開のデータ プロパティを定義してProperty LetProperty Getの各メソッドを実装することによって、論理プロパティNameを実装できることを示しています。

インターフェイスとそれを実装するクラスが作成できたら、実際にインターフェイスを使ってオブジェクトとやり取りできます。たとえば、クライアントからIDog参照を通じてCBeagleオブジェクトとやり取りできます。IDog参照を使い、インターフェイスで公開されているメソッドを起動できます。簡単な例を次に示します。

  Dim Dog As IDog
Set Dog = New CBeagle
' access object through interface reference
Dog.Name = "Spot"
Dog.Bark
Dog.RollOver 12

インターフェイス参照を通じてオブジェクトに接続したクライアントは、メソッドを起動したり論理プロパティにアクセスしたりできます。Visual BasicのIDEは、クラス ベース参照を使う場合と同じMicrosoft IntelliSense(r)、型検査、デバッグの各機能が利用できます。New演算子の後にはインターフェイスを使用できないので注意してください。インターフェイスは作成可能な型ではありません。New演算子を使うときは、必ずCBeagleなどの具象クラスを使ってオブジェクトを作成するようにしましょう。

なぜインターフェイスを使うのか?

アプリケーションの中でのインターフェイスを使う方法を学んでいるVisual Basicプログラマは、しばしば「こんなことをする必要があるのだろうか?」とか「私が気にすべきことなのだろうか?」と自問するはずです。複雑な概念を習得しなければならないユーザー定義インターフェイスに比べて、クラス ベース参照によるプログラミングははるかに簡単なことのように感じられます。たとえば、前出の例でいえば、IDogインターフェイスではなくCBeagleクラスの公開メソッドやプロパティに基づいてクライアント コードを書けば、ずっと簡単なものになっていたでしょう。ユーザー定義インターフェイスには実際には利点などなく、余計な作業をしているように思えます。

Visual BasicまたはCOMのプログラマがインターフェイスについて関心を持たなければならないことには、いくつか重要な理由があります。その理由の1つは、インターフェイスがCOMの土台になっていることです。COMの世界では、クライアントはクラス ベース参照を使用できず、その代わりにインターフェイス参照を通じてCOMオブジェクトにアクセスしなければなりません。実は、Visual Basicはこうした要求の複雑さをきわめて効率よく隠すことができます。クラス ベース参照を使うと、Visual Basicはクラス用のデフォルトのCOMインターフェイスを裏で生成します。つまり、Visual Basicではユーザー定義インターフェイスを自分でいじらなくてもCOMベースのアプリケーションを作成できるということです。しかし、インターフェイス ベース プログラミングを理解すれば、今までよりもずっとレベルの高いCOMプログラムを作れるようになります。

インターフェイスについて関心を持たなければならないもう1つの理由は、インターフェイスがソフトウェアの設計に優れた威力と柔軟性を与えることができるからです。Visual Basicで、クラスと公開インターフェイスが1対1で対応しないとき、ユーザー定義インターフェイスを使うと非常に便利です。たとえば、一般的な例として2つのシナリオが考えられます。1つは、1個のインターフェイスを作成して複数のクラスに実装する方法であり、もう1つは、複数のインターフェイスを1個のクラスの中で実装する方法です。どちらの場合でも、具象クラス ベースの参照をクライアントが使用しなければならないアプリケーション デザインよりも利点があります。インターフェイス ベースのデザインではしばしば複雑さが増すこともありますが、それを使ってできることには限りがありません。

多数のクラスで同じ1個のインターフェイスを実装する場合を考えてみましょう。たとえば、CBeagle、CTerrier、CBoxerのクラスがすべてIDogインターフェイスを実装するとします。この場合、アプリケーションは次のコードを使ってIDogと互換性のあるオブジェクトのコレクションを管理できます。

  Dim Dog1 As IDog, Dog2 As IDog, Dog3 As IDog
' create and initialize dogs
Set Dog1 = New CBeagle
Dog1.Name = "Mo"
Set Dog2 = New CTerrier
Dog2.Name = "Larry"
Set Dog3 = New CBoxer
Dog3.Name = "Curly"
' add dogs to a collection
Dim Dogs As New Collection
Dogs.Add Dog1
Dogs.Add Dog2
Dogs.Add Dog3

アプリケーションは、IDogと互換性のあるオブジェクトをすべて同じように扱うことで、ポリモーフィズムのある動作を実現できます。次の例は、コレクションを列挙して各オブジェクトのBark()メソッドを起動するコードです。

  Dim Dog As IDog
For Each Dog In Dogs
  Dog.Bark
Next dog

このアプリケーションが進化するにつれて、このコレクションを変更してIDogと互換性のあるいろいろなオブジェクトを組み入れることができます。CBeagle、CTerrier、CBoxerなどから作成したオブジェクトはもちろん、IDogインターフェイスを実装するように書かれたクラスなら何でも組み込めます。この例のFor EachはIDogインターフェイスに基づいて書かれており、どの具象クラスにも依存していません。新しい具象クラス型をアプリケーションに導入するときも、このループを変更する必要がありません。

もう1つの強力なデザイン技法は、1つのクラスに複数のインターフェイスを実装する技法です。この技法を使うと複数のインターフェイスをサポートするオブジェクトを作成でき、したがって複数の動作をサポートできます。この技法は「実行時型検査」のしくみと組み合わせて使うと威力を発揮します。たとえば、次のメソッドを持つIWonderDogという別のインターフェイスをサンプル アプリケーションに追加するとします。

  Sub FetchSlippers()
End Sub

ここで、CBeagleクラスはIWonderDogを実装していて、CTerrierクラスはIWonderDogを実装していないとします。クライアントは実行時にオブジェクトを検査して、オブジェクトが特定のインターフェイスをサポートしているかどうかを調べることができます。オブジェクトが特定のインターフェイスをサポートしていれば、クライアントはその機能を要求できます。サポートしていなければ、クライアントは秩序正しく機能を限定することができます。次のコードでは、Visual BasicのTypeOfの構文を使ってIWonderDogがサポートされているかどうかを調べています。

  Dim Dog1 As IDog, Dog2 As IDog
Set Dog1 = New CBeagle
Set Dog2 = New CTerrier
If TypeOf Dog1 Is IWonderDog Then
  Dim WonderDog1 As IWonderDog
  Set WonderDog1 = Dog1
  WonderDog1.FetchSlippers
End If
If TypeOf Dog2 Is IWonderDog Then
  Dim WonderDog2 As IWonderDog
  Set WonderDog2 = Dog2
  WonderDog2.FetchSlippers
End If

クライアントはCBeagleオブジェクトを調べて、IWonderDogと互換性があること、つまり、CBeagleオブジェクトはIWonderDogインターフェイスをサポートしていることを知ります。この場合、クライアントはIWonderDog参照を作成し、Setステートメントを使ってIDog参照を型変換することによって、IWonderDog参照にCBeagleオブジェクトを割り当てることができます。IWonderDog参照を作成したら、クライアントは**FetchSlippers()**を正しく呼び出せます。ここでは2つの参照がありますが、オブジェクトは1つしかないことに注意してください。インターフェイスが複数あるときは、1個のオブジェクトに対する複数の参照を使ってすべての機能を調べることになるため、クライアントのコードが少し複雑です。

一方、CTerrierオブジェクトについてIWonderDogとの互換性を調べると、インターフェイスがサポートされていないことがわかります。これでクライアントは秩序正しく機能を限定することができます。クライアント コードでは、次のようにIDogと互換性のあるオブジェクトのコレクションについてオブジェクトを列挙し、IWonderDogインターフェイスをサポートするオブジェクトの**FetchSlippers()**を正しく呼び出せます。

  Dim Dog As IDog, WonderDog As IWonderDog
For Each Dog In Dogs
  If TypeOf Dog Is IWonderDog Then
    Set WonderDog = Dog
    WonderDog.FetchSlippers
  End If
Next dog

このようにして実行時にオブジェクトの機能を調べる機能は、アプリケーションの進化にとってきわめて有用であることは容易に想像できます。CBoxerクラスが将来のバージョンでIWonderDogインターフェイスを実装したとすれば、このコードのFor Eachループを修正することなく、そのままそのインターフェイスを利用できます。クライアントのコードは、オブジェクトの将来のバージョンで機能がサポートされた場合に備えておけるわけです。

オブジェクトの拡張

前出の例では、1個のオブジェクトを使って複数のインターフェイスをサポートする方法について説明しました。ユーザー定義インターフェイスを応用すれば、既存のメソッドのシグネチャのセットに限界が生じたときに、オブジェクトの動作を安全に拡張することができます。たとえば、IDogインターフェイスで**RollOver()**メソッドを次のように定義しているとします。

  Public Sub RollOver(ByVal Rolls As Integer)
End Sub

アプリケーションの中でdogオブジェクトの機能を拡張して、クライアントがより大きな整数値を渡せるようにしたい場合には、2番目のインターフェイスとしてIDog2というインターフェイスを作成できます。IDog2インターフェイスはIDogと同じメンバーを定義していますが、**RollOver()**メソッドだけは次のように定義しています。

  Public Sub RollOver(ByVal Rolls As Long)
End Sub

新しいクライアントは、IDogオブジェクトが新しい動作をサポートしているか調べることができます。新しい動作をサポートしていなければ、クライアントは単純に古い動作を実行できます。このコード例を次に示します。

  Sub ExerciseDog(Dog As IDog)
  If TypeOf Dog Is IDog2 Then
    ' use new behavior if supported
    Dim Dog2 As IDog2, lRolls As Long
    Set Dog2 = Dog
    lRolls = 50000
    Dog2.RollOver lRolls
  Else
    ' use to older behavior if necessary
    Dim iRolls As Integer
    iRolls = 20000
    Dog.RollOver iRolls
  End If
End Sub

このようなバージョン別処理で注目すべきことは、古いクライアントと古いオブジェクトをそのまま残しつつ、新しいクライアントと新しいオブジェクトをアプリケーションに導入できるということです。新しいオブジェクトでは、古いバージョンのインターフェイスを引き続きサポートすることで、古いクライアントに対応できます。新しいクライアントは、必要に応じて古いインターフェイスを使用することで、古いオブジェクトに対処します。インターフェイスというものがなかったとしたら、オブジェクトを拡張するためにしばしばすべてのクライアントを修正しなければならなかったでしょう。また、クライアントを修正するときも、すべてのオブジェクトを修正しなければならなかったでしょう。インターフェイス ベース プログラミングのおかげで可能となるバージョン別処理によって、すでに製品となっているコードにほとんどあるいはまったく影響を与えることなく、アプリケーションに小さな変更を加えることができます。

ユーザー定義インターフェイスを持つアプリケーションの設計

本稿では、インターフェイス ベース プログラミングの中心となる概念を示すために簡単なアプリケーションの例を紹介してきましたが、実際のアプリケーションではこれらの概念をどのように応用できるのでしょう。たとえば、顧客オブジェクトを使用する大規模なアプリケーションを設計するのであれば、CCustomerというような具象クラスではなく、まずICustomerというユーザー定義インターフェイスを作成し、そのインターフェイスに対して多数のクライアント コードを記述することから作業を開始できます。ICustomerインターフェイスを実装するクラスをいくつか作成すれば、ポリモーフィズムがもたらすプラグ&プレイ方式を活用することができます。顧客オブジェクトごとに動作が違っても、それらすべてを共通のインターフェイスを通じて制御できます。

バージョン別処理の観点から見れば、このような設計手法で新しいインターフェイスをアプリケーションに導入することにより、様々な顧客オブジェクトの動作を進化させることができるようになります。ICustomer2、ICustomer3、ICustomer4などとインターフェイスを順次導入するだけで、顧客オブジェクトの動作を安全に拡張できます。この手法の最も大きな利点は、クライアントとオブジェクトを別々に改訂できることです。古いクライアントとオブジェクトは以前のインターフェイスを使用できる一方で、新しいクライアントとオブジェクトは新しいインターフェイスを通じてやり取りできます。このような動作はすべて、インターフェイスをサポートするための実行時型検査のおかげで可能になります。

アプリケーション デザインにおけるユーザー定義インターフェイスのもう1つの応用例として、Microsoft Transaction Server(MTS)があります。MTSでは「ロール」に基づく宣言型のセキュリティ モデルが導入されています。MTSの管理者は、Microsoft Windows NT(r)のユーザー アカウントとグループ アカウントをMTSのロールに割り当てます。その後で、クラス レベルとインターフェイス レベルの両方について、MTSのコンポーネントへのアクセス権限をそれぞれのロールに設定できます。このとき、ICustomerReadおよびICustomerWriteというインターフェイスを作成して、さらに細かい制御を実現することも考えられます。この2つのインターフェイスをCCustomerに実装することで、あるユーザー群には両方のインターフェイスを通じたオブジェクトへのアクセスを許し、別のユーザー群には読み取り専用アクセスだけを許すといった、細かなセキュリティの設定が簡単に行えるようになります。ユーザー定義インターフェイスのおかげで、MTSのセキュリティ モデルの威力がずっと高まります。

おわりに

ソフトウェア業界では、従来一般的だったクラス ベース参照や実装の継承などの技術に限界が見えてきたことから、インターフェイス ベース プログラミングが採用されてきています。ユーザー定義インターフェイスの登場は、アプリケーション デザインとアプリケーション プログラミングの両面において新しい困難や課題を生み出してはいますが、大規模なアプリケーションでは明らかにユーザー定義インターフェイスの価値が評価されています。ダーウィンの進化論風にいえば、インターフェイス ベース プログラミングのおかげでソフトウェアが生き残りのために適応できるようになったといえるでしょう。インターフェイスの出現によって、コードの再利用、保守、および拡張が容易に行えるようになりました。

COMはどこまでもインターフェイス ベース プログラミングを基にしています。COMでは、インターフェイスと実装とを完全に分離することが必要です。つまり、クライアントはインターフェイス参照を通じてのみオブジェクトとやり取りすることが要求されます。これにより、クライアントはオブジェクトを生成するクラスにまったく依存しなくなります。COMプログラマはクライアント コードが動かなくなるという心配をせずに、自分のオブジェクト コードを改訂できるようになります。COMクライアントは、オブジェクトから実行時に型情報を取得できます。また、いつでもオブジェクトに対して特定のインターフェイスをサポートしているかどうかを問い合わせることができます。要求したインターフェイスがサポートされていなければ、クライアントはそれを検知して、秩序正しく機能を限定することができます。このため、プログラマはコンポーネントとアプリケーションを別々に改訂できます。クライアントとオブジェクトの古いものと新しいものどうしが共存できるのです。ここにCOMにおけるバージョン別処理の鍵があります。