Flytande gränssnitt
Inom mjukvaruteknik är ett flytande gränssnitt ett objektorienterat API vars design i stor utsträckning bygger på metodkedja . Dess mål är att öka kodläsbarheten genom att skapa ett domänspecifikt språk ( DSL). Termen myntades 2005 av Eric Evans och Martin Fowler .
Genomförande
Ett flytande gränssnitt implementeras normalt genom att använda metodkedjning för att implementera metodkaskadning (på språk som inte har stöd för kaskadkoppling), konkret genom att låta varje metod returnera objektet som det är kopplat till, ofta kallat detta
eller själv
. Mer abstrakt uttryckt, ett flytande gränssnitt vidarebefordrar instruktionskontexten för ett efterföljande anrop i metodkedja, där kontexten generellt är
- Definieras genom returvärdet för en anropad metod
- Självrefererande , där det nya sammanhanget är likvärdigt med det sista sammanhanget
- Avslutas genom återlämnande av ett tomt sammanhang
Observera att ett "flytande gränssnitt" betyder mer än bara metodkaskad via kedja; det innebär att designa ett gränssnitt som läser som en DSL, med andra tekniker som "kapslade funktioner och objektomfattning".
Historia
Termen "flytande gränssnitt" myntades i slutet av 2005, även om den här övergripande gränssnittsstilen dateras till uppfinningen av metoden kaskad i Smalltalk på 1970-talet, och många exempel på 1980-talet. Ett vanligt exempel är iostream- biblioteket i C++ , som använder operatorerna <<
eller >>
för att skicka meddelandet, skicka flera data till samma objekt och tillåta "manipulatorer" för andra metodanrop. Andra tidiga exempel inkluderar Garnet-systemet (från 1988 i Lisp) och Amulet-systemet (från 1994 i C++) som använde denna stil för att skapa objekt och tilldela egenskaper.
Exempel
C#
C# använder i stor utsträckning flytande programmering i LINQ för att skapa frågor med "standard frågeoperatorer". Implementeringen baseras på förlängningsmetoder .
var translations = new Dictionary < string , string > { { "cat" , "chat" }, { "dog" , "chien" }, { "fish" , "poisson" }, { "bird" , "oiseau" } }; // Hitta översättningar för engelska ord som innehåller bokstaven "a", // sorterade efter längd och visas med versaler IEnumerable < string > query = translations . Där ( t => t . Nyckel . Innehåller ( "a" )) . OrderBy ( t => t . Value . Length ) . Välj ( t => t . Värde . Till Övre ()); // Samma fråga konstruerad successivt: var filtered = translations . Där ( t => t . Nyckel . Innehåller ( "a") ); var sorterad = filtrerad . OrderBy ( t => t . Value . Length ); var finalQuery = sorterad . Välj ( t => t . Värde . Till Övre ());
Flytande gränssnitt kan också användas för att kedja en uppsättning metoder som driver/delar samma objekt. Istället för att skapa en kundklass kan vi skapa en datakontext som kan dekoreras med ett flytande gränssnitt enligt följande.
// Definierar datakontextklassen Context { public string FirstName { get ; set ; } offentlig sträng Efternamn { get ; set ; } offentlig sträng Sex { get ; set ; } public string Adress { get ; set ; } } class Customer { private Context _context = new Context (); // Initierar sammanhanget // ställer in värdet för egenskaper public Customer FirstName ( string firstName ) { _context . Förnamn = förnamn ; returnera detta ; } public Customer LastName ( sträng efternamn ) { _context . Efternamn = efternamn ; returnera detta ; } offentligt kundsex ( strängsex ) { _context . _ _ Sex = sex ; returnera detta ; } offentlig kundadress ( strängadress ) { _context . _ _ Adress = adress ; returnera detta ; } // Skriver ut data till konsol public void Skriv ut () { Console . WriteLine ( $"Förnamn: {_context.FirstName} \nEfternamn: {_context.LastName} \nKön: {_context.Sex} \nAdress: {_context.Address}" ) ; } } class Program { static void Main ( string [] args ) { // Objektskapande Kund c1 = ny kund (); // Använda metoden chaining för att tilldela och skriva ut data med en enda rad c1 . Förnamn ( "vinod" ). Efternamn ( "srivastav" ). Sex ( "man" ). Adress ( "bangalore" ). Skriv ut (); } }
C++
En vanlig användning av det flytande gränssnittet i C++ är standarden iostream , som kedjer överbelastade operatörer .
Följande är ett exempel på att tillhandahålla ett flytande gränssnitt ovanpå ett mer traditionellt gränssnitt i 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 skapa (){;} }; // Grundläggande användning int main ( int argc , char ** argv ) { GlutApp app ( argc , argv ); app . setDisplayMode ( GLUT_DOUBLE | GLUT_RGBA | GLUT_ALPHA | GLUT_DEPTH ); // Ställ in framebuffer params app . setWindowSize ( 500 , 500 ); // Ställ in fönsterparametrar app . setWindowPosition ( 200 , 200 ); app . setTitle ( "Min OpenGL/GLUT-app" ) ; app . skapa (); } // Fluent wrapper class FluentGlutApp : privat GlutApp { public : FluentGlutApp ( int argc , char ** argv ) : GlutApp ( argc , argv ) {} // Ärv överordnad konstruktör FluentGlutApp & withDoubleBuffer () { getDisplayUTMode ( ) ( setDisplay | GLUBMode () ); returnera * detta ; } FluentGlutApp & withRGBA () { setDisplayMode ( getDisplayMode () | GLUT_RGBA ); returnera * detta ; } FluentGlutApp & withAlpha () { setDisplayMode ( getDisplayMode () | GLUT_ALPHA ); returnera * detta ; } FluentGlutApp & withDepth () { setDisplayMode ( getDisplayMode () | GLUT_DEPTH ); returnera * detta ; } FluentGlutApp & tvärs ( int w , int h ) { setWindowSize ( w , h ); returnera * detta ; } FluentGlutApp & at ( int x , int y ) { setWindowPosition ( x , y ); returnera * detta ; } FluentGlutApp & named ( const char * title ) { setTitle ( title ); returnera * detta ; } // Det är inte meningsfullt att kedja efter create(), så returnera inte *this void create () { GlutApp :: create (); } }; // Flytande användning int main ( int argc , char ** argv ) { FluentGlutApp ( argc , argv ) . withDoubleBuffer (). medRGBA (). med Alpha (). withDepth () . vid ( 200 , 200 ). tvärsöver ( 500 , 500 ) . heter ( "Min OpenGL/GLUT-app" ) . skapa (); }
Java
Ett exempel på en flytande testförväntning i jMock-testramverket är:
håna . förväntar sig ( en gång ()). metod ( "m" ). med ( eller ( stringContains ( "hej" ), stringContains ( "hej" )) ) ;
jOOQ - biblioteket modellerar SQL som ett flytande API i Java:
Författare författare = FÖRFATTARE . som ( "författare" ); skapa . väljFrån ( författare ) . där ( finns ( välj En () . från ( BOK ) . där ( BOK . STATUS . eq ( BOOK_STATUS . SOLD_OUT )) . och ( BOOK . AUTHOR_ID . eq ( författare . ID ))));
Influensannoteringsprocessorn möjliggör skapandet av ett flytande API med Java-annoteringar.
JaQue-biblioteket gör att Java 8 Lambdas kan representeras som objekt i form av uttrycksträd under körning, vilket gör det möjligt att skapa typsäkra flytande gränssnitt, dvs istället för:
Kundobj = ... obj . _ egenskap ( "namn" ). eq ( "John" )
Man kan skriva:
metod < Kund > ( kund -> kund . getName () == "John" )
Dessutom använder det skenbara objekttestningsbiblioteket EasyMock i stor utsträckning denna typ av gränssnitt för att tillhandahålla ett uttrycksfullt programmeringsgränssnitt.
Samling mockCollection = EasyMock . createMock ( Collection . class ); EasyMock . förvänta ( mockCollection . ta bort ( null )) . andThrow ( ny NullPointerException ()) . åtminstone en gång ();
I Java Swing API definierar LayoutManager-gränssnittet hur Container-objekt kan ha kontrollerad komponentplacering. En av de mer kraftfulla LayoutManager-
implementeringarna är GridBagLayout-klassen som kräver användning av GridBagConstraints
-klassen för att specificera hur layoutkontroll sker. Ett typiskt exempel på användningen av denna klass är något i stil med följande.
0
0
GridBagLayout gl = ny GridBagLayout (); JPanel p = ny JPanel (); p . setLayout ( gl ); JLabel l = ny JLabel ( "Namn:" ); JTextField nm = nytt JTextField ( 10 ); GridBagConstraints gc = nya GridBagConstraints (); gc . gridx = ; gc . gridy = ; gc . fill = GridBagConstraints . INGEN ; p . add ( l , gc ); gc . gridx = 1 ; gc . fill = GridBagConstraints . HORISONTALT ; gc . viktx = 1 ; p . add ( nm , gc );
Detta skapar mycket kod och gör det svårt att se exakt vad som händer här. Packer -
klassen ger en flytande mekanism, så du skulle istället skriva:
00
0 JPanel p = ny JPanel (); Packer pk = ny Packer ( p ); JLabel l = ny JLabel ( "Namn:" ); JTextField nm = nytt JTextField ( 10 ); pk . pack ( l ). gridx ( ). gridy ( ); pk . pack ( nm ). gridx ( 1 ). rutnät ( ). fillx ();
Det finns många ställen där flytande API:er kan förenkla hur programvara skrivs och hjälpa till att skapa ett API-språk som hjälper användarna att vara mycket mer produktiva och bekväma med API:t eftersom returvärdet för en metod alltid ger ett sammanhang för ytterligare åtgärder i det sammanhanget.
JavaScript
Det finns många exempel på JavaScript-bibliotek som använder någon variant av detta: jQuery är förmodligen det mest kända. Vanligtvis används flytande byggare för att implementera "databasfrågor", till exempel i Dynamite-klientbiblioteket:
// få ett objekt från en tabellklient . getItem ( 'användartabell' ) . setHashKey ( 'userId' , 'userA' ) . setRangeKey ( 'kolumn' , '@' ) . exekvera () . then ( function ( data ) { // data.result: det resulterande objektet })
Ett enkelt sätt att göra detta i JavaScript är att använda prototyparv och detta
.
// exempel från https://schier.co/blog/2013/11/14/method-chaining-in-javascript.html class Kitten { constructor () { this . name = 'Garfield' ; detta . färg = 'orange' ; } setName ( namn ) { detta . namn = namn ; returnera detta ; } setColor ( color ) { this . färg = färg ; returnera detta ; } spara () { konsol . log ( `sparar ${ this . name } , ${ this . color } kattungen` ); returnera detta ; } } // använd den nya Kitten () . setName ( 'Salem' ) . setColor ( 'svart' ) . spara ();
Scala
Scala stöder en flytande syntax för både metodanrop och klassblandningar, med hjälp av egenskaper och nyckelordet med .
Till exempel:
klass Färg { def rgb (): Tuple3 [ Decimal ] } objekt Svart utökar Färg { override def rgb (): Tuple3 [ Decimal ] = ( "0" , "0" , "0" ); } trait GUIWindow { // Renderingsmetoder som returnerar detta för flytande ritning def set_pen_color ( färg : Färg ): detta . skriv def move_to ( pos : Position ): detta . skriv def line_to ( pos : Position , end_pos : Position ): detta . typ def render (): detta . typ = detta // Rita ingenting, returnera bara detta, för att underordnade implementeringar ska kunna använda flytande def top_left (): Position def bottom_left (): Position def top_right () : Position def bottom_right (): Position } egenskap WindowBorder utökar GUIWindow { def render (): GUIWindow = { super . rendera () . flytta_till ( övre_vänster ()) . set_pen_color ( svart ) . line_to ( top_right ()) . line_to ( bottom_right ()) . line_to ( bottom_left ()) . line_to ( top_left ()) } } klass SwingWindow utökar GUIWindow { ... } val appWin = new SwingWindow () med WindowBorder appWin . rendera ()
Raku
I Raku finns det många tillvägagångssätt, men en av de enklaste är att deklarera attribut som läs/skriv och använda det givna
nyckelordet. Typkommentarerna är valfria, men den inbyggda gradvisa skrivningen gör det mycket säkrare att skriva direkt till offentliga attribut.
0
klass Anställd { delmängd Lön av Real där * > ; delmängd NonEmptyString av Str där * ~~ /\S/ ; # minst ett tecken utan mellanslag har NonEmptyString $.name är rw ; har NonEmptyString $.surname är rw ; har Lön $.salary är rw ; method gist { return qq:to[END]; Namn: $.name Efternamn: $.surname Lön: $.salary SLUT } } min $employee = Anställd . ny (); given $employee { . name = 'Sally' ; . efternamn = 'Rid' ; . lön = 200 ; } säg $anställd ; # Output: # Namn: Sally # Efternamn: Ride # Lön: 200
PHP
I PHP kan man returnera det aktuella objektet genom att använda $this
specialvariabel som representerar instansen. Returnera därför $this;
kommer att få metoden att returnera instansen. Exemplet nedan definierar en klass Anställd
och tre metoder för att ställa in dess namn, efternamn och lön. Varje returnerar instansen av Employee
som tillåter kedja av metoder.
klass Anställd { privat sträng $namn ; privat sträng $efternamn ; privat sträng $lön ; public function setName ( sträng $name ) { $this -> name = $name ; returnera $detta ; } public function setSurname ( sträng $surname ) { $this -> efternamn = $efternamn ; returnera $detta ; } public function setSalary ( sträng $salary ) { $this -> salary = $salary ; returnera $detta ; } offentlig funktion __toString () { $employeeInfo = 'Namn: ' . $this -> namn . PHP_EOL ; $employeeInfo .= 'Efternamn: ' . $this -> efternamn . PHP_EOL ; $employeeInfo .= 'Lön: ' . $this -> lön . PHP_EOL ; returnera $employeeInfo ; } } # Skapa en ny instans av klassen Employee, Tom Smith, med en lön på 100: $employee = ( new Employee ()) -> setName ( 'Tom' ) -> setSurname ( 'Smith' ) -> setSalary ( '100' ); # Visa värdet på Employee-instansen: echo $employee ; # Display: # Namn: Tom # Efternamn: Smith # Lön: 100
Pytonorm
I Python är att returnera själv
i instansmetoden ett sätt att implementera det flytande mönstret.
Det avskräcks dock av språkets skapare, Guido van Rossum, och anses därför vara opytoniskt (inte idiomatiskt).
klass Dikt : def __init__ ( själv , titel : str ) -> Ingen : själv . title = title def indent ( self , spaces : int ): """Indrag dikten med det angivna antalet blanksteg.""" self . title = " " * mellanslag + själv . title return self def suffix ( self , author : str ): """Suffixera dikten med författarens namn.""" self . title = f " { self . title } - { author } " returnerar själv
>>> Dikt ( "Road Not Resed" ) . strecksats ( 4 ) . suffix ( "Robert Frost" ) . titel ' Road Not Traveled - Robert Frost'
Snabb
I Swift 3.0+ är att återvända själv
i funktionerna ett sätt att implementera det flytande mönstret.
class Person { var firstname : String = "" var efternamn : String = "" var favoritCitat : String = "" @ discardableResult func set ( firstname : String ) -> Self { self . firstname = firstname return self } @ discardableResult func set ( efternamn : String ) -> Self { self . efternamn = efternamn returnera själv } @ discardableResult func set ( favoritCitat : String ) -> Self { self . favoritQuote = favoritCitat returnerar själv } }
låt person = Person () . set ( förnamn : "John" ) . set ( efternamn : "Doe" ) . set ( favoritCitat : "Jag gillar sköldpaddor" )
Oföränderlighet
Det är möjligt att skapa oföränderliga flytande gränssnitt som använder kopiera-på-skriv- semantik. I denna variant av mönstret, istället för att modifiera interna egenskaper och returnera en referens till samma objekt, klonas objektet istället, med egenskaper ändrade på det klonade objektet, och det objektet returneras.
Fördelen med detta tillvägagångssätt är att gränssnittet kan användas för att skapa konfigurationer av objekt som kan forkastas från en viss punkt; Tillåta två eller flera objekt att dela en viss del av tillstånd och användas vidare utan att störa varandra.
JavaScript-exempel
Med hjälp av copy-on-write-semantik blir JavaScript-exemplet från ovan:
klass Kattunge { konstruktor () { detta . name = 'Garfield' ; detta . färg = 'orange' ; } setName ( name ) { const copy = new Kitten (); kopiera . färg = detta . färg ; kopiera . namn = namn ; returnera kopia ; } setColor ( color ) { const copy = new Kitten (); kopiera . namn = detta . namn ; kopiera . färg = färg ; returnera kopia ; } // ... } // använd den const kattunge1 = ny kattunge () . setName ( 'Salem' ); const kattunge2 = kattunge1 . setColor ( 'svart' ); konsol . log ( kattunge1 , kattunge2 ); // -> Kattunge({ namn: 'Salem', färg: 'orange' }), Kattunge({ namn: 'Salem', färg: 'svart' })
Problem
Fel kan inte fångas upp vid kompilering
I maskinskrivna språk kommer användningen av en konstruktor som kräver alla parametrar att misslyckas vid kompileringstillfället, medan det flytande tillvägagångssättet bara kommer att kunna generera runtime -fel och missar alla typsäkerhetskontroller av moderna kompilatorer. Det strider också mot " felsnabb "-metoden för felskydd.
Felsökning och felrapportering
Enradiga kedjesatser kan vara svårare att felsöka eftersom felsökningsverktyg kanske inte kan ställa in brytpunkter inom kedjan. Att gå igenom en enkelradssats i en felsökning kan också vara mindre bekvämt.
java . nio . ByteBuffer . fördela ( 10 ). spola tillbaka (). gräns ( 100 );
Ett annat problem är att det kanske inte är klart vilket av metodanropen som orsakade ett undantag, särskilt om det finns flera anrop till samma metod. Dessa problem kan övervinnas genom att dela upp uttalandet i flera rader vilket bevarar läsbarheten samtidigt som användaren kan ställa in brytpunkter inom kedjan och enkelt gå igenom koden rad för rad:
java . nio . ByteBuffer . fördela ( 10 ) . spola tillbaka () . gräns ( 100 );
Vissa felsökare visar dock alltid den första raden i undantaget bakåtspårning, även om undantaget har kastats på vilken rad som helst.
Skogsavverkning
Att lägga till inloggning i mitten av en kedja av flytande samtal kan vara ett problem. Till exempel givet:
ByteBuffer buffer = ByteBuffer . fördela ( 10 ). spola tillbaka (). gräns ( 100 );
För att logga bufferttillståndet efter rewind()
-metodanropet är det nödvändigt att bryta de flytande anropen :
0
ByteBuffer buffer = ByteBuffer . fördela ( 10 ). spola tillbaka (); logga . debug ( "Första byten efter bakåtspolning är " + buffert . get ( )); buffert . gräns ( 100 );
Detta kan lösas i språk som stöder tilläggsmetoder genom att definiera en ny tillägg för att linda in den önskade loggningsfunktionaliteten, till exempel i C# (med samma Java ByteBuffer-exempel som ovan):
0
static class ByteBufferExtensions { public static ByteBuffer Log ( denna ByteBuffer buffer , Log log , Action < ByteBuffer > getMessage ) { string message = getMessage ( buffert ); logga . debug ( meddelande ); returbuffert ; _ } } // Användning: ByteBuffer . Tilldela ( 10 ) . Spola tillbaka () . Logg ( log , b => "Första byte efter bakåtspolning är " + b . Hämta ( ) ) . Gräns ( 100 );
Underklasser
Underklasser i starkt typade språk (C++, Java, C#, etc.) måste ofta åsidosätta alla metoder från sin superklass som deltar i ett flytande gränssnitt för att ändra returtyp. Till exempel:
klass A { public A doThis () { ... } } klass B utökar A { public B doThis () { super . gör detta (); returnera detta ; } // Måste ändra returtyp till B. public B doThat () { ... } } ... A a = new B (). gör det (). gör detta (); // Detta skulle fungera även utan att åsidosätta A.doThis(). B b = nytt B (). gör detta (). gör det (); // Detta skulle misslyckas om A.doThis() inte åsidosattes.
Språk som kan uttrycka F-bunden polymorfism kan använda det för att undvika denna svårighet. Till exempel:
abstrakt klass AbstractA < T utökar SammanfattningA < T >> { @SuppressWarnings ( "unchecked" ) public T doThis ( ) { ...; returnera ( T ) detta ; } } klass A förlänger AbstractA < A > {} klass B förlänger AbstractA < B > { public B doThat () { ...; returnera detta ; } } ... B b = nytt B (). gör detta (). gör det (); // Arbetar! A a = nytt A (). gör detta (); // Fungerar också.
Observera att för att kunna skapa instanser av den överordnade klassen, var vi tvungna att dela upp den i två klasser — AbstractA
och A
, den senare utan innehåll (den skulle bara innehålla konstruktorer om de behövdes). Tillvägagångssättet kan enkelt utökas om vi vill ha underklasser (etc.) också:
abstrakt klass AbstractB < T utökar AbstractB < T >> utökar AbstractA < T > { @SuppressWarnings ( "unchecked" ) public T doThat () { ...; returnera ( T ) detta ; } } klass B förlänger AbstractB < B > {} abstrakt klass AbstractC < T förlänger AbstractC < T >> utökar AbstractB < T > { @SuppressWarnings ( "unchecked" ) public T foo () { ...; returnera ( T ) detta ; } } klass C utökar AbstractC < C > {} ... C c = nytt C (). gör detta (). gör det (). foo (); // Arbetar! B b = nytt B (). gör detta (). gör det (); // Fungerar fortfarande.
I ett beroende skrivet språk, t.ex. Scala, kan metoder också uttryckligen definieras som att de alltid returnerar detta
och kan därför endast definieras en gång för underklasser för att dra nytta av det flytande gränssnittet:
klass A { def doThis (): detta . typ = { ... } // returnerar detta, och alltid detta. } klass B utökar A { // Ingen åsidosättning behövs! def doThat (): detta . typ = { ... } } ... val a : A = nytt B (). gör det (). gör detta (); // Kedjning fungerar i båda riktningarna. val b : B = nytt B (). gör detta (). gör det (); // Och båda metodkedjorna resulterar i ett B!
Se även
externa länkar
- Martin Fowlers ursprungliga bliki-inlägg som myntar termen
- Ett Delphi-exempel på att skriva XML med ett flytande gränssnitt
- Ett flytande .NET-valideringsbibliotek skrivet i C#
- En handledning för att skapa formella Java-API:er från en BNF-notation
- Flytande gränssnitt är onda
- Att utveckla ett flytande api är så coolt