Skip to content

Fluent API

wikipedia Fluent interface

A fluent interface is normally implemented by using method cascading (concretely method chaining) to relay(传达) the instruction context of a subsequent call (but a fluent interface entails more than just method chaining [1]). Generally, the context is

  • defined through the return value of a called method

NOTE: context是通过被调用的method的返回值来定义的

  • self-referential, where the new context is equivalent to the last context

NOTE: "self-referential"的含义是 "自引用的",即新context和上一个context相同。

  • terminated through the return of a void context.

NOTE: 需要标识合适终止

NOTE: 上面这段话所着重描述的是 context,显然被连续调用的method,它们所共用的就是context

History

A common example is the iostream library in C++, which uses the << or >> operators for the message passing, sending multiple data to the same object and allowing "manipulators" for other method calls.

Examples

JavaScript

There are many examples of JS libraries that use some variant of this: jQuery probably being the most well known. Typically fluent builders are used to implement 'DB queries', for example in https://github.com/Medium/dynamite :

// getting an item from a table
client.getItem('user-table')
    .setHashKey('userId', 'userA')
    .setRangeKey('column', '@')
    .execute()
    .then(function(data) {
        // data.result: the resulting object
    })

NOTE: 这是典型的asynchronous programming,现代很多programming language都是支持这种paradigm的,在后面的"Asynchronous programming"章节会进行详细描述。

A simple way to do this in javascript is using prototype inheritance and this.

// example from http://schier.co/post/method-chaining-in-javascript
// define the class
var Kitten = function() {
  this.name = 'Garfield';
  this.color = 'brown';
  this.gender = 'male';
};

Kitten.prototype.setName = function(name) {
  this.name = name;
  return this;
};

Kitten.prototype.setColor = function(color) {
  this.color = color;
  return this;
};

Kitten.prototype.setGender = function(gender) {
  this.gender = gender;
  return this;
};

Kitten.prototype.save = function() {
  console.log(
    'saving ' + this.name + ', the ' +
    this.color + ' ' + this.gender + ' kitten...'
  );

  // save to database here...

  return this;
};

// use it
new Kitten()
  .setName('Bob')
  .setColor('black')
  .setGender('male')
  .save();

Although it's lots of clumsy code, a better alternative would be to pack these method in single function thus creating a framework.

A more general way to do this is implemented in mu-ffsm.

var mkChained = function(spec) {
  return function(init) {
    var s = spec[0] ? spec[0](init) : 0;

    var i = function(opt) {
      return spec[1] ? spec[1](s, opt) : s;
    }

    Object.keys(spec).forEach(
      function(name){
        // skip `entry` and `exit` functions
        if(/^\d+$/.test(name))
          return;

        // transition 'name : (s, opt) -> s'
        i[name] = function(opt) {
          s = spec[name](s, opt);
          return i;
        };
    });

    return i;
  }
};

var API = mkChained({
  0:    function(opt)    {return ;/* create initial state */},
  then: function(s, opt) {return s; /* new state */},
  whut: function(s, opt) {return s; /* new state */},
  1:    function(s, opt) {return ;/* compute final value */}
});

// We create an instance of our newly crafted API,
var call = API() // entry
   .whut()       // transition
   .then()       // transition
   .whut();      // transition

// And call it
var result0 = call() // exit
  , result1 = call() // exit

NOTE: JavaScript这种语言是非常容易实现这种功能的

Java

The jOOQ library models SQL as a fluent API in Java

Author a = AUTHOR.as("a");
create.selectFrom(a)
      .where(exists(selectOne()
                   .from(BOOK)
                   .where(BOOK.STATUS.eq(BOOK_STATUS.SOLD_OUT))
                   .and(BOOK.AUTHOR_ID.eq(a.ID))));

The op4j library enables the use of fluent code for performing auxiliary tasks like structure iteration, data conversion, filtering, etc.

String[] datesStr = new String[] {"12-10-1492", "06-12-1978"};

List<Calendar> dates = 
    Op.on(datesStr).toList().map(FnString.toCalendar("dd-MM-yyyy")).get();

C++

A common use of the fluent interface in C++ is the standard iostream, which chains overloaded operators.

The following is an example of providing a fluent interface wrapper on top of a more traditional interface in C++:

 // Basic definition
 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(){;}
 };
 // Basic usage
 int main(int argc, char **argv) {
     GlutApp app(argc, argv);
     app.setDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_ALPHA|GLUT_DEPTH); // Set framebuffer params
     app.setWindowSize(500, 500); // Set window params
     app.setWindowPosition(200, 200);
     app.setTitle("My OpenGL/GLUT App");
     app.create();
 }

 // Fluent wrapper
 class FluentGlutApp : private GlutApp {
 public:
     FluentGlutApp(int argc, char **argv) : GlutApp(argc, argv) {} // Inherit parent constructor
     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;
     }
     // It doesn't make sense to chain after create(), so don't return *this
     void create() {
         GlutApp::create();
     }
 };
 // Fluent usage
 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();
 }

Problems

Debugging & error reporting

NOTE: 带来了调试的不便

Logging

NOTE:

One more issue is with adding log statements.

ByteBuffer buffer = ByteBuffer.allocate(10).rewind().limit(100);

E.g. to log the state of buffer after rewind() method call, it is necessary to break the fluent calls:

ByteBuffer buffer = ByteBuffer.allocate(10).rewind();
log.debug("First byte after rewind is " + buffer.get(0));
buffer.limit(100);

Subclasses

用法总结

下面是对fluent API的用法总结:

Builder pattern

Bluent API的一个重要用法就是实现builder pattern。

What is the difference between a fluent interface and the Builder pattern?

C++ Named Parameter Idiom

参见: wikibooks Named Parameter Idiom

httpcomponents#HttpClient

Fluent API

// Execute a GET with timeout settings and return response content as String.
Request.Get("http://somehost/")
        .connectTimeout(1000)
        .socketTimeout(1000)
        .execute().returnContent().asString();

Asynchronous programming

jQuery

在前面的JavaScript章节中给出了非常好的例子。

Apache Curator

Fluent API and pipeline

Fluent API 和 Unix pipeline 是有些类似的,它们都是chain。