オブジェクト指向プログラミングとインターフェース
インターフェースは入門書の後半に学ぶ
オブジェクト指向プログラミングをソフトウェア工学的に学ぶとすると、クラス、インスタンス、継承、カプセル化、多態性、オーバーロード、オーバーライド、コンストラクタ等を学ぶことになります。そのうちの一つにインターフェースがあります。しかし、いきなり学ぶことはあまりありません。入門書であれば後半の章に書かれることになるでしょう。
初期のオブジェクト指向プログラミングにはインターフェースはない
オブジェクト指向プログラミングの歴史をさかのぼるとSimulaそしてSmalltalkまでさかのぼることができます。Simulaにオブジェクトという考えがあり、Smalltalkでパロアルト研究所のアラン・ケイ的な知と結びつきここまで展開されたと言えるでしょう。とはいえ、この段階においてはクラスや継承という考えは生まれていますが、インターフェースはまだ生まれていません。
インターフェースはObjective-Cから生まれた
初期のオブジェクト指向のプログラミング言語がSimulaやSmalltalkだとすれば、続くのはC++、C#、Objective-C、そしてJavaでしょう。インターフェースという考えが生まれたのはObjective-Cと言ってよいです。
Objective-Cはクラスの定義をインターフェースと実装に分ける
Objective-Cではクラスの定義をヘッダ(.h)とメイン(.m)に分けて書きます。
@interface TestClass : NSObject {
}
- (void)method;
@end
@implementation TestClass {
}
- (void)method {
...
}
@end
@interface
と @implementation
という構文(コンパイラディレクティヴ)から分かるように、クラスの定義はインターフェースと実装から成り立つ、という考えになっています。インターフェースはフィールドやメソッドの名前やクラスそして引数や返値についてしか定めていません。それが呼び出されたときにどのように評価されるかまでは定めていないわけです。
いろいろな言い方ができると思います。たとえば、クラス図を書くときにはインターフェースだけで十分かもしれません。つまり、実装前の設計的なものなわけです。クラスの定義から実装を差し引けばインターフェースなので。
Javaのクラス定義はインターフェースと実装を合わせて書く
Javaではインターフェースと実装を合わせて書きます。
public class TestClass {
public void method(){
...
}
}
「...」で省略している部分が実装になります。Javaのクラスの定義ではこれを書かなければなりません。もちろんObjective-Cでも @implementation
がなければクラスの定義は未完成です。そのため、Javaではクラスの定義にインターフェースと実装があることはあまり意識されないでしょう。
Objective-Cではプロトコルがインターフェースのみを定める
Objective-Cでは @interface
の代わりに @protocol
と書くことでインターフェースのみを定義することができます。
@protocol TestProtocol <NSObject> {
}
- (void)method;
@end
Objective-Cではこのプロトコルに準拠したクラスを定めるという方法で実装されます。
@interface TestImpl : NSObject <TestProtocl> {
}
- (void)method;
@end
@implementation TestImpl {
}
- (void)method {
...
}
@end
クラス TestImpl は プロトコル TestProtocol に準拠したクラスです。そのため、プロトコルの未実装の部分をここで実装することになります。準拠 という言い回しから分かる通り、プロトコルは実装について統制を与えられます。
Javaではclassの代わりにinterfaceでインターフェースのみを定義する
Javaのクラス定義はインターフェースと実装を合わせて書きましたが、class
の代わりに interface
にすることでインターフェースのみを定義することができます。
public interface TestInterface {
public void method();
}
このインターフェースは実装がありません。実装はクラス定義にimplements
という構文で書かれます。
public class TestImpl implements TestInterface {
public void method(){
...
}
}
インターフェースは多重継承問題を回避する
なぜインターフェースと実装を分ける必要があるのか。その理由としてしばしば言われるのが多重継承です。クラス定義が必ず実装付きだとすると多重継承をしたときに実装上の競合が起こりえます。たとえば、TestClass1, TestClass2を継承するTestClass1が次のように定義されたとします。
public class TestClass1 {
public void method(){
...
}
}
...
public class TestClass2 {
public void method(){
...
}
}
...
public class TestClass extends TestClass1, TestClass2 {
public void method(){
super.method();
}
}
TestClass1とTestClass2にそれぞれの実装が書かれているため継承先で継承元を呼び出すときに競合が起きます(菱形問題)。そのため、Javaではクラスの多重継承は認められていません。
これが実装のないインターフェースであれば実装をひとつに集約できるのでこの問題を回避できます。
public class TestInterface1 {
public void method();
}
...
public class TestInterface2 {
public void method();
}
...
public class TestImpl implements TestInterface1, TestInterface2 {
public void method(){
...
}
}
上記コードでは実装がひとつしか現れていないため、競合が回避されています。
インターフェースは結合度を疎にする
インターフェースであれば実装前にあらかじめ呼び出しを作ることができます。
public class Test {
public void test(TestInterface test){
test.method();
}
}
Javaでは上記のコードは許されます。TestInterfaceの引数を受け取りインターフェースで定められているメソッドmethodを呼び出すだけだからです。つまり、インターフェースで完結しているわけです。このときTestInterfaceを実装するクラスがまだ用意されている必要はありません。たとえば、このtest
メソッドが呼び出されるときにはTestInterfaceを実装したクラスのインスタンスが引数に与えられるため実装済みであることが保証されるからです。
かつ、TestInterfaceの実装は複数あり得ます。にもかかわらず、このTestクラスはどの実装クラスが呼び出されるかに依存しません。つまり、結合が疎である、ということです。実装クラスがどのように実装をされてもこのクラスは変更を受けません。
インターフェースと実装に分ける理由
まとめるとおおむね次の三つが考えられます
- プロトコル準拠
- 多重継承問題回避
- 疎結合
プロトコル準拠とはコードの記述に制約を与えるということです。プロトコル準拠をすれば実装漏れが防がれます。多重継承問題回避も現実的な理由です。継承はたしかに実装の省略という側面もありますが、それに限定してしまうと多重継承が許されません。疎結合も現実的な理由でかつ半ば高度な理由です。開発者は実装部分のみを書けばそれをどのように呼び出すかまで考えなくて済みます。
あとがき
多くのひとがオブジェクト指向たとえばJavaやC#を書くときにインターフェースの理解に苦しむと思われます。インターフェース という語から想像されるものは多い。そして多重継承と言われると一見それと無関係かのように思える。
疎結合の例がもっともインターフェースという語の意味が表れていると思います。この概念の延長線上に「依存性の注入(Dependency Injection)」もあります。つまり、プログラム全体をコンポーネントの集まりとして考えたときに、なるべく独立的で解体可能なものとしたいときにインターフェースと実装を分けて考えることの効果が生まれます。