tifyty

pure Java, what else ?

Folyós api

Van egy library, amiben vannak osztályok, és vannak metódusaik. Ezeket használjuk. Például van egy olyan metódus, amelyik egy stringről leveszi a nyitó és záró idézőjeleket (mert mondjuk a Java annotációs api így adja vissza a String annotáció értékét, hogy az elején és a végén ott van egy ". Akkor lehet valami ilyen:

final String bareType = StringTool.unquote(quoted);

Aztán arra is szükségünk lehet, mert például annotációkat kezelgetünk, meg java forrást generálunk, hogy ha a string (most már idézőjelek nélkül) egy Java osztály neve, és speciel a java.lang csomagban van, akkor le is szedhetnénk a string elejéről a java.lang. prefixet. Erre is van egy metódus. Most már így néz ki a kódunk:

final String unquoted = StringTool.unquote(quoted);
final String bareType = StringTool.unJavaLangized(unquoted);

Ja, csak éppen elfelejtettük, hogy a stringben pontok helyett a csomagnevek között - jelek vannak. Ezt is át kell konvertálni mielőtt levesszük a prefixet, de szerencsére erre is van egy metódus. Nosza editáljuk meg az előző példát:

final String unquoted = StringTool.unquote(quoted);
final String dotted = StringTool.dashDotConvert(unquoted);
final String bareType = StringTool.unJavaLangized(unquoted);

És kész is. Persze nem működik, és nem is szép. Mennyivel szebb lenne valahogy így:

final Strign bareType = FromThe.string(quoted).unquote()
                             .dashDotConvert().unJavaLangized().get();

És nem csak olvashatóbb, de működik is. Ez már félig fluent API, de még nem teljesen az. Ezt ne felejtsük el, és erre még visszatérek később. Az előző példa viszont azért nem működik, mert el lett gépelve: és milyen könnyű elgépelni, és milyen könnyű utána ezt nem észrevenni.

Most nézzük meg, hogy hogyan kell egy ilyen API-t megvalósítani.

A FromThe osztályban van egy static metódus, ami létrehoz egy StringTool objektumot. Ez az osztály a konstruktorában fogad egy String-et, amit jól eltesz egy final StringBuilder field-be. Valahogy így:

class FromThe {
  StringTool string(String s){
    return new StringTool(s);
    }
  }

class StringTool {
  final StringBuilder s;
  StringTool(String s){
    this.s = new StringBuilder(s);
    }
  }

Utána a unquote, dashDotConvert és unJavaLangized metódusok a belső mezőn dolgoznak, és a this értékét adják vissza, majd a get() metódus visszaadja a string építőben összerakott karaktereket String-ként.

De mi van akkor, ha rosszul használjuk az API-t, és

final Strign bareType = FromThe.string(quoted).unquote()
                             .unJavaLangized().get();

formában használjuk? Ha előbb nem, futás során kiderül. De miért nem derül ki előbb? Miért ne lehetne, hogy készítsünk a StringTool osztály mellé egy Unquoted, DashDotConverted interfészt, amik deklarálják a dashDotConvert és unJavaLangized metódusokat? A metódusok pedig továbbra is a this értékét adják vissza, de metódusok visszatérési típusa nem StringTool hanem valamelyik interfész, amelyek mindegyikét természetesen implementálja a StringTool osztály.

class FromThe {
  Quoted string(String s){
    return (Quoted)new StringTool(s);
    }
  }

interface Quoted { Unquoted unquote(); }
interface Unquoted { DashDotConverted dashDotConvert(); }
interface DashDotConverted { DashDotConverted unJavaLangized(); String get();}

class StringTool implements Quoted, Unquoted, DashDotConverted {
  final StringBuilder s;
  StringTool(String s){
    this.s = new StringBuilder(s);
    }

  // method implementations

  }

Ekkor már a fenti hiba fordítási időben kiderül a

    FromThe.string(quoted).unquote()

típusa Unquoted, az az interfész pedig nem definiálja az unJavaLangized() metódust, az IDE már húzza is alá pirosan. Ez már alakul, ezt már hívhatjuk fluent API-nak. (Ha esetleg valakinek nem esett le, hogy a példa meglehetősen egyszerű, és csak mintapélda és esetleg azt gondolná, hogy ez egy overkill, akkor ajánlom figyelmébe a JOOQ (ejtsd dzsók, mint az angol joke szó) API-t, és ha még nem találkozott fluent API-val, akkor próbálja megérteni azon keresztül, és ha nem sikerült, akkor örömmel látom újra itt ennél a cikknél megérteni az alapokat.)

Érdemes észrevenni, hogy a unJavaLangized visszatérési típusa is DashDotConverted. Előfordulhat ugyanis, hogy a nem akarjuk unjavalangizálni a stringet, ezért a get() metódusnak benne kell lennie a DashDotConverted interfészben, és ugyan lehetne egy új interfész ami csak a get() metódust tartalmazza, és ami a unJavaLangized visszatérési típusa lehetne, de felesleges, mert a unJavaLangized metódus idempotens. (Hú, wazze, jó lett volna odafigyelni az iskolában!)

Mi történik akkor, ha szükségünk van a hosszú és a rövid formára is?

final DashDotConverted converted = FromThe.string(quoted).unquote()
                                                     .dashDotConvert();
final String shortClassName = converted.unJavaLangized().get();
final String longClassName  = converted.get();

Röviden: pofáraesés van. Mind a két eseten a rövid verziót kapjuk. Miért? Mert a metódusaink megváltoztatják az egyetlen StringTool objektumunk állapotát. Mit lehet tenni? A junior megoldás szerint először a hosszú nevet kell leírni. (ha ha ha) Na de nem azért tervezünk fluent api-t, hogy utána mindenféle mellékhatásokat kelljen a használónak figyelembe vennie.

Mi lenne, ha inkább minden egyes metódusunk úgy lenne implementálva a StringTool metódusban, hogy a megváltozott String-et nem helyben teszi el, hanem egy új StringTool objektumot hoz létre, és abban lesz a megváltoztatott string, és azt adja vissza? Ezzel ugyan megoldjuk a fenti problémát, de egyre több, rövid életű objektumot generálunk. Ez viszont legyen a JVM meg a java fordító gondja: a Java 7 már fordítási időben kioptimalizálja ami nem szökik ki a metódusból, és nem is a heap-en allokálja: amikor visszatér a metódus az objektum GC nélkül begyűjtődik.

Az élet szép és jó. Egy kicsit ugyan zavaró, hogy sok sablon kódot írunk, főleg az a rész, hogy minden egyes metódusunk létrehoz egy új StringTool példányt. Ez mindenhol tök egyforma, és így minden metódus két dolgot csinál: ami a funkciója, plusz még “klónozza” az aktuális objektumot. De nincs “demanding need”, hogy változtassunk, és lustán megelégszünk ezzel az eljárással, és programozásai pattern-nel.

Aztán jön az újabb ügyféligény, hogy az kellene, hogy a pontok helyett dash karaktereket tartalmazó csomag+osztály nevet is meg kellene tudnunk szabadítani a java-lang prefix-től. Megoldás?

Írjuk át a unJavaLangized metódust. És ebben a pillanatban történik a tragédia. És akkor egy kis kitérő:

Amikor API-t tervezünk figyelembe kell vennünk, hogy hogyan lehet az API-t használni, hogy hatékonyan lehessen programozni. Ez azonban másodlagos. Elsősorban azt kell jó alaposan meggondolnunk, hogy hogyan fogják az api-t rosszul használni, abuzálni. Elferdítve Murphy törvényét: amit el lehet, azt el fogják.

A tragédia az, hogy tisztelt API használó, aki maga is programozó, lustaságból kihasználta, hogy unJavaLangized idempotens. Volt olyan metódusa, amelyik már rövidített nevet kapott néha, máskor meg nem, és az egyszerűség kedvéért meghívta rá a unJavaLangized metódust. Most viszont ez a metódus megszűnt idempotens lenni, például a java.lang.java-lang-java.lang. stringet három unJavaLangized() hívás eltünteti. Ez ugyan csak ritkán okoz problémát, de aki egy ideje programoz az pontosan tudja, hogy azt a bugot könnyű elkapni ami gyakran okoz problémát, és azok, amelyek csak a hold a nap és a csillagok együttállásakor jelennek meg akasztott ember árnyékában örök életűek. Ezért a “csak ritkán okoz problémát” nem érv, főleg amikor a lélegeztető gép szoftverhibája a te gyereked műtétje közben gondol úgy, hogy akcióba kell lépnie. (Jobb, ha megnézed mielőtt a pacemakert beültetik, hogy nincs-e rajta Made in China felirat. Én szerencsére csak egy svájci hátizsákkal jártam így. Minőség! Hahh!)

Jobb, ha inkább csinálunk egy dashUnJavaLangized metódust, és mindegyik csinálja a saját dolgát. Viszont az ügyfél pampog, hogy ez így neki nem jó, az API maga ne változzon. Na jó, akkor legyen a unJavaLangized univerzális, és csináljuk meg, hogy úgy legyen idempotens, hogy megjegyezzük, hogy már egyszer végre lett hajtva. Gányolgatunk? Ja. Valójában a string mellé, ami az egyetlen változó volt, ami az api végrehajtás állapotát reprezentálta, most felvettünk még egy változót (feltehetően boolean-t), és minden egyes metódusban gondoskodnunk kell, hogy ez is megfelelően legyen kezelve, “klónozva”, másolva.

Ha eddig nem vált világossá, akkor tegyük tisztába: kétféle állapotunk van ebben a modellben. Az egyik az aktuális kalkuláció állapota. Ez elvileg végtelen, illetve csak azért véges, mert a gépek mérete véges. A másik a hívási sorrend állapota. Ez véges. Ha az API-t egy véges automatával modellezzük, és minden egyes metódushívást egy átmenet, és minden egyes interface, ami a visszatérési típus akkor ez a modell stimmel is. Amikor bevezettük (volna) az extra boolean változót, akkor a kalkuláció állapotában tartottuk volna nyilván a hívási sorrend állapotát. Ez nem jó, ez nem az amit a tigrisek szeretnek.

Eddig interfészeket használtunk az állapotok reprezentálására, és nagyon sokat segít abban, hogy az IDE csak azokat a metódusokat kínálja fel kódkiegészítésnél, amik az adott hívási sorrendben hívhatók. Használhatnánk osztályokat is. Miért nem szoktuk? Azért, mert sokat kell gépelni. Az interfészben csak fel kell sorolni a metódusokat, az osztályokat implementálni kell: legalábbis minden egyes metódusnak meg kell hívni a központi (példánkban StringTool) megfelelő metódusát.

Mi lenne az előny? Az első, hogy lehetőségünk lesz arra, hogy egy metódus visszatérési típusa más legyen, attól függően, hogy milyen állapotból hívtuk. Amíg csak interfészeket használtunk az állapotok reprezentálására, addig a unJavaLangized minden esetben a DashDotConverted állapotba vitte át az automatát. Ez itt nem gond, de általánosságban, egy összetettebb API esetében nagy előny, ha lehetőségünk van egy metódust több, különböző állapot között használni.

A másik nagy előny, hogy a különböző állapotokat reprezentáló osztályokban az egyes metódus átmenetek különbözőképpen lehetnek implementálva. Konkrétan, a mi esetünkben: az egyik osztályban a unJavaLangized() az eredeti, azonos nevű metódust hívja meg, míg a másikban meghívhatjuk a dashUnJavaLangized metódust. Nem kell bevezetnünk új változót, csak a hívási sorrend mögötti logika lesz összetettebb. De ez nem baj, amíg karbantartható, és amíg nem az a fő feladatunk, hogy template kódot kell írnunk az időnk 90%-ban. Pedig erre ezzel a megközelítéssel lenne esély, de a programozó legnagyobb erénye a lustaság: inkább ír programot, ami kódot ír, mintsem, hogy kódot írjon. Más szavakkal: programozni szeretünk, nem kódolni.

Kulcsszavak a továbbolvasáshoz, és a fluent API-t támogató annotációs processzorhoz: GitHub, verhas, fluflu.

Ezen kívül érdemes elolvasni Lukas blogbejegyzését is.

One response to “Folyós api

  1. tamasrev július 27, 2013 10:17 du.

    Kicsit nézni kellett a nem működős példát, hogy miért is nem (copypaste bug: undotted)
    A szomorúbb felismerés az, hogy nehezebb jó folyós apit írni, mint ahogy azt eddig gondoltam.

Vélemény, hozzászólás?

Adatok megadása vagy bejelentkezés valamelyik ikonnal:

WordPress.com Logo

Hozzászólhat a WordPress.com felhasználói fiók használatával. Kilépés / Módosítás )

Twitter kép

Hozzászólhat a Twitter felhasználói fiók használatával. Kilépés / Módosítás )

Facebook kép

Hozzászólhat a Facebook felhasználói fiók használatával. Kilépés / Módosítás )

Google+ kép

Hozzászólhat a Google+ felhasználói fiók használatával. Kilépés / Módosítás )

Kapcsolódás: %s

%d blogger ezt kedveli: