はじめに
クラス間で共通する操作をひとまとめに定義させたり、空のインターフェースを定義することで実装クラスに何らかの特性をもたせて処理の意図を明確にさせたりと、インターフェースにはクラスの設計を助けてくれる利点が色々とあります。
中でも次に掲げる点は、インターフェースの最大の利点と感じる人が多いのではないのでしょうか。
- 利用者1がそのクラスの内部構造を知らずして、ある契約2に沿ってそのクラスの機能を利用することを強制させることができる点
- 実装が変更した場合に生じる不必要な修正を防ぎ、変更に強いプログラムを維持できる点
ただ、これらの意味がいまいち整理できていなかったので、自分なりの言葉で整理することとしました3。
インターフェースを使わないと・・・
例えば次のように、Aクラスに3つのメソッドを定義したとします。
class A {
// コールするメソッド
void call() {
System.out.println("たたいて・かぶって・ジャンケンポン");
}
// ジャカるメソッド
void jakajaka() {
System.out.println(
"ジャカジャカジャン、ジャカジャカジャン、ジャカジャカジャカジャカ、ジャンケンポーン!"
);
}
// 踊るメソッド
void odoru() {
System.out.println("事件は会議室で起きてるの。");
System.out.println("違う、現場だ。");
System.out.println("会議室よ。");
System.out.println("現場、現場。");
System.out.println("会議室、会議室、会議室。");
System.out.println("現場、現場、現場、現場、...現場室!!!");
}
}
そして、このクラスを利用するために、次のようなコードを定義したとします。
A a = new A();
a.call();
a.jakajaka();
a.odoru();
これを実行すれば、当然ながらそれぞれのメソッドによって定義されたメッセージが出力されます。
ここで、次のようにAクラスの実装を変更したBクラスを新設し、利用者に対し今後はAクラスではなくBクラスを利用するよう呼びかけたとします。
class B {
// callメソッドを削除
// メソッド名をjakajakaからsingへ変更
void sing() {
... // 同上
}
// メソッド名をodoruからgenbashitsuへ変更
void genbashitsu() {
... // 同上
}
}
利用者はAクラスをやめてBクラスを利用しなければならなくなるため、Bクラスの実装を把握し、今までのコードを次のように修正する必要が生じます。
B b = new B();
b.sing();
b.genbashitsu();
singメソッド、genbashitsuメソッドからは今までと同様のメッセージが出力されるものの、callメソッドが使えず困った利用者が、呼びかけに反し一部Aクラスの機能を利用してしまったとします。
A a = new A(); // 呼びかけ違反
a.call(); // 呼びかけ違反
B b = new B();
b.sing();
b.genbashitsu();
その場しのぎとしては良いかもしれませんが、もしAクラスが廃止予定であったとしたら、そのうちAクラスにアクセスしている箇所はすべてコンパイルエラーとなるかもしれません。そのときは、将来その箇所を修正しなければならなくなるでしょう。
このようにインターフェースを使わない場合では、何らかの実装の変更が生じたときに、利用者が新しい実装の変更箇所を把握し、今までどおりの実装を維持するためにどのようにコードを修正しなければならないかを考えなければならなくなります。また、利用すべきでない古い機能を利用してしまえば、将来その機能が廃止されたときに生じる修正に手間をかけることとなります。
インターフェースを使うと・・・
インターフェースを使えば、利用者に対し利用できるメソッドを知らせることができます。利用者は、インターフェースに定義されているメソッドと利用する実装クラスの機能さえ把握していれば、たとえ実装が変更されたとしても、利用できるメソッドと利用できないメソッドの確認をとる必要はなくなります。
interface Contract {
void call();
void sing();
void genbashitsu();
}
class B implements Contract {
// コールするメソッド
@Override public void call() {}; // 空実装
// ジャカるメソッド
@Override public void sing() {
... // 同上
}
// 踊るメソッド
@Override public void genbashitsu() {
... // 同上
}
}
また、インターフェースを実装するクラスに空実装されたメソッドが用意されていれば、必要に応じて利用者はそのメソッドを無名クラスを利用して実装することもできるようになるでしょう4。
Contract c = new B() {
@Override public void call() {
System.out.println("たたいて・かぶって・ジャンケンポン");
}
};
c.call(); // たたいて・かぶって・ジャンケンポン
c.sing(); // ジャカジャカジャン、ジャカジャカジャン、ジャカジャカジャカジャカ、ジャンケンポーン!
c.genbashitsu(); // 事件は(中略)現場室!!!
まとめ
インターフェースを使えば、利用者が実装の変更に伴う修正の必要性に怯えることなく、柔軟なプログラムを構築することができるようになります。次のコードのように新しい実装クラスが登場し、そのクラスを利用したくなったとしても、生成するインスタンスを変更するだけでその実装を利用することができます。
class NewB implements Contract {
// コールするメソッド
@Override public void call() {}; // 空実装
// ジャカるメソッド
@Override public void sing() {
System.out.println("オウイエア");
}
// 踊るメソッド
@Override public void genbashitsu() {
System.out.println("ララララブサンバディトゥナイト ネバネバネバネバネバネバネバレッザラブゴー");
}
}
Contract c = new NewB() {
@Override public void call() {
System.out.println("たたいて・かぶって・ジャンケンポン");
}
};
c.call(); // たたいて・かぶって・ジャンケンポン
c.sing(); // オウイエア
c.genbashitsu(); // ララララブサンバディトゥナイト ネバネバネバネバネバネバネバレッザラブゴー