interfaceについて本気出して考えてみた

本気出す第二弾。

オブジェクト指向を良く知らなかった頃*1Javaの勉強を始めると、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 オーディオのアンプを思い浮かべて欲しい。

http://img.yamada-denkiweb.com/item/image/item_image/L/L214493018.jpg

これをまとめると、以下のようになる。

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を設計しています。

*1:じゃあ今はオブジェクト指向を良く知っているのかと言われると、やっぱり自信がないのだが…w

*2:後で定義するかもw まだ決めてないw

*3:考えて決める事

*4:現実問題、場合によって[↑/↓]ボタンだったり、スライダーだったりしますが…

*5:アンプの理論を考案した人と、アンプという製品を具体的に設計する人は、別ですよね。