Folyékony interfész
A számítógép-programozásban a folyékony interfész egy módszer objektumorientált API-k megalkotására. A program forrásának olvashatósága megközelíti, vagy eléri az általános írott szövegekét. A folyékony interfészt rendszerint metódusok láncolásával valósítják meg, egy utasítás kontextusát a következő utasításnak adva át. Általában a kontextus:
- meghatározója a hívás visszaadott értéke,
- önhivatkozó, a következő kontextus ekvivalens az utolsóval,
- void kontextus visszaadásával terminál.
Története
Az interfész általános stílusa az 1970-es évekre megy vissza, egészen a Smalltalkig. Az 1980-as évekből számos példa ismert. Ismert példa a C++ iostream könyvtára, aminek << és >> operátorai több adatot küldenek el ugyanannak az objektumnak, és más metódushívások számára lehetővé teszik manipulátorok használatát. További korábbi példák a Garnet system (1988-tól, Lisp-ben) és az Amulet system (1994-től, C++-ban). A folyékony interfész elnevezést 2005 végén alkották meg.
Példák
C#
A C#-ban gyakori a folyékony interfész használata a LINQ-val, amikor lekérdezéseket építenek szabványos lekérdezésoperátorokkal. A megvalósítás kiterjesztési metódusokon alapul.
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());
Fluent interface can also be used to chain a set of method, which operates/shares the same object. Like instead of creating a customer class we can create a data context which can be decorated with fluent interface as follows.
// 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++
C++-ban a folyékony interfész gyakori az iosteam könyvtár használatakor, ami túlterhelt operátorokat kapcsol össze. A következő példa folyékony interfész adaptert mutat egy hagyományosabb interfész fölött:
// 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();
}
D
Az egységes függvényhívás szintaxisnak köszönhetően (Uniform Function Call Syntax, UFCS) D-ben különösen egyszerű a metódusok láncolása.[1] Ha azt írjuk, hogy
x.toInt();
de nincs x
típusának toInt()
tagfüggvénye, akkor a fordító keres egy
toInt(x);
formájú független függvényt. Ez lehetővé teszi a metódusok láncolását
x.toInt().toString(format);
formában, ahelyett, hogy
toString(toInt(x),format);
Java
A jOOQ könyvtár az SQL-t folyékony interfészként modellezi:
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))));
Az op4j könyvtár[2] lehetővé teszi kisegítő feladatok beépítését a láncba, mint struktúra iteráció, adatkonverzió, szűrés:
String[] datesStr = new String[] {"12-10-1492", "06-12-1978"};
...
List<Calendar> dates =
Op.on(datesStr).toList().map(FnString.toCalendar("dd-MM-yyyy")).get();
A fluflu annotációfeldolgozó[3] annotációkat biztosít folyékony interfész létrehozásához. A JaQue könyvtár[4] lehetővé teszi a Java 8 lambdáinak kifejezésfa objektumként[5] való reprezentációját. Ezzel típusbiztos folyékony interfész alakítható ki. Azaz ahelyett, hogy:
Customer obj = ...
obj.property("name").eq("John")
írható, hogy:
method<Customer>(customer -> customer.getName() == "John")
Az EasyMock mock objektumos tesztelő könyvtár[6] kiterjedten használja ezt a stílust, hogy kifejező interfészt adjon a programnak:
Collection mockCollection = EasyMock.createMock(Collection.class);
EasyMock
.expect(mockCollection.remove(null))
.andThrow(new NullPointerException())
.atLeastOnce();
A Java Swing APIban a LayoutManager interfész definiálja, hoigy a Container objektumok hogyan lehet ellenőrzött Component elhelyezésük. Az egyik legjobban konfigurálható LayoutManager a GridBagLayout, amiben GridBagConstraints osztály használható az elhelyezés vezérlésére. Egy tipikus példa:
GridBagLayout gl = new GridBagLayout();
JPanel p = new JPanel();
p.setLayout( gl );
JLabel l = new JLabel("Name:");
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 );
Ez hosszú kód írását igényli, amiben nehéz látni, hogy mi is történik. A Packer osztály[7] folyékony mechanizmussal látja el az osztályt, ezzel a fenti kód tömörebben írható:
JPanel p = new JPanel();
Packer pk = new Packer( p );
JLabel l = new JLabel("Name:");
JTextField nm = new JTextField(10);
pk.pack( l ).gridx(0).gridy(0);
pk.pack( nm ).gridx(1).gridy(0).fillx();
Még több példa is létezik, amikor a folyékony interfész nagyban egyszerűsíti a program lekódolását, továbbá segíti egy API nyelv létrehozását, ami segíti az API kezelését, mivel egy metódus visszatérési értéke kontextust ad a következő akciónak.
JavaScript
Több JavaScript könyvtár is ezen a megközelítésen alapul. Talán a jQuery a legismertebb. Tipikusan folyékony építőket használ az adatbázis lekérdezésekhez, például a dynamite-ban:[8]
// getting an item from a table
client.getItem('user-table')
.setHashKey('userId', 'userA')
.setRangeKey('column', '@')
.execute()
.then(function(data) {
// data.result: the resulting object
})
Egyszerű példa JavaScriptben a prototípus öröklés és a this használata:
// 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();
Perl 6
Perl 6-ban a folyékony interfészre több megközelítés is létezik. Az egyik legegyszerűbb az attribútumok deklarálása read/write tulajdonságokkal, és a given kulcsszó használata. A típus annotációk opcionálisak, de a natív graduális típusozottság sokkal biztonságosabbá teszi a publikus attribútumok közvetlen írását.
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-ben az aktuális objektum hivatkozható a $this változóval, ami a példányt reprezentálja. Így a return $this; utasítás az aktuális példányt adja vissza. A példa definiál egy Employee osztályt és három metódust a név, vezetéknév és a fizetés beállítását. Mindegyik visszaadja a példányt, így a metódusok összekapcsolhatók.
<?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
Pythonban a folyékony interfész megvalósításának egy módja, hogy a példánymetódusok a self objektumot adják vissza:
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'
Ruby
A Ruby lehetővé teszi a beépített osztályok bővítését, így a folyékony interfészek támogatása is természetesebb. A stringek a String osztály példányai. Ha új metódusokat definiálunk a String osztályhoz, amelyek stringet adnak vissza, akkor a metódusok láncolása természetesen fog működni. A példában három új metódust definiálunk: indent, prefix és suffix. Mindegyik stringet ad vissza, aminek a String osztály példányaként szintén megvan a három új művelete.
# 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
A Scala támogatja metódushívások és mixinek esetén is a folyékony szintaxist, a trait és a with kulcsszavak használatával. Például:
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()
Swift
Swift 3.0+-ban többek közül a self visszaadásával is megvalósítható a minta:
class 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: "I like turtles")
Problémák
Hibakeresés
A hibakeresést megnehezíti, ha egy sorba írják, mivel a debugger nem tud több töréspontot elhelyezni a láncba. Nehezebb megtudni, hogy melyik metódushívás dobott kivételt, különösen, ha egy metódust többször is hívtak. Ezek a problémák kezelhetők, ha az utasítást nem egy sorba írják. Azaz ahelyett, hogy
java.nio.ByteBuffer.allocate(10).rewind().limit(100);
azt írják, hogy
java.nio.ByteBuffer
.allocate(10)
.rewind()
.limit(100);
Azonban néhány debugger mindig csak az első sort mutatja, habár a kivétel bármelyik sorban keletkezhetett.
Naplózás
További probléma a naplózás. Például,
ByteBuffer buffer = ByteBuffer.allocate(10).rewind().limit(100);
de ha a buffer
állapotát akarjuk feljegyezni a rewind()
hívása után, akkor meg kell törni a hívásfolyamot:
ByteBuffer buffer = ByteBuffer.allocate(10).rewind();
log.debug("First byte after rewind is " + buffer.get(0));
buffer.limit(100);
Kiterjesztett metódusokkal új kiterjesztés definiálható, ami beburkolja a kívánt tevékenységet. Például C#-ban:
static class ByteBufferExtensions
{
public static Bytebuffer Log(this ByteBuffer buffer, Log log, Action<ByteBuffer> getMessage)
{
string message = getMessage( buffer );
log.debug( message );
return buffer;
}
}
// Usage:
ByteBuffer
.Allocate(10)
.Rewind()
.Log( log, b => "First byte after rewind is " + b.Get(0) )
.Limit(100);
Öröklődés
Öröklődéskor a gyermek osztályok gyakran felüldefiniálnak öröklött metódusokat, hogy megváltoztassák a visszatérési típust. Például Javában:
class A {
public A doThis() { ... }
}
class B extends A{
public A doThis() { super.doThis(); return this; } // Must change return type to B.
public B doThat() { ... }
}
...
A a = new B().doThat().doThis(); // It works even without overriding A.doThis().
B b = new B().doThis().doThat(); // It would fail without overriding A.doThis().
F-korlátos minősítéssel ez egyszerűsíthető. Például Javában:
abstract class AbstractA<T extends AbstractA<T>> {
@SuppressWarnings("unchecked")
public T doThis() { ...; return (T)this; }
}
class A extends AbstractA<A> {}
class B extends AbstractA<B> {
public B doThat() { ...; return this; }
}
...
B b = new B().doThis().doThat(); // Works!
A a = new A().doThis(); // Also works.
Ahhoz, hogy a szülő osztályt példányosítani lehessen, ketté kell bontani: az A osztály tartalmazza a konstruktorokat, és az AbstractA a metódusokat. A megoldás tovább folytatható az unoka és a további leszármazott osztállyal:
abstract class AbstractB<T extends AbstractB<T>> extends AbstractA<T> {
@SuppressWarnings("unchecked")
public T doThat() { ...; return (T)this; }
}
class B extends 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(); // Works!
B b = new B().doThis().doThat(); // Still works.
Jegyzetek
- ↑ Uniform Function Call Syntax, Dr. Dobbs Journal, 28 Mar 2012
- ↑ http://www.op4j.org/
- ↑ https://github.com/verhas/fluflu
- ↑ https://github.com/TrigerSoft/jaque
- ↑ ttp://msdn.microsoft.com/en-us/library/bb397951.aspx
- ↑ http://easymock.org/
- ↑ Archivált másolat. [2017. március 21-i dátummal az eredetiből archiválva]. (Hozzáférés: 2017. december 19.)
- ↑ https://github.com/Medium/dynamite
Fordítás
Ez a szócikk részben vagy egészben a Fluent interface című angol Wikipédia-szócikk fordításán alapul. Az eredeti cikk szerkesztőit annak laptörténete sorolja fel. Ez a jelzés csupán a megfogalmazás eredetét és a szerzői jogokat jelzi, nem szolgál a cikkben szereplő információk forrásmegjelöléseként.