簡介
流式接口通常採取方法瀑布調用(具體說是方法鏈式調用)來轉發一系列對象方法調用的上下文。這個上下文(context)通常是指:
通過被調方法的返回值定義
自引用,新的上下文等於老的上下文
返回一個空的上下文來終止
設計一個好的流暢API並不像從方法中返回“this”那么容易。可讀性是流暢API的一個關鍵方面,需要您注意如何命名和構造代碼。實現流暢的API也很可能會增加
代碼的複雜性。但是,如果設計得當,流暢的API具有很強的可發現性並提高了代碼的可讀性。
流暢接口中的方法通常可以連結,從而創建方法調用流。要允許連結方法,只有一條規則:
該方法必須返回非void值。
由於方法的返回類型決定了接下來可以調用哪種方法,因此它確定了它的流程。
以下是非流動方法:
public class NonFlowing
{
public void Log(string str)
{
...
}
}
就像Log()方法返回void,不可能有方法調用鏈:
NonFlowing nonFlowing = new NonFlowing();
// Error: fails to compile
nonFlowing.Log("line 1")
.Log("line 2")
.Log("line 3");
我們可以通過返回一個非空類型來補救這個問題:
public class Flowing
{
public Flowing Log(string str)
{
...
return this;
}
}
Log()方法返回Flowing實例時,我們可以調用哪些方法Log()已經結束了。我們可以將調用連結到Log()方法:
Flowing flowing = new Flowing();
flowing.Log("line 1")
.Log("line 2")
.Log("line 3");
如果我們改變Log()方法的返回類型,則流更改為該類型:
public class FlowingContext
{
public string Log(string str)
{
...
return str;
}
}
FlowingContext flowing = new FlowingContext();
// Returns "LINE"
flowing.Log("line 1")
.Substring(0, 4)
.ToUpper();
設計原則
可讀性:在設計FLUENT界面時,對可讀性的設計是非常重要的。
高級函式:有時類具有多個較低級別的屬性或函式,這些屬性或函式通常是一起修改的。在這些情況下,可以通過提供更高級別的抽象來提高可讀性。
預先填充的欄位:在某些情況下,你可以期望通常使用一組特定的參數調用代碼。在這種情況下,提供具有該組參數的預先填充的實例可能是方便的。
發展歷史
“Fluent Interface”這個術語是在2005年底創造出來的,儘管這種整體界面風格可以追溯到20世紀70年代的Smalltalk的方法級聯發明,也是20世紀80年代的很多例子。 一個常見的例子是C ++中的iostream庫,它使用<<或>>操作符來傳遞訊息,將多個數據傳送到同一對象,並允許“操縱器”用於其他方法調用。 其他早期的例子包括Garnet系統(從1988年在Lisp)和Amulet系統(從1994年在C ++),它使用這種風格進行對象創建和屬性分配。
實例
JavaScript
用於資料庫查詢的jQuery:
// 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’。
// 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#
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支持使用方法和使用關鍵字的方法調用和類混合的流暢語法,例如:
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
在Perl 6中,有許多方法,但最簡單的方法之一是將屬性聲明為讀/寫並使用給定的關鍵字。類型注釋是可選的,但是本機漸進式類型使得直接寫入公共屬性更加安全。
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
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'