Fluent Interface (流れるようなインターフェース)
2005年12月20日
数ヶ月前、エリック・エヴァンスとのワークショップに参加した際、彼が特定のインターフェーススタイルについて語りました。私たちはそれを「fluent interface (流れるようなインターフェース)」と名付けることにしました。一般的なスタイルではありませんが、もっと広く知られるべきだと考えています。おそらく、それを説明する最良の方法は例を示すことでしょう。
最も単純な例は、おそらくエリックのtimeAndMoneyライブラリからのものです。時間間隔を作成する通常のやり方では、このようなものが考えられます。
TimePoint fiveOClock, sixOClock; ... TimeInterval meetingTime = new TimeInterval(fiveOClock, sixOClock);
timeAndMoneyライブラリのユーザーは、このように操作します。
TimeInterval meetingTime = fiveOClock.until(sixOClock);
顧客の注文を作成する一般的な例を続けて説明します。注文には、数量と製品を含む品目があります。品目はスキップ可能であり、つまり、注文全体を遅らせるよりも、この品目なしで配達したいということです。注文全体に緊急ステータスを設定できます。
私がよく見かけるこの種の構築方法は、このようになっています。
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); }
本質的に、さまざまなオブジェクトを作成し、それらを一緒に配線します。コンストラクタですべてを設定できない場合は、配線を完了するのに役立つ一時変数を作成する必要があります。これは特に、コレクションに項目を追加する場合に当てはまります。
同じアセンブリをfluentスタイルで行うと、このようになります。
private void makeFluent(Customer customer) { customer.newOrder() .with(6, "TAL") .with(5, "HPK").skippable() .with(3, "LGV") .priorityRush(); }
このスタイルで最も重要なことは、内部ドメイン固有言語のようなものを目指しているということです。実際、これが私たちがそれを「fluent (流れるような)」と表現することを選んだ理由であり、多くの点でこの2つの用語は同義です。APIは、主に読みやすく、流れやすいように設計されています。この流暢さの代償は、思考とAPI構築自体の両方における労力の増加です。コンストラクタ、セッター、および追加メソッドの単純なAPIは、はるかに簡単に記述できます。優れたfluent APIを作成するには、かなりの思考が必要です。
実際、この小さな例の問題の1つは、私がカルガリーのコーヒーショップで朝食時にさっと作っただけということです。優れたfluent APIを構築するには時間がかかります。より深く考え抜かれたfluent APIの例が必要な場合は、JMockをご覧ください。JMockは、他のモックライブラリと同様に、複雑な動作の仕様を作成する必要があります。過去数年間で多くのモックライブラリが構築されてきましたが、JMockのライブラリには非常に優れたfluent APIが含まれており、非常にスムーズに動作します。以下に期待の例を示します。
mock.expects(once()).method("m").with( or(stringContains("hello"), stringContains("howdy")) );
スティーブ・フリーマンとナット・プライスがJAOO2005でJMock APIの進化について素晴らしい講演を行いました。彼らはその後、それをOOPSLA論文にまとめました。
これまで、オブジェクトの構成を作成するためにfluent APIを主に見てきました。これには、しばしば値オブジェクトが含まれます。これが決定的な特徴かどうかはわかりませんが、宣言的なコンテキストに現れることに関連した何かがあるのではないかと考えています。私たちの考える流暢さの重要なテストは、ドメイン固有言語の質です。APIの使用が言語のような流れを持つほど、より流暢になります。
このようなfluent APIを構築すると、いくつかの通常とは異なるAPIの習慣につながります。最も明白なものの1つは、値を返すセッターです。(注文の例では、with
は注文に注文明細を追加し、注文を返します。)中括弧の世界では、変更メソッドはvoidであるというのが一般的な慣例です。これはコマンドクエリ分離の原則に従っているため、私はこの慣例が好きです。この慣例はfluentインターフェースの妨げになるため、この場合は慣例を一時停止する傾向があります。
流れるようなアクションを継続するために必要なものに基づいて、戻り値の型を選択する必要があります。JMockは、次に何が必要になる可能性が高いかに応じて型を移動することを重視しています。このスタイルの優れた利点の1つは、メソッド補完(インテリセンス)が次に何をタイプすべきかを教えてくれることです。これは、IDEのウィザードのようです。一般的に、動的言語は構文が煩雑になりにくいため、DSLに適しています。ただし、メソッド補完を使用することは、静的言語にとってプラスになります。
fluentインターフェースのメソッドの問題の1つは、それ自体ではあまり意味がないということです。メソッドごとのドキュメントのメソッドブラウザを見ても、with
にはあまり意味がありません。実際、それ単独では、意図をうまく伝えていない名前の悪いメソッドであると主張するでしょう。fluentアクションのコンテキストでのみ、その強みがわかります。この問題を回避する方法の1つは、このコンテキストでのみ使用されるビルダーオブジェクトを使用することです。
エリックが述べたことの1つは、これまで彼が使用し、見てきたfluentインターフェースは、主に値オブジェクトの構成に関するものであるということです。値オブジェクトには、ドメインに意味のある識別情報がないため、簡単に作成して破棄できます。したがって、流暢さは、古い値から新しい値を作成することに依存しています。その意味で、注文の例は、エヴァンスの分類ではエンティティであるため、それほど一般的ではありません。
私はまだ多くのfluentインターフェースを見ていないので、それらの長所と短所についてあまり知らないと結論付けます。したがって、それらを使用するようにとのいかなる勧告も予備的なものにすぎません。しかし、私はそれらがより多くの実験に適していると思います。
ピアーズ・コーリーによる、これに対する優れたフォローアップがあります。
更新 (2008年6月23日)。私がこの投稿を書いて以来、この用語は非常に広く使用されるようになり、心地よい満足感を得ています。私は、現在執筆中の本で、fluentインターフェースと内部DSLについての考えを洗練させてきました。また、一般的な誤解にも気づきました。多くの人がfluentインターフェースをメソッドチェーンと同一視しているようです。確かに、チェーンはfluentインターフェースで使用する一般的な手法ですが、真の流暢さはそれ以上のものです。
上記のJMockの例では、メソッドチェーンを使用していますが、入れ子になった関数とオブジェクトスコープも使用しています。