ソフトウェア工学において、流れるようなインターフェースとは、メソッド連鎖に大きく依存した設計のオブジェクト指向 APIである。その目的は、ドメイン固有言語(DSL)を作成することでコードの可読性を高めることである。この用語は、2005年にエリック・エバンスとマーティン・ファウラーによって造られた。[1]
実装
流暢なインターフェースは、通常、メソッド連鎖を使用してメソッドカスケーディングを実装することによって実装されます(カスケーディングをネイティブにサポートしていない言語の場合)。具体的には、各メソッドが、それが接続されているオブジェクトを返すようにすることで実装されます[要出典]this 。これは、またはと呼ばれることがよくあります self。より抽象的に言えば、流暢なインターフェースは、メソッド連鎖内の後続の呼び出しの命令コンテキストを中継します。通常、コンテキストは
「流れるようなインターフェース」とは、単に連鎖によるメソッドのカスケード以上の意味を持つことに注意してください。それは、「ネストされた関数やオブジェクトのスコープ」などの他の技術を使用して、DSLのように読み取れるインターフェースを設計することを伴います。[1]
歴史
「流れるようなインターフェース」という用語は 2005 年後半に造られましたが、この全体的なインターフェースのスタイルは、 1970 年代のSmalltalkでのメソッド カスケードの発明と、1980 年代の多数の例にまで遡ります。一般的な例としては、C++のiostreamライブラリがあります。これは、メッセージの受け渡しにor演算子を使用し、複数のデータを同じオブジェクトに送信し、他のメソッド呼び出しの「マニピュレータ」を許可します。その他の初期の例としては、オブジェクトの作成とプロパティの割り当てにこのスタイルを使用した Garnet システム (1988 年の Lisp) と Amulet システム (1994 年の C++) があります。
<<>>
例
C#
C# は、LINQで Fluent Programming を広範に使用し、「標準クエリ演算子」を使用してクエリを構築します。実装は拡張メソッドに基づいています。
var translations = new Dictionary < string , string > { { "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 ());
// 同じクエリを段階的に構築します:
var filtered = translations . Where ( t => t . Key . Contains ( "a" )); var sorted = filtered . OrderBy ( t => t . Value . Length ); var 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++ のより伝統的なインターフェースの上に、流れるようなインターフェース ラッパーを提供する例です。
// 基本的な定義
class GlutApp { private : int w_ , h_ , x_ , y_ , argc_ , display_mode_ ; char ** argv_ ; char * title_ ; public : GlutApp ( int argc , char ** argv ) { argc_ = argc ; argv_ = argv ; } void setDisplayMode ( int mode ) { display_mode_ = mode ; } int getDisplayMode () { return display_mode_ ; } void setWindowSize ( int w , int h ) { w_ = w ; h_ = h ; } void setWindowPosition ( int x , int y ) { x_ = x ; y_ = y ; } void setTitle ( const char * title ) { title_ = title ; } void create (){;} }; // 基本的な使用法int main ( int argc , char ** argv ) { GlutApp app ( argc , argv ); 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
。作成する();
}
// Fluent ラッパー
クラスFluentGlutApp : private GlutApp { public : FluentGlutApp ( int argc , char ** argv ) : GlutApp ( argc , argv ) {} // 親コンストラクタを継承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 char * title ) { setTitle ( title ); return * this ; } // create() の後に連鎖するのは意味がないので、*this を返さないでください。void create () { GlutApp :: create (); } };
// Fluent の使用法
int main ( int argc , char ** argv ) { FluentGlutApp ( argc , argv ) . 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 を式ツリーの形式でオブジェクトとして表現できるため、次のような型セーフな流れるようなインターフェースを作成できるようになります。
顧客obj = ... obj . property ( "name" ). eq ( "John" )
次のように書くこともできます:
メソッド< Customer > ( customer -> customer . getName () == "John" )
また、モック オブジェクト テストライブラリ EasyMock は、このスタイルのインターフェイスを広範に使用して、表現力豊かなプログラミング インターフェイスを提供します。
コレクションmockCollection = EasyMock.createMock ( Collection.class ) ; EasyMock.expect ( mockCollection.remove ( null ) ) . andThrow ( new NullPointerException ( ) ) . atLeastOnce ( ) ;
Java Swing API では、LayoutManager インターフェースは、Container オブジェクトがコンポーネントの配置を制御する方法を定義します。より強力なLayoutManager実装の 1 つは、レイアウト制御を行う方法を指定するためにクラスの使用を必要とする GridBagLayout クラスですGridBagConstraints。このクラスの一般的な使用例は次のようになります。
GridBagLayout gl = new GridBagLayout (); JPanel p = new JPanel (); p .setLayout ( gl ) ;
JLabel l = new JLabel ( "名前:" ); JTextField nm = new 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]
JPanel p = new JPanel (); Packer pk = new Packer ( p );
JLabel l = new JLabel ( "名前:" ); JTextField nm = new JTextField ( 10 );
pk.pack ( l ) .gridx ( 0 ) .gridy ( 0 ) ; pk.pack ( nm ) .gridx ( 1 ) .gridy ( 0 ) .fillx ( ) ;
Fluent 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" ); }
特性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にはさまざまなアプローチがありますが、最も簡単な方法の 1 つは、属性を読み取り/書き込みとして宣言し、givenキーワードを使用することです。型注釈はオプションですが、ネイティブの段階的型付けにより、パブリック属性に直接書き込む方がはるかに安全です。
class Employee {
subset Salary of Real where * > 0 ;
subset NonEmptyString of Str where * ~~ /\S/ ; # 少なくとも 1 つのスペース以外の文字
空で ない文字列 $.name は rwです。
空でない文字列$ .surnameはrwです。
給与$.salaryはrwです。
メソッドの 要点{
return qq:to[END];
名前: $.name
姓: $.surname
給与: $.salary
END
}
}
私の $employee =従業員. new ();
与えられた $employee {
. name = 'サリー' ;
. surname = '乗車' ;
.給料 = 200 ;
}
$employeeと言います。
# 出力:
# 名前: Sally
# 姓: Ride
# 給与: 200
PHP の
PHPでは、$thisインスタンスを表す特別な変数を使用して現在のオブジェクトを返すことができます。したがって、return $this;メソッドはインスタンスを返すようになります。以下の例では、クラスEmployeeと、その名前、姓、給与を設定する 3 つのメソッドを定義します。それぞれがクラスのインスタンスを返すため、Employeeメソッドを連鎖させることができます。
<?php を 宣言します( strict_types = 1 );
最終的 なクラス Employee
{
private string $name ;
private string $surname ;
private string $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
>>>詩( 「旅されなかった道」) .インデント( 4 ) .接尾辞( 「ロバート・フロスト」) .タイトル
'旅されなかった道 - ロバート・フロスト'
迅速
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' );
定数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# など)のサブクラスでは、戻り値の型を変更するために、流れるようなインターフェースに参加するスーパークラスのすべてのメソッドをオーバーライドしなければならないことがよくあります。例:
class A { public 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 境界多態性を表現できる言語では、それを使用してこの問題を回避できます。例:
抽象クラス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 (); // これも動作します。
親クラスのインスタンスを作成できるようにするには、親クラスをAbstractAと の2 つのクラスに分割する必要があることに注意してください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を開発するのはとてもクールです
