流暢なインターフェース
ソフトウェア工学において、流暢なインターフェースとは、メソッド連鎖を多用した設計のオブジェクト指向 APIです。ドメイン固有言語(DSL)を作成することでコードの可読性を向上させることを目的としています。この用語は、2005年にエリック・エヴァンスとマーティン・ファウラーによって造られました。[1]
実装
流暢なインターフェースは、カスケーディングをネイティブに提供していない言語においてメソッド連鎖を実現するために、メソッド連鎖を通じて実装されるのが一般的です。これは通常、各メソッドが呼び出されたオブジェクトを返すようにすることで実現されます[要出典] 。これはしばしばまたはと呼ばれます。より抽象的に言えば、流暢なインターフェースは、チェーン内の後続の呼び出しの命令コンテキストを保持します。コンテキストは通常、以下のようになります。thisself
流れるようなインターフェースには、メソッドの連鎖だけでなく、連鎖呼び出しがドメイン固有言語(DSL)のように読めるようにAPIを設計することも含まれます。これには、ネストされた関数や慎重なオブジェクトのスコープ設定などのテクニックが組み込まれることがよくあります。[1]
歴史
「流れるようなインターフェース」という用語は2005年後半に造語されましたが、このインターフェースのスタイル全体は、1970年代のSmalltalkにおけるメソッドカスケーディングの発明、そして1980年代の数多くの例にまで遡ります。一般的な例としては、 C++のiostreamライブラリが挙げられます。これはメッセージパッシングにor演算子を使用し、複数のデータを同じオブジェクトに送信し、他のメソッド呼び出しのための「マニピュレータ」を可能にします。その他の初期の例としては、Garnetシステム(1988年のLisp)とAmuletシステム(1994年のC++)があり、これらはオブジェクトの作成とプロパティの割り当てにこのスタイルを使用していました。<<>>
例
C#
C#では、 LINQの流暢なプログラミング言語を広範囲に活用し、「標準クエリ演算子」を用いたクエリを構築します。実装は拡張メソッドに基づいています。
// 英語からフランス語への動物名の辞書Dictionary < string , string > translations = new () { { "cat" , "chat" }, { "dog" , "chien" }, { "fish" , "poisson" }, { "bird" , "oiseau" } }; // 文字 "a" を含む英語の単語の翻訳を検索します。// 長さでソートされ、大文字で表示されます。 IEnumerable < string > query = translations . Where ( t => t . Key . Contains ( "a" )) . OrderBy ( t => t . Value . Length ) . Select ( t => t . Value . ToUpper ()); // 同じクエリを段階的に構築します: IEnumerable < string > filtered = translations . Where ( t => t . Key . Contains ( "a" )); IEnumerable < string > sorted = filtered . OrderBy ( t => t . Value . Length ); IEnumerable < string > finalQuery = sorted . Select ( t => t . Value . ToUpper ()); Fluentインターフェースは、同じオブジェクトを操作/共有する一連のメソッドを連結するためにも使用できます。カスタムクラスを作成する代わりに、次のようにFluentインターフェースで装飾できるデータコンテキストを作成できます。
// データ コンテキストクラスを定義しますContext { public string FirstName { get ; set ; } public string LastName { get ; set ; } public string Sex { get ; set ; } public string Address { get ; set ; } } class Customer { private Context _context = new Context (); // コンテキストを初期化する // プロパティの値を設定しますpublic Customer FirstName ( string firstName ) { _context . FirstName = firstName ; return this ; } public Customer LastName ( string lastName ) { _context . LastName = lastName ; return this ; } public Customer Sex ( string sex ) { _context . Sex = sex ; return this ; } public Customer Address ( string address ) { _context . Address = address ; return this ; } // データをコンソールに出力しますpublic void Print () { Console . WriteLine ( $"First name: {_context.FirstName} \nLast name: {_context.LastName} \nSex: {_context.Sex} \nAddress: {_context.Address}" ); } } class Program { static void Main ( string [] args ) { // オブジェクトの作成Customer c1 = new Customer (); // メソッドチェーンを使用して、1行でデータの割り当てと印刷を行うc1 . FirstName ( "vinod" ). LastName ( "srivastav" ). Sex ( "male" ). Address ( "bangalore" ). Print (); } } .NETテスト フレームワークNUnit は、 C# のメソッドとプロパティを流暢なスタイルで組み合わせて使用し、「制約ベース」のアサーションを構築します。
アサート.That ( ( ) => 2 * 2 , Is .AtLeast ( 3 ) . And .AtMost ( 5 )); C++
C++における Fluent インターフェイスの一般的な用途は、オーバーロードされた演算子を連結する標準iostreamです。
以下は、C++ のより従来的なインターフェースの上に、流れるようなインターフェース ラッパーを提供する例です。
stdをインポートします。 std :: stringを使用します。std :: vectorを使用します。 // 基本定義class GlutApp { private : int width ; int height ; int x ; int y ; int displayMode ; vector < string > args ; string title ; public : GlutApp ( const vector < string >& args ) : args { args } {} void setDisplayMode ( int mode ) noexcept { displayMode = mode ; } [[ nodiscard ]] int getDisplayMode () const noexcept { return displayMode ; } void setWindowSize ( int w , int h ) noexcept {幅= w ;高さ= h ; } void setWindowPosition ( int x , int y ) noexcept { this -> x = x ; this -> y = y ; } void setTitle ( const string & title ) noexcept { this -> title = title ; } voidを作成します() { // ... } }; // 基本的な使用法int main ( int argc , char * argv []) { vector < string > args ( argv , argv + argc ); GlutApp app ( args ); app . setDisplayMode ( GLUT_DOUBLE | GLUT_RGBA | GLUT_ALPHA | GLUT_DEPTH ); // フレームバッファのパラメータを設定app . setWindowSize ( 500 , 500 ); // ウィンドウのパラメータを設定app . setWindowPosition ( 200 , 200 ); app . setTitle ( "My OpenGL/GLUT App" ); app . create (); } // Fluent ラッパークラスFluentGlutApp : private GlutApp { public : FluentGlutApp ( const vector < string >& args ) : GlutApp ( args ) {} // 親コンストラクタを継承する FluentGlutApp & withDoubleBuffer () { setDisplayMode ( getDisplayMode () | GLUT_DOUBLE ); return * this ; } FluentGlutApp & withRGBA () { setDisplayMode ( getDisplayMode () | GLUT_RGBA ); return * this ; } FluentGlutApp & withAlpha () { setDisplayMode ( getDisplayMode () | GLUT_ALPHA ); return * this ; } FluentGlutApp & withDepth () { setDisplayMode ( getDisplayMode () | GLUT_DEPTH ); return * this ; } FluentGlutApp & across ( int w 、int h ) { setWindowSize ( w 、h ); return * this ; } FluentGlutApp & at ( int x 、int y ) { setWindowPosition ( x 、y ); return * this ; } FluentGlutApp & named ( const string & title ) { setTitle ( title ); return * this ; } // create() の後に連鎖するのは意味がないので、*this を返さないでくださいvoid create () { GlutApp :: create (); } }; // Fluent な使用法int main ( int argc , char * argv []) { vector < string > args ( argv , argv + argc ); FluentGlutApp ( args ) . withDoubleBuffer ( ) . withRGBA ( ) . withAlpha ( ) . withDepth ( ) . at ( 200 , 200 ) . across ( 500 , 500 ) . named ( "My OpenGL/GLUT App" ) . create ( ) ; } ジャワ
jMockテストフレームワークにおける流暢なテストの期待値の例は次のとおりです。[1]
モック。( once ()). method ( "m" ). with ( or ( stringContains ( "hello" ), stringContains ( "howdy" )) );を期待します。 jOOQライブラリは、 SQL を Java の流暢な API としてモデル化します。
著者author = AUTHOR . as ( "author" ); create . selectFrom ( author ) . where ( exists ( selectOne () . from ( BOOK ) . where ( BOOK . STATUS . eq ( BOOK_STATUS . SOLD_OUT )) . and ( BOOK . AUTHOR_ID . eq ( author . ID )) ) ); fluflu アノテーション プロセッサを使用すると、Java アノテーションを使用して Fluent API を作成できます。
JaQue ライブラリを使用すると、実行時に Java 8 Lambda を式ツリーの形式でオブジェクトとして表現できるため、次のような型安全な流れるようなインターフェースを作成できるようになります。
顧客customer = new Customer ();顧客.プロパティ( "name" ) .eq ( "John" ) 次のように書くこともできます。
メソッド< Customer > ( customer -> customer . getName () == "John" ) また、モック オブジェクトテスト ライブラリ EasyMock は、このスタイルのインターフェイスを広範に使用して、表現力豊かなプログラミング インターフェイスを提供します。
Collection mockCollection = EasyMock.createMock ( Collection.class ) ; EasyMock.expect ( mockCollection.remove ( null ) ) . andThrow ( new NullPointerException ( ) ) . atLeastOnce ( ) ; Java Swing APIでは、LayoutManagerインターフェースは、Containerオブジェクトにおけるコンポーネントの配置を制御する方法を定義します。より強力なLayoutManager実装の一つにGridBagLayoutクラスがあり、このGridBagConstraintsクラスを使用してレイアウト制御の方法を指定します。このクラスの典型的な使用例は以下のようになります。
javax.swing.JLabelをインポートします。javax.swing.JPanelをインポートします。javax.swing.JTextFieldをインポートします。 GridBagLayout gl = new GridBagLayout (); JPanel p = new JPanel (); p . setLayout ( gl ); JLabel l =新しいJLabel ( "名前:" ); JTextField nm =新しいJTextField ( 10 ); GridBagConstraints gc = new GridBagConstraints ( ) ; gc.gridx = 0 ; gc.gridy = 0 ; gc.fill = GridBagConstraints.NONE ; p.add ( l , gc ) ; gc.gridx = 1 ; gc.fill = GridBagConstraints.HORIZONTAL ; gc.weightx = 1 ; p.add ( nm , gc ) ; これによりコードが大量に生成され、ここで何が起こっているのか正確に把握することが難しくなります。Packerクラスは流暢なメカニズムを提供しているので、代わりに次のように記述します。[2]
javax.swing.JLabelをインポートします。javax.swing.JPanelをインポートします。javax.swing.JTextFieldをインポートします。 JPanel p =新しいJPanel (); Packer pk =新しいPacker ( p ); JLabel l =新しいJLabel ( "名前:" ); JTextField nm =新しいJTextField ( 10 ); pk.pack ( l ) .gridx ( 0 ) .gridy ( 0 ) ; pk.pack ( nm ) .gridx ( 1 ) .gridy ( 0 ) .fillx ( ) ;流暢な API によってソフトウェアの記述方法が簡素化され、メソッドの戻り値が常にそのコンテキスト内でのさらなるアクションのコンテキストを提供するため、ユーザーが API をより生産的かつ快適に使用できる API 言語の作成を支援できる箇所は数多くあります。
JavaScript
これに類似したJavaScriptライブラリの例は数多くありますが、最もよく知られているのはおそらくjQueryでしょう。一般的に、データベースクエリの実装にはFluent Builderが用いられます。例えば、Dynamiteクライアントライブラリでは次のような例が挙げられます。
// テーブルからアイテムを取得するclient.getItem('user-table'). setHashKey ( ' userId ' , ' userA ' ) . setRangeKey ( ' column' , '@' ) . execute () . then ( function ( data ) { //data.result : 結果のオブジェクト}) JavaScript でこれを行う簡単な方法は、プロトタイプ継承とを使用することですthis。
// https://schier.co/blog/2013/11/14/method-chaining-in-javascript.html からの例クラスKitten {コンストラクター() { this . name = 'Garfield' ; this . color = 'orange' ; } setName ( name ) { this . name = name ; this を返します; } setColor ( color ) { this . color = color ; thisを返します; } save () { console . log ( `$ { this . name } 、${ this . color }子猫を保存しています` ); return this ; } } // これを使用しますnew Kitten () . setName ( 'Salem' ) . setColor ( 'black' ) . save (); スカラ
Scalaは、特性とキーワードを使用して、メソッド呼び出しとクラスミックスインの両方に流暢な構文をサポートしていますwith。例えば:
クラスColor { def rgb (): Tuple3 [ Decimal ] }オブジェクトBlack はColorを拡張します{ override def rgb (): Tuple3 [ Decimal ] = ( "0" , "0" , "0" ); } trait GUIWindow { // 滑らかな描画のためにこれを返すレンダリングメソッドdef set_pen_color ( color : Color ): this . type def move_to ( pos : Position ): this . type def line_to ( pos : Position , end_pos : Position ): this . type def render (): this . type = this // 何も描画せず、子実装がスムーズに使用できるように this を返す def top_left ():位置def bottom_left ():位置def top_right ():位置def bottom_right ():位置} 特性WindowBorder はGUIWindowを拡張します{ def render (): GUIWindow = { super . render () . move_to ( top_left ()) . set_pen_color ( Black ) . line_to ( top_right ()) . line_to ( bottom_right ( )) . line_to ( bottom_left ()) . line_to ( top_left ()) } } SwingWindowクラスはGUIWindowを拡張します{ ... } val appWin = new SwingWindow ()とWindowBorder appWin . render () 楽
Rakuには様々なアプローチがありますが、最もシンプルな方法の一つは、属性を read/write として宣言し、givenキーワードを使用することです。型アノテーションはオプションですが、ネイティブの段階的型付けにより、パブリック属性に直接書き込む方がはるかに安全です。
class Employee { subset Salary of Real where * > 0 ; subset NonEmptyString of Str where * ~~ /\S/ ; # 少なくとも1つのスペース以外の文字 空でない文字列 $ .name は rwです。 空でない文字列$ .surname はrw です。給与 $ .salaryはrwです。 メソッド gist { return qq:to[END]; 名前: $.name 姓: $.surname 給与: $.salary END }}私の $employee =従業員. new ();与えられた $employee { . name = 'サリー' ; . surname = 'ライド' ; .給料 = 200 ;} $employeeと言えば、# 出力: # 名前: Sally # 姓: Ride # 給与: 200PHP
PHPでは、$thisインスタンスを表す特別な変数を使用することで、現在のオブジェクトを返すことができます。これによりreturn $this;、メソッドはインスタンスを返すようになります。以下の例では、クラスEmployeeと、その名前、姓、給与を設定する3つのメソッドを定義しています。各メソッドはクラスのインスタンスを返すため、Employeeメソッドを連鎖させることができます。
<?php 宣言( strict_types = 1 );最終 クラス Employee { プライベート 文字列 $name ; プライベート 文字列 $surname ; プライベート 文字列 $salary ; パブリック 関数 setName (文字列 $name ) : self { $this -> name = $name ; $thisを返します。 } パブリック 関数 setSurname (文字列 $surname ) : self { $this -> surname = $surname ; $thisを返します。 } パブリック 関数 setSalary (文字列 $salary ) : self { $this -> salary = $salary ; $thisを返します。 } public function __toString () : string { return <<< INFO 名前: {$this->name} 姓: {$this->surname} 給与: {$this->salary} INFO ; } } # 給与 100 で Employee クラスの新しいインスタンス Tom Smith を作成します: $employee = ( new Employee ()) -> setName ( 'Tom' ) -> setSurname ( 'Smith' ) -> setSalary ( '100' );# Employee インスタンスの値を表示します: echo $employee ;# 表示: # 名前: Tom # 姓: Smith # 給与: 100パイソン
Pythonでは、selfインスタンス メソッドで返すことが、Fluent パターンを実装する 1 つの方法です。
しかし、言語の作者であるGuido van Rossum [3]は、このパターンを非推奨としており、新しい値を返さない操作ではPythonらしくない(慣用的ではない)とされています。Van Rossumは、Fluentパターンが適切だと考える文字列処理操作を例として挙げています。
クラスPoem : def __init__ ( self , title : str ) -> None : self . title = title def indent ( self , spaces : int ): """指定された数のスペースで詩をインデントします。""" self . title = " " * spaces + self . title return self def suffix ( self , author : str ): """詩の末尾に著者名を追加します。""" self . title = f " { self . title } - { author } " return self >>>詩( "Road Not Travelled" ) . indent ( 4 ) . suffix ( "Robert Frost" ) . title 'Road Not Travelled - Robert Frost'迅速
Swift 3.0以降ではself、関数内で返すことが Fluent パターンを実装する 1 つの方法です。
クラスPerson { var firstname : String = "" var lastname : String = "" var favoriteQuote : String = "" @discardableResult func set ( firstname : String ) - > Self { self . firstname = firstname return self } @discardableResult func set ( lastname : String ) - > Self { self . lastname = lastname return self } @discardableResult func set ( favoriteQuote : String ) - > Self { self . favoriteQuote = favoriteQuote return self } } let person = Person () . set ( firstname : "John" ) . set ( lastname : "Doe" ) . set ( favoriteQuote : "私はカメが好きです" ) 不変性
コピーオンライトセマンティクスを活用した、不変で流れるようなインターフェースを作成することは可能です。このパターンのバリエーションでは、内部プロパティを変更して同じオブジェクトへの参照を返すのではなく、オブジェクトを複製し、複製されたオブジェクトのプロパティを変更して、そのオブジェクトを返します。
このアプローチの利点は、インターフェイスを使用して、特定のポイントから分岐できるオブジェクトの構成を作成できることです。これにより、2 つ以上のオブジェクトが一定量の状態を共有し、互いに干渉することなくさらに使用できるようになります。
JavaScriptの例
コピーオンライトセマンティクスを使用すると、上記の JavaScript の例は次のようになります。
クラスKitten {コンストラクター() { this . name = 'Garfield' ; this . color = 'orange' ; } setName ( name ) { const copy = new Kitten (); copy . color = this . color ; copy . name = name ; return copy ; } setColor ( color ) { const copy = new Kitten (); copy . name = this . name ; copy . color = color ; return copy ; } // ... }// それを使用しますconst kitten1 = new Kitten () . setName ( 'Salem' ); const kitten2 = kitten1 . setColor ( 'black' ); console . log ( kitten1 , kitten2 ); // -> Kitten({ name: 'Salem', color: 'orange' }), Kitten({ name: 'Salem', color: 'black' }) 問題
コンパイル時にエラーを捕捉できない
型付き言語では、すべてのパラメータを必要とするコンストラクタを使用するとコンパイル時にエラーが発生しますが、流暢なアプローチでは実行時エラーしか発生せず、最新のコンパイラの型安全性チェックがすべて欠落します。また、これはエラー保護のための 「フェイルファスト」アプローチにも矛盾します。
デバッグとエラー報告
単一行の連鎖文は、デバッガーが連鎖内にブレークポイントを設定できない可能性があるため、デバッグが困難になる可能性があります。また、デバッガーで単一行の文をステップ実行することも不便です。
java.nio.ByteBuffer.allocate ( 10 ) .rewind ( ) . limit ( 100 ) ;もう1つの問題は、どのメソッド呼び出しが例外を引き起こしたのかが明確でない可能性があることです。特に、同じメソッドが複数回呼び出されている場合に顕著です。これらの問題は、ステートメントを複数行に分割することで解決できます。これにより、可読性を維持しながら、ユーザーがチェーン内にブレークポイントを設定し、コードを1行ずつ簡単にステップ実行できるようになります。
java . nio . ByteBuffer . allocate ( 10 ) . rewind () . limit ( 100 ); ただし、一部のデバッガーでは、例外がどの行にスローされていても、例外バックトレースの最初の行が常に表示されます。
ログ記録
一連の fluent 呼び出しの途中にログ記録を追加すると、問題が発生する可能性があります。例えば、次のような場合です。
ByteBufferバッファ= ByteBuffer.allocate ( 10 ) .rewind ( ). limit ( 100 ) ; bufferメソッド呼び出し後の状態をログに記録するにはrewind()、Fluent 呼び出しを中断する必要があります。
ByteBuffer buffer = ByteBuffer.allocate ( 10 ) .rewind ( ); log.debug ( "巻き戻し後の最初のバイトは" + buffer.get ( 0 ) ) ; buffer.limit ( 100 ) ; 拡張メソッドをサポートする言語では、必要なログ記録機能をラップする新しい拡張機能を定義することでこれを回避できます。たとえば、C# では次のようになります (上記と同じ Java ByteBuffer の例を使用)。
静的クラスByteBufferExtensions { public static ByteBuffer Log ( this ByteBuffer buffer 、Log log 、Action < ByteBuffer > getMessage ) { string message = getMessage ( buffer ); log . debug ( message ); return buffer ; } } //使用法: ByteBuffer.Allocate ( 10 ) .Rewind ( ) . Log ( log , b => "巻き戻し後の最初のバイトは" + b.Get ( 0 ) ) . Limit ( 100 ) ; サブクラス
強く型付けされた言語(C++、Java、C#など)のサブクラスでは、戻り値の型を変更するために、スーパークラスの流暢なインターフェースに参加するすべてのメソッドをオーバーライドしなければならないことがよくあります。例えば、
クラス A {パブリックA doThis () { // ... } } class B extends A { public B doThis () { super . doThis (); return this ; } // 戻り値の型を B に変更する必要があります。 public B doThat () { // ... } } ... A a = new B (). doThat (). doThis (); // これは、A.doThis() をオーバーライドしなくても動作します。B b = new B (). doThis (). doThat (); // これは、A.doThis() がオーバーライドされていない場合は失敗します。 F-bound多態性を表現できる言語では、それを用いてこの問題を回避できます。例えば、
抽象クラスAbstractA < TはAbstractA < T >>を拡張します{ @SuppressWarnings ( "unchecked" ) public T doThis () { // ... return ( T ) this ; } }クラスAはAbstractA < A >を拡張します{}クラスBはAbstractA < B >を拡張します{ public B doThat () { // ... return this ; } } ... B b = new B (). doThis (). doThat (); // 動作します! A a = new A (). doThis (); // これも動作します。 親クラスのインスタンスを作成できるようにするには、親クラスを と の2つのクラスに分割する必要がありました。後者AbstractAにはA中身がありません(必要な場合のみコンストラクタが含まれます)。このアプローチは、サブクラス(など)も作成したい場合に簡単に拡張できます。
抽象クラスAbstractB < TはAbstractB < T >>を拡張し、 AbstractA < T >を拡張します{ @SuppressWarnings ( "unchecked" ) public T doThat () { // ... return ( T ) this ; } }クラスBはAbstractB < B >を拡張します{} abstract class AbstractC < T extends AbstractC < T >> extends AbstractB < T > { @SuppressWarnings ( "unchecked" ) public T foo () { // ... return ( T ) this ; } } class C extends AbstractC < C > {} ... C c = new C (). doThis (). doThat (). foo (); // 動作します! B b = new B (). doThis (). doThat (); // まだ動作します。 Scala などの依存型言語では、メソッドは常に戻るものとして明示的に定義することもできるためthis、サブクラスが流れるようなインターフェースを利用するためにメソッドを 1 回だけ定義することができます。
class A { def doThis (): this . type = { ... } // this を返します。常に this を返します。} class B extends A { // オーバーライドは必要ありません。def doThat (): this . type = { ... } } ... val a : A = new B (). doThat (). doThis (); // チェーンは両方向に機能します。val b : B = new B (). doThis (). doThat (); // そして、両方のメソッド チェーンの結果は B になります。 参照
参考文献
- ^ abc Martin Fowler、「FluentInterface」、2005年12月20日
- ^ "Interface Pack200.Packer". Oracle . 2019年11月13日閲覧。
- ^ Rossum, Guido van (2003年10月17日). 「[Python-Dev] sort() の戻り値」 . 2022年2月1日閲覧。
外部リンク
- マーティン・ファウラーのブリキのオリジナル記事でこの用語が生まれた
- 流暢なインターフェースでXMLを書くDelphiの例
- C#で書かれた.NETの流暢な検証ライブラリ。2017年12月23日にWayback Machineにアーカイブされました。
- BNF表記から正式なJava Fluent APIを作成するためのチュートリアル
- 流暢なインターフェースは悪である
- 流暢なAPIを開発するのはとてもクールです