インタフェースはどこに作るべきか
Kent Beck
実装パターンにおいて
インタフェースのすべてのレイヤにはコストがかかる。学ぶ、理解する、文書化する、デバッグする、整理する、閲覧する、名付けることが増えるからだ。インタフェースが生み出す柔軟性が必要なところだけに、そのコストを払うようにしよう。
ただ、必要な箇所は事前にわからないことも多いので、後から追加できるようにしておくことも大事だ、としている。
Martin Fowler インタフェース言及集
ミニマルインタフェース
必要最小限のメソッドのみを定義するようにしよう。誰かが必要とするかもしれない、でメソッドを追加していくとキリがない。
思いやりのあるインタフェース
JavaのListは25のインスタンスメソッドを持つのに対し、RubyのArrayは78のメソッドを持つ。
配列の最後の要素を取得するのは、Javaだと、
code:java
aList.get(aList.size-1);
と書くが、Rubyでは
code:ruby
anArray.last
と書ける。最初の要素を取得するのもanArray.firstと書ける。
だからといって、何でもかんでもメソッドを作ってしまうと、巨大なクラスができてしまう。思いやりのあるインタフェースにはどういう基準でメソッドを加えたらよいのだろうか?
そのクラスの最も一般的な用途を特定し、それが簡単に使えるようにインタフェースを設計する。そしてこれには短い名前をつける。
これはA Philosophy of Software Designの深いモジュール・浅いモジュールに繋がる議論でもある。
リクワイアードインタフェース
機能を提供する側がインタフェースを決めるのではなく、使う側(クライアント側)の要請でインタフェースを実装する。
例: Comparable
ロールインタフェース
提供側と使う側のインタラクションを見ながら定義する。そうではなくクラスの暗黙的なパブリックインタフェースを、そのまんま明示的にインタフェース化するのはヘッダインタフェースと呼ぶ。
例: PERTのタスク
ヘッダインタフェースの場合、返されるインタフェース
code:java
private int duration;
public MfDate earliestStart() {
MfDate result = MfDate.PAST;
for (?TYPE? p : predecessors())
if (p.earliestFinish().after(result))
result = p.earliestFinish();
return result;
}
public MfDate earliestFinish() {
return earliestStart().addDays(duration);
}
public MfDate latestFinish() {
MfDate result = MfDate.FUTURE;
for (?TYPE? s : successors())
if (s.latestStart().before(result))
result = s.latestStart();
return result;
}
public MfDate latestStart() {
return latestFinish().minusDays(duration);
}
code:InterfaceImplementationPair.java
interface Activity {
MfDate earliestStart();
MfDate earliestFinish();
MfDate latestFinish();
MfDate latestStart();
}
class ActivityImpl {
}
ロールインタフェースの場合は、協調するオブジェクトがどう使われるかを見る。この場合、「後続」インタフェースはlatestStartしか使われないし、PredecessorはearliestFinishしか使われない。なので本当に使うメソッドをもつようにインタフェースを2つに分けて作る。
code:Successor.java
interface Successor {
MfDate latestStart();
}
code:Predecessor.java
interface Predecessor {
MfDate earliestFinish();
}
code:Activity.java
class Activity {
List<Predecessor> predecessors();
List<Successor> successors();
}
パブリッシュドインタフェース
プロジェクトのコードベースの外へ公開するインタフェース。通常のpublicインタフェースであれば、リファクタリングツールを使って変更は比較的容易にできるが、コードベースの外にあると影響範囲が見えないし、変更も難しくなる。
暗黙のインタフェース実装
クラスがあるときは、いつでも暗黙のインタフェースが存在する。class Customerがあるとき、このCustomerの暗黙的なインタフェースを使って、ValuedCustomer implements Customerを定義することは、通常のOO言語ではできない。
これをやるためには、class Customerからinterface Customerを抽出し、実装はclass CustomerImplとして分離して持つ必要がある。
必ずクラスのインタフェースを作るようルール化すればよい気がするが、これはインタフェース実装ペアというパターンで、コード記述量が増えるのであまり好まれない。言語の機能で、暗黙的なインタフェースを作ってくれれば良いのに… 流れるようなインタフェース
2005年に書かれて、当時大流行した。
code:Normal.java
private void makeNormal(Customer customer) {
Order o1 = new Order();
customer.addOrder(o1);
OrderLine line1 = new OrderLine(6, Product.find("TAL"));
o1.addLine(line1);
OrderLine line2 = new OrderLine(5, Product.find("HPK"));
o1.addLine(line2);
OrderLine line3 = new OrderLine(3, Product.find("LGV"));
o1.addLine(line3);
line2.setSkippable(true);
o1.setRush(true);
}
のように書く代わりに、以下のようなインタフェースを提供する。
code:Fluent.java
private void makeFluent(Customer customer) {
customer.newOrder()
.with(6, "TAL")
.with(5, "HPK").skippable()
.with(3, "LGV")
.priorityRush();
}
SQLビルダなどでDSLのように使えると、補完が効くのでコーディング速度の向上が期待できる。
Stackoverflowにおける10年以上続く議論
チームで仕事するときの対人/対チームの取り決めとして必要?
いや、たとえ1人で作るとしても必要だ。
ポリモーフィズムを実現するために必要だ。まぁ、これも手段の1つであってインタフェースを実装から切り離すことによって、別の実装に置き換えやすくするのが目的だ。結果として、テストのときにモックへの置き換えが簡単になったりする。