流式接口

流式接口(fluent interface)是軟件工程面向對象API的一種實現方式,以提供更為可讀的原始碼。最早由Eric Evans英語Eric Evans (technologist)Martin Fowler於2005年提出。

通常採取方法瀑布調用 (具體說是方法鏈式調用)來轉發一系列對象方法調用的上下文 [1]。這個上下文(context)通常是指:

  • 通過被調方法的返回值定義
  • 自引用,新的上下文等於老的上下文。
  • 返回一個空的上下文來終止。

C++iostream流式調用就是一個典型的例子。Smalltalk在1970年代就實現了方法瀑布調用

例子

JavaScript

用於數據庫查詢的jQuery,例如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
    })

JavaScript使用原型繼承與`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();

Java

jOOQ庫模擬了SQL

Author 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))));

C#

C#在LINQ中大量使用 standard query operators擴展方法

var translations = new Dictionary<string, string>
                   {
                       {"cat", "chat"},
                       {"dog", "chien"},
                       {"fish", "poisson"},
                       {"bird", "oiseau"}
                   };

// Find translations for English words containing the letter "a",
// sorted by length and displayed in uppercase
IEnumerable<string> query = translations
	.Where   (t => t.Key.Contains("a"))
	.OrderBy (t => t.Value.Length)
	.Select  (t => t.Value.ToUpper());

// The same query constructed progressively:
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());

流式接口可用於一系列方法,他們運行在同一對象上。

// Defines the data context
class 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(); // Initializes the context

    // set the value for properties
    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;
    }

    // Prints the data to console
    public void Print()
    {
        Console.WriteLine("First name: {0} \nLast name: {1} \nSex: {2} \nAddress: {3}", _context.FirstName, _context.LastName, _context.Sex, _context.Address);
    }
}

class Program
{
    static void Main(string[] args)
    {
        // Object creation
        Customer c1 = new Customer();
        // Using the method chaining to assign & print data with a single line
        c1.FirstName("vinod").LastName("srivastav").Sex("male").Address("bangalore").Print();
    }
}

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();
 }

Ruby

Ruby語言允許修改核心類,這使得流式接口成為原生易於實現。

# Add methods to String class
class String
  def prefix(raw)
    "#{raw} #{self}"
  end
  def suffix(raw)
    "#{self} #{raw}"
  end
  def indent(raw)
    raw = " " * raw if raw.kind_of? Fixnum
    prefix(raw)
  end
end
 
# Fluent interface
message = "there"
puts message.prefix("hello")
            .suffix("world")
            .indent(8)

Scala

Scala supports a fluent syntax for both method calls and class mixins, using traits and the with keyword. For example:

class Color { def rgb(): Tuple3[Decimal] }
object Black extends Color { override def rgb(): Tuple3[Decimal] = ("0", "0", "0"); }

trait GUIWindow {
  // Rendering methods that return this for fluent drawing
  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 // Don't draw anything, just return this, for child implementations to use fluently

  def top_left(): Position
  def bottom_left(): Position
  def top_right(): Position
  def bottom_right(): Position
}

trait WindowBorder extends 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())
   }
}

class SwingWindow extends GUIWindow { ... }

val appWin = new SwingWindow() with WindowBorder
appWin.render()

Perl 6

In Perl 6, there are many approaches, but one of the simplest is to declare attributes as read/write and use the given keyword. The type annotations are optional, but the native gradual typing makes it much safer to write directly to public attributes.

class Employee {
    subset Salary         of Real where * > 0;
    subset NonEmptyString of Str  where * ~~ /\S/; # at least one non-space character

    has NonEmptyString $.name    is rw;
    has NonEmptyString $.surname is rw;
    has Salary         $.salary  is rw;

    method gist {
        return qq:to[END];
        Name:    $.name
        Surname: $.surname
        Salary:  $.salary
        END
    }
}
my $employee = Employee.new();

given $employee {
    .name    = 'Sally';
    .surname = 'Ride';
    .salary  = 200;
}

say $employee;

# Output:
# Name:    Sally
# Surname: Ride
# Salary:  200

PHP

在PHP中,可以使用表示實例的特殊變量$this返回當前對象。因此返回$this將使方法返回實例。下面的示例定義了一個Employee類和三個方法來設置它的名稱、姓和薪水。每個Employee類的實例允許調用這些方法。

<?php
class Employee
{
    public $name;
    public $surName; 
    public $salary;

    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    public function setSurname($surname)
    {
        $this->surName = $surname;

        return $this;
    }

    public function setSalary($salary)
    {
        $this->salary = $salary;

        return $this;
    }

    public function __toString()
    {
        $employeeInfo = 'Name: ' . $this->name . PHP_EOL;
        $employeeInfo .= 'Surname: ' . $this->surName . PHP_EOL;
        $employeeInfo .= 'Salary: ' . $this->salary . PHP_EOL;

        return $employeeInfo;
    }
}

# Create a new instance of the Employee class, Tom Smith, with a salary of 100:
$employee = (new Employee())
                ->setName('Tom')
                ->setSurname('Smith')
                ->setSalary('100');

# Display the value of the Employee instance:
echo $employee;

# Display:
# Name: Tom
# Surname: Smith
# Salary: 100

Python

Python通過在實例方法中返回`self`:

class Poem(object):
    def __init__(self, content):
        self.content = content

    def indent(self, spaces):
        self.content = " " * spaces + self.content
        return self

    def suffix(self, content):
        self.content = self.content + " - " + content
        return self
>>> Poem("Road Not Travelled").indent(4).suffix("Robert Frost").content
'    Road Not Travelled - Robert Frost'


Visual Basic.Net


參考文獻

  1. ^ bliki: FluentInterface. [2016-07-02]. (原始內容存檔於2021-03-08). 

外部連結