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 szerkesztés

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 szerkesztés

C# szerkesztés

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++ szerkesztés

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 szerkesztés

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 szerkesztés

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 szerkesztés

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 szerkesztés

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 szerkesztés

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 szerkesztés

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 szerkesztés

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 szerkesztés

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 szerkesztés

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 szerkesztés

Hibakeresés szerkeszté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 szerkeszté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 szerkeszté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 szerkesztés

  1. Uniform Function Call Syntax, Dr. Dobbs Journal, 28 Mar 2012
  2. http://www.op4j.org/
  3. https://github.com/verhas/fluflu
  4. https://github.com/TrigerSoft/jaque
  5. ttp://msdn.microsoft.com/en-us/library/bb397951.aspx
  6. http://easymock.org/
  7. Archivált másolat. [2017. március 21-i dátummal az eredetiből archiválva]. (Hozzáférés: 2017. december 19.)
  8. https://github.com/Medium/dynamite

Fordítás szerkeszté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.