インターフェース分離の原則(Interface Segregation Principle)

クライアントに、クライアントが利用しないメソッドへの依存を強制してはならない。

アジャイルソフトウェア開発の奥義 第2版 p.178

Rubyはダックタイピングな言語であり、この原則をイメージできるコード例を示すことはできない。

なので、まずは例え話から始め、次にJavaの例をあげる。

あなたは一人の人間で、いくつかのコミュニティに属しているだろう。天涯孤独でなければ親族というコミュニティ、結婚しているなら家庭というコミュニティ、働いているなら職場というコミュニティといった具合に。あなたはそれぞれのコミュニティでそれぞれの顔を持っているはずだ。家族としての顔は破綻した関係でも、職場での顔は面倒見の良い上司かもしれない。このように別々の顔を持っていても問題にはならない。家庭での顔は職場というコミュニティにとっては関係がなく、必要な顔さえ見せていればよいからだ。

逆に、自分の視点から他の人を考えてみる。あなたがコンビニで買い物をするとき、レジを打ってくれるなら店員が誰であっても構わない。コンビニ店長でも、新人アルバイトとサポート担当の2人組でも、あるいは凄腕のヒットマンだったとしても、買い物ができるならあなたにとっては関係がないはずだ。

クラスの関係においても同じである。JavaのArrayListを例に考える。

すべての実装されたインタフェース:

Serializable, Cloneable, Iterable, Collection, List, RandomAccess

ArrayListクラスは

  • Serializableインターフェースを実装しているので、シリアライズが可能という顔を持っている
  • Iterableインターフェースを実装しているので、イテレーションが可能という顔を持っている
  • RandomAccessインターフェースを実装しているので、ランダムアクセスが可能という顔を持っている

と言える。

以下のようなメソッドを考える。

void print(ArrayList<String> list) {
  list.forEach(string -> System.out.println(s));
}

forEachメソッドは1つ1つ要素を取得して処理するイテレーションメソッドである。つまり、Iterableインターフェースのメソッドである。

printメソッドは処理の上では引数listに対して、シリアライズもランダムアクセスも可能であることを求めていない。唯一、イテレーション可能であるということを求めている。

しかし、引数の型がArrayList<String>クラスのため、ArrayListクラスを継承したクラスしか指定することができない。printメソッドは処理の上ではイテレーション可能であることしか求めていないのだから、他のIterableインターフェースを実装しているクラスでも指定できる方が理にかなっている。それゆえ、以下のようにするべきである。

void print(Iterable<String> list) {
  list.forEach(string -> System.out.println(s));
}

これで、LinkedListクラスなどIterableインターフェースを実装しているクラスを指定できるようになった。printメソッドは実体がどのクラスのインスタンスであれイテレーション可能ならどれでも良い状態になった(コンビニ店員が誰でも良い状態と同じ)。

インターフェース分離の原則は、クラスレベルに限った原則ではない。Twitter APIなどのAPIサービスも、おそらく一般利用者のための外向けAPIと管理者のための内向けAPIの2つのAPIを持ったシステムが存在しているはずである。社内では2つのAPIは区別なく同じエンドポイントにリクエストできるが、外からのリクエストは外向けAPIしか実行されないような仕組みになっているだろうと想像するのは難しくない。

まとめ

クラスを定義する際、そのクラスはどのような顔を持つのか考えなければならない。いくつかの顔を持つなら、それぞれの顔に対応するインターフェースを定義した方が良い。

クラスを使う側は、そのクラスのどの顔に依存しているのか考えなければならない。特定の顔にのみ依存しているなら、その顔に対応するインターフェースにのみ依存しなければならない。