流式接口是软件工程中面向对象API的一种实现方式,以提供更为可读的源代码。最早由Eric Evans与Martin Fowler于2005年提出。C++的iostream流式调用就是一个典型的例子。Smalltalk在1970年代就实现了方法瀑布调用。
流式接口通常采取方法瀑布调用(具体说是方法链式调用)来转发一系列对象方法调用的上下文。这个上下文(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
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'