Funkcionalno programiranje u praksi: Jednostavan život podataka vaših web aplikacija

Rastko Vukašinović, JS developer u Vastu, piše zašto sve operacije nad podacima radi funkcionalno i prateći principe nepromenljivosti

Rastko Vukašinović - 27. Jul, 2016.

Da bi se u potpunosti razumeo način života web aplikacije, mora se prvo savladati način na koji se podaci menjaju i teku. Postoji mnogo načina kako da se to uradi, i malo manje, ali i dalje mnogo načina da se to uradi kako treba.

Pravi način je samo pitanje lakoće održavanja i popravljanja problema. Siguran sam da će sve funkcionalnosti vaše aplikacije biti po specifikaciji kada odu na produkciju :)

Poslednjih godina, sve operacije nad podacima sam počeo da radim funkcionalno i prateći principe nepromenljivosti. Sa željom da, u nekom trenutku, u potpunosti pređem na funkcionalno – reaktivni pristup programiranju, ali držimo se teme. Na brzinu ću vas upoznati sa konceptima nepromenljivosti, a onda ćemo zaći u neke od funkcionalnih pristupa manipulaciji podatacima.

Nepromenljivost

Nepromenljivi podaci se, očigledno, ne mogu menjati.

Tako da se, u suštini, nijedan objekat ne menja prilikom kreiranja (suprotno od generalno prihvaćenog načina u objektno orijentisanom programiranju gde uvek menjamo isto stanje objekta).

Potpuna nepromenljivost znači da za svaku promenu, novi objekat biva kreiran, a stari može da služi kao referenca na prethodno stanje. To nam daje potpuno novi skup pravila i mogućnosti, kao što je povratak na prethodno stanje, druge jednostavne manipulacije prethodnim verzijama, praćenje promena i optimizacija toka podataka na prirodan način.

Ako želite da na pravi način primenite ove koncepte bez zalaženja u teoretske detalje, predlažem Facebook-ov immutable.js. Ako se odlučite da koristite immutable.js, provedite neko vreme kako biste ga razumeli, ne želim da ljudi rade samo ono što im se kaže…

Rad sa skupom podataka

Uglavnom od 80 do 90% podataka u web aplikacijama čine skupovi podataka, većinom kolekcije ili višedimenzijske strukture nalik drvetu sa strukturama podataka koje se ponavljaju. Ako ne radite sa ovakvom vrstom podatka u svojim aplikacijama, ovaj članak će vam biti manje koristan i možete jednostavno pogledati prvih nekoliko primera.

Kako počinjemo sa korišćenjem funkcionalnih pristupa, moramo razumeti još jedan važan koncept koji je potrebno pratiti, a usko je povezan sa nepromenljivošću. Ovaj koncept zahteva da svaka funkcija unutar sebe razreši sve za nju vezane zadatke, i na izlazu da čistu transformaciju ulaznih podataka bez sporednih efekata.

Ovo praktično znači da je svaka funkcija bez ikakvog stanja (samo mutator podataka) i da pri svom izvršenju nema nikakav uticaj na okolni kod – na izlazu daje podatke (ili funkciju) koji isključivo zavise od onoga što je dobila na ulazu.

Lako je pričati – pokaži mi kod*

Pokazaću nekoliko primera korišćenja standardnih JavaScript funkcija višeg reda za obavljanje uobičajenih operacija nad podacima.

Uobičajen pristup bi bio korišćenje for, forEach ili underscore/lodash each za prolazak kroz listu i kreiranje novih nizova koji bivaju popunjeni izvan petlje, ili, još gore, menjanje same liste.

Podaci koje ćemo koristiti u većini primera:

/** We got array of results coming our way, here is example of form of results:
[{
    "objectID": 9131042,
    "name": "360fly - Panoramic 360° HD Video Camera - Black",
    "description": "This 360fly panoramic 360° blah, blah blah",
    "brand": "360fly",
    "categories": [
      "Cameras & Camcorders",
      "Camcorders",
      "Action Camcorders",
      "All Action Camcorders"
    ],
    "hierarchicalCategories": {
      "lvl0": "Cameras & Camcorders",
      "lvl1": "Cameras & Camcorders > Camcorders",
      "lvl2": "Cameras & Camcorders > Camcorders > Action Camcorders",
      "lvl3": "Cameras & Camcorders > Camcorders > Action Camcorders > All Action Camcorders"
    },
    "type": "Point&shoot camcrder",
    "price": 399.99,
    "price_range": "200 - 500",
    "image": "http://img.bbystatic.com/BestBuy_US/images/products/9131/9131042_rb.jpg",
    "free_shipping": true,
    "popularity": 10000
  }, ... lot more stuff in same form]
*/

Jednostavna iteracija kroz dobijeni skup podataka pripremajući ih za upotrebu u korisničkom interfejsu.

//Here we consider dataset being array of objects, and we want to keep it an array, just return formatted stuff
var renderData = responseData.results.map((item) => {
     return {
       name: item.name,
       price: formatPrice(item.price),
       image: item.image,
       badges: getBadges(item), //adding badges links based on popularity, price, free shipping etc...
       ...
     }
});

renderData je sada niz parsiranih elemenata. Korišćenje .map() omogućava nam da jednostavno kreiramo postavku za novi niz bez dodatnih sporednih efekata i potrebe da menjamo originalni skup podataka, u potpunosti nas čuvajući od bagova… Jedina greška koju možemo da napravimo je u “kolbek” metodi za parsiranje podataka koju predajemo map metodi

Menjanje strukture podataka iz liste po id-ju u niz i obrnuto i povezani komplikovaniji zadaci

Ovo je uobičajen zadatak, i veoma sklon bagovima ako se radi na uobičajen način – rezervišemo promenljivu za novi niz i onda prolazimo kroz listu ključeva stavljajući ih sve na svoje mesto u nizu, zahtevajući pomoćne skupove podataka — što su sporedni efekti koji mogu primati i druge uticaje (neka druga funkcija može menjati našu novoformiranu promenljivu i tako uticati na proces obrade podataka, što je veoma teško pratiti)…

Uradiću sledeće na osnovu gornjeg primera i kreiraću listu elemenata po id-ju.

var renderData = responseData.results.map((item) => {
         return { [item.objectID]: item };
}).reduce((a, b) => {
   return Object.assign(a,b);
});

Metod prosleđen u .reduce() prima perviousValue i currentValue kao prva dva argumenta, tako da vi birate kako će biti redukovani u jednu vrednost (trenutna vrednost je uvek rezultat svih prethodnih redukcija, tako da vam i to može biti korisno). U ovom slučaju, redukcija znači da se od dve vrednosti, na određeni način dobija jedna, dakle kolbek koji dajemo metodi reduce treba da odredi koja se operacija dešava nad ove dve vrednosti.

Do sada bi trebalo da smo shvatili značajne prednosti ovakvog pristupa – nastavljamo da radimo na podacimo kroz seriju jednostavnih “pipeline-a” koji samo rade svoje jednostavne i lako kontrolisane zadatke (koje je, takođe lako ispratiti)

Pronalaženje predmeta u skupu podataka po sadržaju jedne od njegovih vrednosti

Sada znamo kako se parsiraju podaci, na koji način iteriramo i kako ih vracamo, da li možemo to da iskoristimo da filtriramo i pronađemo stvari…

Pogledajmo prvo filtriranje – prvo želimo da proberemo elemente, a onda da ih pripremimo za UI:

var hdRenderData = responseData.results.filter((item)=>{
     return item.name.includes('HD');
  }).map((item) => {
     return {
       name: item.name,
       price: formatPrice(item.price),
       image: item.image,
       badges: getBadges(item), //adding badges links based on popularity, price, free shipping etc...
       ...
     }
});

Ovde koristimo .filter(), metodu koja vraća niz predmeta za koje je callback funkcija vratila true. Filter je poslednju funkciju višeg reda koju želim da iskoristim ovde.

Ali, evo i ostalih korisnih saveta i primera:

Lako filtriranje po kategoriji i vraćanje samo potrebnih podatka:

var drones = responseData.results.filter((i) => {
   return i.categories.reduce((a, b) => (a + b)).includes(“Drone”);
}).map((item) => item.objectID);

U gornjem primeru filtriramo predmete, uprošćavajući niz kategorija u string (nadovezivanjem niza stringova u jedan string) i uzimajući samo predmete čije kategorije sadrže reč “Drone” u sebi (koristeći .includes() na novoformiranom stringu). Zatim mapiramo niz koji vraćamo tako da vraća samo ID-jeve pronađenih komponenti.

Rukovanje byId listama (bilo koja lista sa ponavljajućim strukturama, ali bez ključeva baziranih na inkrementalnim brojevima) Prvo moramo da napravimo niz koji ćemo mapirati:

var keyArr = Object.keys(yourDict);

Kada imamo strukturu kroz koju možemo da iteriramo, možemo da se bacimo u akciju

//If you want it mapped as new array structure, with key present as an ID
var result = Object.keys(yourDict).map((key) => {
        var entity = yourDict[key];
        entity.id = key;
        return entity;
});

Ako želiš da zadržiš [ključ]:[vrednost] strukturu, možeš koristiti map->reduce patern za pakovanje jednog objekta prikazan iznad.

Čuvanje istorije promene podataka

map(), reduce() i filter() ne menjaju nizove na kojima ih koristite, već vraćaju nove podatke, tako da u zavisnosti od toga šta želimo da postignemo, možemo pratiti i čuvati istoriju promene podataka jednostavno ubacivanjem u niz istorije, ili pratiti još kompleksnije dimenzije promena… Ovo dolazi u paketu sa nepromenljivošću.

Sve se svodi na vaše iskustvo u razvoju

Na kraju, rešenje će biti mereno skalabilnošću, fleksibilnošću i održivošću… Svi spolja će samo brinuti da li radi.

Primeri i diskusije iznad bi trebali da vam pruže drugu opciju.

Po mom mišljenju, ovaj pristup promeni podataka daje jasniji, jednostavniji i kod otporniji na bagove. Nema spoljnih efekata, ne možeš kreirati bagove izvan metoda koje prosleđuješ funkcijama višeg reda. Pored jednostavnosti, ovo daje opisnost tvojoj obradi podataka, čineći kod čitljivijim i razumljivijim.

Na projektima na kojima radim, trudim se da koristim funkcionalno programiranje što je više moguće, sa konačnim ciljem da ga proširim na potpuni FRP pristup u backendu i na klijentskoj strani. Ovi jednostavni pristupi učinili su čuda na novim front end (javascript full stack) aplikacijama na kojima moj tim i ja radimo u Vast-u.

*Talk is cheap, show me the code – L. Torvalds, tvorac Linuxa

Tekst u originalu možete pročitati ovde.