interfaceについて本気出して考えてみた
本気出す第二弾。
オブジェクト指向を良く知らなかった頃*1、Javaの勉強を始めると、class, field, method, interface などのオブジェクト指向的な概念を覚えていくことになります。
その中で、一番「よくわからんけど、まぁそんなものがあるのね。しっかし、何のためにあるんだか全く分からない存在だな…。」という印象を受けるであろう、というか受けたモノは interface だった。
「プログラミング」=「処理手順を書く」という認識で interface を見ると、全く存在価値が感じられないんだな。だって「処理手順書けない」んだもん。それなら別にわざわざ interface を implements とかしないで、処理手順を記述したclassの方の型で扱えばいいじゃん、と。
そんなこんなが、紆余曲折を経て、なんか interface を使ったコードを書いている。かといって、「interfaceとは!?」と聞かれて、この感覚をきっちり伝えるための的確な説明がサクッとできない。サクッとできないので、色々考えた事をジトッと掃き出してみる。
interfaceの基礎
まずこのコードを見てみよう。interface を使った簡単なコードですね。前回のネタで使ったコードに近い。
public class Main { public static void main(String[] args) { Foo foo = FooFactory.newFoo(); foo.foo(); } } public class FooFactory { public static Foo newFoo() { return new FooImpl(); } } public interface Foo { void foo(); } public class FooImpl implements Foo { public void foo() { // ... } }
ここでの登場人物は4人。Main, FooFactory, Foo, FooImplだ。それぞれ「クライアント」「ファクトリ」「インターフェイス」「実装」と呼ばれるので、ここでも以後そう呼ぶことにする。
これを、現実のモノに例えて考えると分かりやすい、と思っているw オーディオのアンプを思い浮かべて欲しい。
これをまとめると、以下のようになる。
Java | アンプ(機械) | |
---|---|---|
クライアント | Main | アンプを操作する人間 |
ファクトリ | FooFactory | アンプ工場 |
インターフェイス | Foo | アンプ操作パネル |
実装 | FooImpl | アンプ内の電子回路 |
- アンプを工場に作ってもらって、受け取る → ファクトリでインスタンス生成
- 使用者は、アンプ内の電子回路の詳細を知らなくていい → MainはFooImplの中でどんな処理が行われているか知らなくていい
- 使用者は、ボリュームツマミを時計回りに回すと音がデカくなる事をしっている → Mainはfooメソッドを呼ぶとhogeる*2ことを知っている
イメージ沸きます? 俺、これ思いついた時、脳内で花火打ち上がりまくったんですけどw まぁいいや。
「インターフェイスを作る」ということ
「この型には、fooメソッドがあり、実行するとhogeります」という決め事。これが「仕様」です。インターフェイスを決めるということは、仕様を策定*3する事と同義です。
「ボリュームツマミを時計回りに回すと、音がデカくなります」という決め事。これも「仕様」です。操作パネルの使い方を決めるということは、仕様を策定(ry
仕様っていうのは、その機能概念(Fooが果たす役割=hogeする、アンプが果たす役割=音を大きくする)を考えた人が決める事です。ボリュームを大きくする方法を決めるのは、「アンプメーカー(FooImplをプログラミングする人)」じゃなくて、「アンプを考えた人(Fooを考えた人)」の役割です。
アンプメーカーが各々全く独自に「左回しだとボリュームでかくなる」とか「上上下下左右左右BAで音がでかくなる」とか「涙を流しながら天に祈るとでかくなる」等を勝手に決め出すと、ユーザは非常に困ります。
アンプ(実装)毎に操作方法が違う訳ですから、全てのアンプの使い方を覚えなければなりません。
しかし幸いな事に、アンプのボリュームの上げ方は統一*4されていますので、「アンプの使い方」を知っていれば「Pioneerのアンプの使い方」「SONYのアンプの使い方」「DENONのアンプの使い方」などを個別に覚える必要はありません。
これと同じように、hogeり方は統一されているので、「FooImpl」「FooImpl2」「FooImpl3」などがあったとしても、個別に使い方を覚える必要がないのです。
仕様を決める、とはこういうことを意味します。従って、上記コードのFoo定義はまだ不完全です。何が起るかハッキリしません。以前のエントリ「仕様違反実装をしない様に - 都元ダイスケ IT-PRESS」にも書きましたが、Javadocがあって初めて仕様は完成します。
/** * FOOを実現する為のインターフェイス。 */ public interface Foo { /** * hogeる。 */ void foo(); }
「実装を作る」ということ
これは「仕様(=インターフェイス)に従った動作をするプログラムを書く」ということになります。アンプの例で言えば「ボリュームツマミを右に回すと音がでかくなる機械を作る」ということです。
ここで例えば、仕様違反をしてみます。Fooをimplementsしているから「hogeる」ハズなのに、実行すると「fugaる」実装だって書く事はできます。コンパイルも通ります。エラー終了もしません。
しかしこれは、大きな混乱を招きます。一般的な見た目のアンプがあって、右に回したら音が小さくなったとか、右に回したら電源が切れたとか、右に回したら神龍が降りてきたとか、そのぐらい困った事になります。大変ですね。
「インターフェイスを使う」ということ
クライアントは、インターフェイスを使います。クライアントは実装(アンプの中のカラクリ)を知らないので、音がでかくなる事を期待してツマミを右に回すわけです。hogeることを期待してfoo()を呼ぶのです。仕様違反をおかすと、そりゃーバグが生まれるのは必然です。
じゃあ「クライアントも実装を知ってればこんな事起らないじゃん」と思うかもしれませんが、それならわざわざインターフェイスを定義する必要はありません。このようなコードにすればいいのです。
FooImpl fi = new FooImpl();
fi.foo();
アンプの例に例えれば、アンプ内のカラクリを把握していれば、ツマミを右に回した時にボリュームが小さくなってもビビらないよね、という理屈。
でも、こんなアンプを上手く使いこなすには、アンプ内の電子回路を事細かに知っておかなければいけません。内部の電子回路理論を把握しないと使えないアンプなんて、誰が使いますかw
「インターフェイスのメソッドを増やす」ということ
version1として一度リリースされた仕様とその実装があって、既にユーザが居るとします。さらに、ここでは仕様策定者と実装者は別人だと考えます。Fooを作った人と、FooImplを作った人は別人*5、ということです。
ここで仕様策定者は、version2としてbar()メソッドも提供する!と決めたとします。FOOを実現するために、さらに便利な機能の追加提供です。ユーザにとっては嬉しい限り。
/** * FOOを実現する為のインターフェイス。 */ public interface Foo { /** * hogeる。 */ void foo(); /** * piyoる。 */ void bar(); }
そしてこの Foo v2 がめでたくリリースされました。ここで困るのは実装者達です。自分の実装は、v2ではエラーを起こし、使えなくなってしまいます。アンプに新しいボタンが増えたのに、その操作によって何を処理すればいいのか分からないので、エラーになるのです。
The type FooImpl must implement the inherited abstract method Foo.bar()
実装者は、苦労して bar()メソッドも追加で実装し、FooImpl v2 の追加リリースを迫られます。自分が仕様策定者になった場合、インターフェイスメソッドの追加は、この覚悟をもって行わなければならない訳です。
ちなみに、最もやってはいけないのは、fooのJavadocだけを変えたv2のリリース。
/** * FOOを実現する為のインターフェイス。 */ public interface Foo { /** * ponyoる。 */ void foo(); }
エラーが起らないため、実装者は変更を見落とす可能性があります。そのままユーザがFooを扱うと、ponyoると思ってたのにhogeられてしまい、まさかの神龍降臨です。
「インターフェイスのメソッドを減らす」ということ
さて、Foo及びFooImplのv2がリリースされ、こちらのユーザも増えました。そんなある日、仕様策定者が「foo()メソッドいらなくね?」と判断したとします。大いなる決断、Foo v3のリリースです。
今回、実装者は楽です。何も考えなくてもいいです。FooImpl v2には、Foo v3にとって「余計なメソッド」が一つあるだけです。別に余計な実装があるからといって、問題は何も起りません。アンプの操作パネルには無いけど、筐体を開ける(FooImplにキャストする)と操作できる、サービスマン用の隠し機能、のような立場になるだけです。
今度困るのはユーザ側です。今まで Foo v2 を使って foo()メソッドをあちこちで使ったコードを書いてきています。こいつが突然動かなくなるというかコンパイルエラーを起こすのです。直すのは大変。今までツマミを右に回すことによってボリュームを上げることに慣れ親しんで来た人たちが、突然「ツマミなくなりました」って言われるのと同じです。
この決断は、極限まで避けなければならない事態なのがお分かり頂けるかと。被害の範囲が「メソッドを増やす」とは比べものにならない位デカいです。
しかし、現実問題、たまにはこの様な処置も必要になるでしょう。そんな場合はどうするか。@Deprecated です。
/** * FOOを実現する為のインターフェイス。 */ public interface Foo { /** * hogeる。 * @deprecated fooと同じ事を実現するには×××すればいい */ @Deprecated void foo(); /** * piyoる。 */ void bar(); }
v3は、ひとまずこのような形でリリースを行います。この処置があれば「うお、非推奨化しよった。書き直さなきゃ」ということにユーザが気づくことができます。そして、しかるべき移行期間を設けた後、v4, v5 場合によっては v6 以降でやっと、foo()を廃止してもまぁいいか、という状態になります。
「インターフェイスのメソッドシグネチャを変える」ということ
v1の状態から、以下のようにバージョンアップをしたとします。
/** * FOOを実現する為のインターフェイス。 */ public interface Foo { /** * hogeる。 */ void foo(int integer); }
これ、もう最悪ですね。「foo()が減って、foo(int)が増えた」のと同じ事になります。実装者もユーザも大混乱です。是が非でも避けましょう。正解はこちら。
/** * FOOを実現する為のインターフェイス。 */ public interface Foo { /** * hogeる。 * @deprecated foo(0)で、これの代替になる。 */ @Deprecated void foo(); /** * hogeる。 */ void foo(int integer); }
こちらも、然るべき移行期間を設けた後に、foo()を廃止しましょう。
と、まぁ
interfaceの考え方、利点、利用の仕方、作り方というポイントでつらつらと書いてみました。こんな事を考えながら、Jiemamyを設計しています。