Javaやオブジェクト指向で重要な概念の一つ、インタフェース。
初学者向けの参考書の多くは、「インタフェースとはどういう機能か」みたいなことは書いてあるが、「なぜインタフェースが必要なのか?」とか「インタフェースがどのように使われるのか?」ということがてんで書かれていない。
ちょっと深掘りして考えることができる方は、「なぜ継承じゃダメなの?」とか「実開発ではどう使うの?」という疑問を持つかもしれない。
使いまわされた例題ごとく、DogやBirdにいくら鳴かせても、これらの疑問は解決しないだろう。
初学者が分かった気になっただけで満足させてしまう様々な解説サイトを見て、実開発目線で解説することが重要なのに…といつも思う。
そこで今回は、Javaにおけるインタフェースの意義について、できる限り実用目線で深掘りして解説していこうと思う。
結論
まずインタフェースがあなたに便利な機能を提供してくれるもの、と思っているならその認識は捨てたほうがいい。
Javaにおける(というかオブジェクト指向における)インタフェースは、「これを使って実装することで将来の変更に強いプログラムを作りなさい」と素晴らしいプログラムの作成を強いるためのもの、だと思ってもらった方が良い。
インタフェースと切り離せないのが、ポリモーフィズムであり、ポリモーフィズムを実現するためにインタフェースがあるといっても過言ではない。
そしてポリモーフィズムを意識して実装する、ということは疎結合なプログラムを作る、ということに繋がる。
疎結合なプログラムを作る、ということは将来の変更に強いプログラムを作る、ということである。
つまりインタフェースというのは、将来の変更に強いプログラムを作るために使用しなければならないものである。
※ただし、クラスの継承は将来の変更に強くない。(後述する)
疎結合なプログラムとは?
密結合なプログラムの例
class 音楽再生機能 {
...
public int 音楽を再生する (イヤホンクラス イヤホン) {
...
イヤホン.音を出力する(音楽データ等);
...
}
...
}
public class Main {
public static void main () {
...
音楽再生機能オブジェクト.音楽を再生する(イヤホンオブジェクト);
...
}
}
ほんと~~~に適当な例だが、上のような音楽再生アプリの一機能を考えてみよう。
音楽再生機能はアプリ上で音楽を再生するために必要な処理をする機能で、その中でも「音楽を再生する」メソッドでは、出力するデバイスのコントローラーを取得し、音楽データなどと共に音を出力する処理を委譲することとする。(実際このようなアプリは作ったことないので、あくまでイメージである)
上記はなぜ密結合といえるだろうか?
- 「音楽を再生する」メソッド実装の際、イヤホンクラスの扱い方を知っておかなければならない。
このメソッドが使いたいのは「音を出力する」という操作のみであるが、イヤホンオブジェクトそのものを使用するうえでは、イヤホンの使い方に責任を持つ必要がある。
⇒クラスを使用するメソッドの実装における問題 - 後々、イヤホンのみでなくスピーカーにも音楽を出力したい、となった場合に音楽再生機能クラスに大量の変更が必要となる。
スピーカークラスをイヤホンクラスと共存させるためにはどうすればよいか、また上位メソッドからどういう風に選択させるか?等、新しく実装を検討しなす必要が出てくる。
⇒メソッド内で使用するクラスを変えたいときにおける問題 - 後々、イヤホンクラスに拡張や変更をする場合、イヤホンクラスはこのメソッドに影響がないかどうかを考えなければならない。
そして、できる限り影響がないような改造を検討しなければならない。
これはこのメソッドのみでなく、このように直接イヤホンクラスを使用しているメソッド全てについて考えなければならない。
⇒クラスそのものに手を加えたいときにおける問題
そして、上記のような問題を乗り越えながら実装を重ねていった結果、見事むりくり整合性を合わせたようなスパゲッティコードが出来上がる。
インタフェースを使用して実装した例
class 音楽再生機能 {
...
public int 音楽を再生する (音出力可能インタフェース 音出力可能な何か) {
...
音出力可能な何か.音を出力する(音楽データ等);
...
}
...
}
interface 音出力可能インタフェース {
public void 音を出力する(...);
}
public class Main {
public static void main () {
...
音楽再生機能オブジェクト.音楽を再生する(イヤホンオブジェクト);
...
}
}
ポリモーフィズムによって、「音楽を再生する」メソッドでは実クラスを使用せず、インタフェースのみを使用して実装できている。
さて、前章で見たデメリットと対応させてみよう。
- クラスを使用するメソッドの実装においては?
呼び出し元の「音楽を再生する」メソッドでは、「音出力可能インタフェース」の「音を出力する」メソッドの使い方(入出力)のみ分かっていればよい。
実態がイヤホンオブジェクトだろうがスピーカーオブジェクトだろうが、インタフェースに束縛しているので、それらのクラスのことは気にしなくてよい。 - メソッド内で使用するクラスを変えたいときは?
音楽出力先がイヤホンだろうがスピーカーだろうが、出力先の可能性のある全てのクラスが「音出力可能インタフェース」のルールに従っていれば、「音楽を再生する」メソッドは何も変更しなくてよい。 - クラスそのものに手を加えたいときは?
「音出力可能インタフェース」のルールを侵すような変更でなければ、「音楽を再生する」メソッドの中身など気にする必要がない。なぜなら「音楽を再生する」メソッドは、「音出力可能インタフェース」に関心があるのであって、本クラスに関心はないからだ。
このクラスが実際に使われている場所(主にMainクラスやコントロールする系の、上位に位置するような関数)のみ、変更しても大丈夫かを気にすればよい。
以上から疎結合であり、将来の変更に強いプログラムが作れることが分かると思う。
インタフェースをどう使うか?
上の例のように、インタフェースはポリモーフィズムを実現するための手段となっていることが分かったと思う。
ポリモーフィズムを意識して開発することは、密結合なプログラムを回避することに繋がる。
最初の例のように、各モジュールからクラスそのものを多用してしまうと、そのモジュールとクラス間で密結合となってしまう。
2個目の例のように、「インタフェース」 × 「ポリモーフィズム」を意識して設計することで、素晴らしいプログラムが出来上がるだろう。
ただしMainクラスのようないわゆる調理台となるようなクラスや、そもそもオブジェクトの作成を担っているようなクラスは別である。
これらのクラスについては、どんどん実クラスを使ってよい。
ところでポリモーフィズムは「親クラス」へのキャストでも実現できるが、インタフェースのほうが推奨される。
クラスの継承でポリモーフィズムを実現するのは、一体何が問題なのか?ということについて触れてみる。
クラスの継承ではダメなの?
クラスは変わりうるものである
まず「クラス」というのは、変更やら拡張やらで変わりうるものである。
そしてそれは、何かしらのクラスに継承されている親クラスも同様である。
一方インタフェースは、対象とそれを使用する側の間の最低限の決まり・仕様であって、早々変わらないものである。
イメージとしては、クラスが枝の先の節であり、どんどん成長したり伸びたりするものだとすれば、インタフェースは枝の元の支点である。
インタフェースをもとにしてどんどん作られていくのに、それがコロコロ変わってしまってはいけない。
そもそもインタフェースは多重継承可能なので、変更が必要になったら新しく作るのがいい。
ただ、親クラスは別である。
クラスはいつ誰に継承されるか、親になるかなど分からないし、そこに制限をかけてしまっては仕様変更の多いシステム開発では鎖にしかならない。
親クラスだろうが、誰にも継承されていないクラスだろうが、クラスは変更があること前提に作られるべきである。
で、なぜダメなの?
class 音楽再生機能 {
...
public int 音楽を再生する (イヤホンクラス イヤホン) {
...
イヤホン.音を出力する(音楽データ等);
...
}
...
}
class イヤホン {
...
public void 音を出力する (...) {
...
}
...
}
class Bluetoothイヤホン extends イヤホン {
...
// オーバーライド!
public void 音を出力する (...) {
...
}
...
}
public class Main {
public static void main () {
...
音楽再生機能オブジェクト.音楽を再生する(Bluetoothイヤホンオブジェクト);
...
}
}
イヤホンにはBluetoothイヤホンとコード付きイヤホンがあって、それぞれに共通するイヤホンクラスを親として継承すれば、結合度を少し小さくできるよね、っていうので例を挙げてみた。
一見インタフェースを使って実装したものと似た構成ではあるが、親クラスと子クラスの継承でポリモーフィズムを実現しようとすると、これもまた将来の変更に弱いプログラムになってしまう。
なぜなら先ほど述べた、親クラスも変わりうるものであるからだ。
親クラスである「イヤホンクラス」を変更しようとするとき、「音楽を再生する」メソッドでどういう影響があるかを考えて実装する必要がある。
これがあっちでもこっちでも使われていたら、至る所に目線を配らせて変更しなければならない。
そしてあれこれむりくり整合性を合わせていくと、結局スパゲッティコードが出来上がってしまう。
つまり「クラス継承」×「ポリモーフィズム」は、汎用性の観点では多少プラスであるものの、将来の変更に強いプログラムにはならない場合が多いのである。
このような理由でインタフェースを使った実装が推奨されるのであり、クラス継承でないと実装できない等という場合は、そもそも設計が良くないパターンが多いと思われる。
まとめ
インタフェースは、それを使用して実装することで、将来の変更に強いプログラムの作成を強いるものである。
クラス継承は、機能としてはインタフェースを内包するものではあるが、将来の変更に強いプログラムとはならない場合が多い。
インタフェースどんどん使っていこう!
コメント