Ovih meseci se stvarno mngo moglo čuti i pročitati na temu DSLa, odnosno Domain Specific Languages. Ako se eto nekako niste još uvek sreli s tim pojmom, evo kratko objašnjenje. Java, C# ili Groovy spadaju u jezike opšte namene, dakle u njima je moguće rešiti proizvoljan problem. Bez obzira da li pišete softver za knjigovodstvo, web aplikaciju ili nešto treće bilo koji od ovih jezika je sposoban da reši zadati problem.
S druge strane SQL je najpoznatiji primer domenski specifičnog jezika. U SQLu teško da ćete napisati web aplikaciju ali zato je za manipulaciju velikom količinom podataka nenadmašan. Dakle, domenski specifični jezici su u najkraćem jezici dizajnirani da reše usku klasu (domen) problema.
Ako mene pitate, oko DSLa se digala prevelika prašina pa često možete čuti ljude kako pričaju o tome da je DSL idealan za to da korisnici sami u nekoj meri proširuju funkcionalnosti aplikacija. Iako sam jednom imao pozitivna iskustva u implementaciji neke vrste DSLa, ipak mislim da je DSL usmeren prema korisnicima uglavnom „pipe dream“. Uostalom i SQL je bio zamišljen kao jezik koji bi trebalo da omogući običnim korisnicima da na običnom engleskom jeziku sami vrše upite i obradu podataka. Mase šalterskih službenika potkovanih SQLom koje viđamo na svakom koraku potvrđuju moju prethodnu tvrdnju.
Ipak ne možemo reći da je SQL bio promašaj. Teško da možete zamisliti neku netrvijalnu aplikaciju koja radi s bazom, a da se SQL ne koristi u manjoj ili većoj meri. Isto tako svaki drugi DSL može nam pomoći da uobičajene zadatke pojednostavimo, odnosno da minimizujemo količinu koda potrebnu za njihovo rešavanje. Pri tom je svakako potrebno da rezultujući kod bude čitljiviji, odnosno lakši za kasnije održavanje.
Princip pojednostavljivanja svejedno upotrebljavamo u pisanju bilo koje aplikacije tako što pišemo sopstvene APIje, odnosno skup klasa i metoda koji automatizuju određene poslove. U tom smislu DSL možemo posmatrati kao dodatni korak, gde menjamo sintaksu samog jezika tako da kod bude još razumljiviji i jednostavniji.
Gde se tu uklapa Groovy? Groovy je za razliku od Jave dinamički jezik tako da na raspolaganju imamo veći broj „trikova“ kojima možemo napraviti privid da je sintaksa jezika promenjena. Ovaj tekst pokazuje nekoliko najjednostavnijih tehnika kako to da postignemo.
Celu ovu priču najlakše je pokazati kroz primer. Sledeći Java kod radi nekoliko jednostavnih operacija. Postavite se u situaciju da treba da izmenite ovaj kod koji je neko drugi pisao, i probajte da pogodite šta je tačno namera ovog programa:
List<Book> shelf = new ArrayList<Book>();
|
Program iz primera dakle sadrži nekoliko knjiga, koje su opisane nekim atributima. Program filtrira podatke po nekom kriterijumu i ispisuje rezultat. Pretpostavimo da knjiga ima atribut „količina“. Pogledajte ponovo primer, ali sada sa idejom da promenite količinu knjiga „Battlefield earth“. Iz datog koda nije očigledno šta bi se trebalo promeniti, što znači da održavanje zahteva da više poznajete strukturu programa. A program ima samo dve klase, a zamislite tek da ih ima 200 ili 2000! Ili na primer pogledajte ovu liniju koda:
shelf.add(new Book("Battlefield Earth", 200, 20 * 94.43));
|
Izraz 20 * 94.43 zaista predstavlja izazov za razumevanje, pogotovo ako ovaj kod čitate u trenutku kad se kurs evra značajno promenio. Ili ovaj deo programa:
for (Book book : shelf) {
|
Traženje po nekom kriterijumu je zaista nerazumljivo i u najmanju ruku zamorno pisati. Svakako je da postoje načini da se ovaj program napiše bolje, da se nekako definiše API koji radi sa knjigama na razumljiviji način. Ono što ćemo pokušati u ovom tekstu je da napravimo Groovy verziju programa, sa ciljem da na kraju izgleda otprilike ovako:
shelf = []
|
Groovy ima interesantnu mogućnost da se parametri u pozivu metoda označe imenom. Ovo doduše važi samo za metode koji kao argument prihvataju mapu, ili u slučaju da konstruišemo bean. Uprkos tome ovo sitno sintaksno poboljšanje mnogo utiče na čitljivost koda. Ako pogledamo kod iz našeg primera:
//Najobičnija klasa, bez posebno definisanog konstruktora
|
Odjednom održavanje programa postaje lakše. Sada prethodni zahtev da se promeni količina određene knjige postaje trivijalan jer nam je iz samog konstruktora jasno koji broj šta predstavlja. Koliko ova sitnica pomaže u oslobađanju programa od bagova pokazuje i sledeći primer:
//Geokoordinate
|
Groovy stoprocentno podržava Java sintaksu. To uključuje i naredbe, kao i konvenciju za pozivanje metoda. Na primer, poziv metode koji će raditi i u Groovyju i u Javi:
shelf.add(someBook);
|
Groovy nudi i poboljšanja te sintakse – pre svega tačka-zarez na kraju izraza nije neophodna. Zatim, ukoliko se metodi prosleđuju parametri zagrade nisu neophodne. I na kraju, ako je poslednji parametar closure dozvoljeno ga je staviti van zagrada.
Iako na prvi pogled deluju nevažno, ta poboljšanja omogućavaju da Groovy imitira nove sintaksne konstrukcije dajući time privid da je jezik proširiv novim naredbama. U našem primeru sintaksa je upotrebljena na sledećim mestima:
shelf.add book (title: "Orlovi rano lete", price: 200, quantity: 100)
|
Poziv shelf.add je jednostavno poziv metode List klase, ali samo uklanjanje zagrada i tačke-zareza na kraju je doprinelo da kod izgleda čitljivije. Uz korišćenje imenovanih parametara se dobija konstrukcija koja bi u nekoj meri bila razumljiva čak i neprogramerima.
Zanimljivija je konstrukcija withBooks {...}. To je takođe najobičniji poziv metoda koji kao parametar prihvata closure. Prividno taj poziv metoda nije moguće razlikovati od if, for ili while naredbe. Sad naš program već liči na pravi DSL jer smo dodali jezičku konstrukciju koja ima smisla samo za domen koji rešavamo.
Evo i jedan primer kako se mogu napraviti opštije konstrukcije, na primer za upravljanje greškama. U mnogim slučajevima kada se pojavi izuzetak programer i nema mnogo izbora nego da grešku zapiše u log i nastavi sa izvršavanjem programa. Beskonačni i često besmisleni try-catch blokovi se ako ništa mogu napraviti da izgledaju kraće i elegantnije:
silent {
|
Dinamički jezik kao Groovy ima tu super osobinu da se tipovi razrešavaju prilikom rada programa, a ne tokom kompajliranja. Da nije toga, cela stvar jednostavno ne bi radila. Pošto se tipovi podataka (klase) razrešavaju tokom rada programa, tako se i pozivi metoda razrešavaju tokom rada programa. Recimo kad napišete:
def s = "groovy"
|
Sve dok se program ne počne izvršavati nije izvesno kako će metod toUpperCase() biti pozvan. Kad je situacija već takva zašto ne bismo mogli napisati i ovako nešto:
//Sa navodnicima
|
Pozivi metoda iz gornjeg primera su savršeno validni u Groovyju, dakle kod će proizvesti očekivani rezultat. Dinamičko pozivanje metoda je osnova za mnoga poboljšanja jezika u odnosu na Javu, od kojih je par najjednostavnijih opisano u nastavku. Inače, ako vas zanima detaljniji algoritam kako se izvršava poziv metoda u Groovyju, ovde je prikazan dobar pregled: http://www.agiledeveloper.com/blog/content/binary/groovymethodhandling.jpg
Svakako jedna od najmoćnijih osobina Groovyja proizilazi iz činjenice da je svaki objekat moguće proširivati po potrebi, bez obzira da li imate pristup izvornom kodu i da li je klasu moguće naslediti. Svaki objekat u Groovyju ima osobinu metaClass pomoću koje je moguće dodavati metode, konstruktore, statičke metode i osobine uz pomoć elegantne sintakse.
Ako ponovo pogledamo Java verziju našeg primera:
shelf.add(new Book("Battlefield Earth", 200, 20 * 94.43));
|
Izraz 20 * 94.43 je u ovom slučaju potpuno van konteksta, odnosno nemamo ni najblažu predstavu o nameri programera koji je kod pisao (a koji se jednostavno „snašao“ pa zakucao kurs evra magičnom konstantom). Naravno, savestan programer bi napravio novu klasu za konverziju, pa bi izraz glasio nekako ovako:
ExchangeRate.convert(20, 94.43);
|
U redu, sad već znamo nameru programera, ali magična konstanta 94.43 i dalje postoji. Ok, rešićemo to uvođenjem još jedne wrapper klase koja jednostavno daje kontekst konstanti:
ExchangeRate.convert(20, new RateEur(94.43));
|
Super, sada izraz ima solidno definisano značenje, ali smo usput naš program povećali za dve klase a i smanjili čitljivost samog izraza. Zar ne bi bilo zgodnije da je moguće napisati nešto poput:
20.eur(rate:94.43)
|
Uz pomoć metaClass je iznenađujuće jednostavno postići taj efekat. Dovoljno je klasu Number proširiti dodatnim metodom:
Number.metaClass.eur = { map -> delegate * map.rate }
|
Šta smo ovde uradili? Klasa Number je proširena metodom „eur“. Eur je definisan kao closure koji ima jedan parametar - map. Parametar map je tipa Map (Groovy je dinamički tipiziran, pa tip ne moramo eksplicitno navoditi). Delegate je objekat nad kojim se metod poziva. Da bi bilo jasnije poziv može da se napiše u Java stilu:
//Groovy
|
Ako pogledamo primer:
withBooks {
|
Jasno je da je withBooks u stvari metod koji prima jedan argument tipa closure. Ali odakle dolazi promenjiva „title“? Ta promenjiva u stvari ne postoji i kad god se izvrši linija koda „println title“ pojavi se exception tipa MissingPropertyException. Ono što mi u stvari radimo je to da presretnemo tu grešku i vratimo vrednost po našoj želji. Konkretno, metod withBooks je implementiran kao:
def currentBook = null
|
U svakoj iteraciji kroz listu knjiga mi na neki način definišemo kontekst tako što postavljamo vrednost promenjivoj currentBook. Svaki put kada se pojavi MissingPropertyException, ukoliko u klasi postoji metod propertyMissing, Groovy ga automatski poziva. Mi smo naš propertyMissing definisali kao:
def propertyMissing(String propertyName) {
|
Kada napišemo u programu „title“, greška će se pojaviti, Groovy će pozvati propertyMissing i dinamički trenutnoj knjizi u iteraciji (currentBook) zatražiti property sa imenom „title“, i napokon vratiti nazad rezultat.
Na kraju kad pogledamo sve ove tehnike jasno je da jednostavno možemo bitno poboljšati sam jezik dodavanjem novih konstrukcija ili pametnijim korišćenjem sintakse. Krajnji rezultat je svakako kod koji bolje opisuje domen koji rešavamo. S druge strane treba biti jako oprezan, pogotovo prilikom korišćenja Expando mogućnosti – loš ili neoprezan programer je sada dobio alat da napravi ubitačno nerazumljiv kod, san svakog programera kojem je job security primarni motiv :)