Tekijät: Marko Haanranta, Kasper Kivikataja, Kati Kyllönen, Jussi Miestamo
Pääsivu
2. viikko
3. viikko
4. viikko
Aluksi pohdimme hieman ohjelmointityyliä yleensä. Mitä on hyvä ohjelmointityyli ja kenen kannalta jonkinlainen ohjelmointityyli on hyvää, entä missä tilanteissa? Keskeisenä tavoitteena ohjelmointityyliä ajatellen voidaan varmaankin pitää jonkinlaista tasapainoa koodin tehokkuuden ja luettavuuden välillä. Tehokkuus on itsestään selvä ja välttämätön tavoite, ja siihen pitää pyrkiä, muttei kuitenkaan ymmärrettävyyden kustannuksella. Koodin luettavuus on välttämätön tavoite sekä koodaajan itsensä että muiden sen kanssa tekemisiin joutuvien takia. Koodaajien on pystyttävä seuraamaan ohjelman logiikkaa helposti, jotta säilyisi ymmärrys siitä, mitä se tekee. Mikä sitten on luettavaa koodia? Jokaisella on varmasti mieltymyksensä, mutta joitain valitusta ohjelmointikielestä riippumattomia nyrkkisääntöjä on kuten pyrkimys pitää funktiot (tai metodit) lyhyinä ja yhtenäinen käytäntö luokkien, objektien, primitiivityyppien nimennässä. Tähän mennessä mainitut hyvät käytännöt liittyvät lähinnä koodin ulkoasuun, mutta ulkoasu ja tyyli eivät ole sama asia, vaan ohjelmointityyli liittyy terminä olennaisesti myös - kurssin nimen mukaisesti - ohjelmointitekniikkaan eli siihen, miten asioita oikeasti toteutetaan, ja tähän pääsemme seuraavaksi.
Javascriptille luonteenomaista on jättää paljon ohjelmoijan vastuulle. Yhteen tilanteeseen sopiva ohjelmointityyli ei välttämättä sovi toiseen, mutta Javascript antaa ohjelmoijalle paljon valinnanvaraa. Toisella viikolla tuli todetuksi, että käyttäjän syötettä tarkistettaessa pitää olla tarkkana sen suhteen, mitä voidaan sallia. Jos käyttäjältä pyydetään syötteenä jotakin numeerista, ei ohjelma välttämättä kaadu, jos syötteen seassa on kirjaimia. Kuitenkin pitää ottaa huomioon, mihin syötettä on tarkoitus käyttää: Java ilmoittaa oma-aloitteisesti poikkeuksella, jos syöte ei ole sitä, mitä metodille pitäisi parametreina syöttää. Javascript-ohjelmoijan sen sijaan pitää itse pitää huoli siitä, että käyttäjän syöte ei etene vaikkapa seuraavan funktion kutsuun parametrina ja aiheuta jotakin odottamatonta, vaikkapa ohjelman kaatumista. Javascriptin tyypittömyys voi näin epäsuorasti aiheuttaa sen, että koodista tulee vaikeammin debugattavaa: Java-kääntäjä ilmoittaa virheellisestä syötteestä saman tien kuvatun kaltaisessa esimerkkitapauksessa. Javascriptiä ajettaessa sen sijaan virheellinen syöte saattaa edetä hyvinkin pitkälle suoritusaikana ennen kuin virhe ilmenee, ja tällöin sen löytäminen on vaikeampaa. Web-sovelluksen käyttäjälle ohjelman kaatuminen voi aiheuttaa sen, että ruudulla näkyy toivotun sisällön sijasta pelkkä valkoinen selainikkuna. Käytti sitten kumpaa tahansa (tai mitä tahansa) ohjelmointikieltä, syöte on tarkistettava jotenkin. JS:n tapauksessa "virheellinen" syöte ei siis (välttämättä) kaada ohjelmaa ainakaan heti, mutta ohjelmoijan on pidettävä huolta, ettei se riko myöhemmin esimerkiksi tietokannan toimintaa.
Seuraavan kaltaisessa yksinkertaisessa esimerkkitapauksessa sillä, käyttääkö Javaa tai JS:ää ei ole paljon merkitystä, ehkä JS:llä syöteen validointi on kevyempi toteuttaa. Oletamme, että parametrina annettava String s
tai value
on html-lomakkeesta luettu käyttäjän syöte.
Java:
public static boolean isInteger(String s) {
try {
Integer.parseInt(s);
} catch(NumberFormatException e) {
return false;
}
return true;
}
function isInteger(value) {
return value === Math.floor(value)
}
Kummassakin tapauksessa syöte on siis tarkistettava jotenkin. Varsinaista etua JS:n tyypittömyydestä ei juuri tässä tapauksessa ole, eikä näiden koodin pätkien suorituksessa ero laskentatehon tarpeessakaan liene merkittävä. Kuitenkin, jos metodi- tai funktiokutsuja tulee kymmeniä tuhansia sekunnissa, eroja voi syntyä. (Suorituskyvyn suhteen on Javascriptin eduksi otettava huomioon se, että Javalla validointi on tehtävä palvelinpäässä, jolloin kaikki rasituskin kohdistuu palvelimiin.) Jos käsiteltävän datan määrä on suuri, on eduksi, jos voi välttää turhien poikkeusten heittelyn. Lisäksi JS:llä tässä tilanteessa ei välttämättä edes tarvittaisi tyyppitarkistusta, vaan riittäisi, että testattaisiin vaikkapa säännöllisellä lausekkeella, koostuuko kentästä saatu merkkijono vain numeroista. Näin yksinkertaisessa koodissa valitulla tavalla ei kuitenkaan ole juuri merkitystä. Lisäksi Javascriptillä toteutus on huomattavasti kompaktimpi, mikä helpottaa ohjelmoijan elämää.
Tyypittömyyden hyödyt ja haitat tulevat esille oikeastaan tilanteittain. Dynaaminen tyypitys voi vapauttaa ohjelmoijan tekemään asioita kompaktisti, mutta toisaalta se siirtää vastuun ohjelmoijalle, ja huolimattomuudesta voi seurata kauheita. Seuraava esimerkki on vielä helpohkosti hahmotettavissa, mutta entä jos idea vietäisiin pidemmälle? Mitä jos funktioista koostuva taulukko olisi funktion parametrina - tai olion konstruktorin parametrina? Tällainen koodi voi olla tehokasta, mutta jossain kohtaa tulee kaikille ohjelmoijille vastaan raja, jonka jälkeen ei enää itsekään ymmärrä omaa koodiaan?
--
summa2 = function(a){return a+a}
potenssi2 = function(a){return a*a}
a = [summa2,potenssi2] // Luo funktioista koostuvan taulukon
for (i=0;i<a.length;i++)
{
console.log(a[i](3)) // Tulostaa 6 ja 9
}
Se, että Javascript antaa muuttaa tietorakenteiteita dynaamisesti tekee algoritmien koodaamisesta joustavaa. Esimerkiksi taulukon täyttyessä ei tarvitse kopioida alkioita isompaan taulukkoon. Myös puuttuvan kentän lisääminen tietorakenteeseen käy helposti, mikä tekee algoritmin helposti muunneltavaksi. Yllä olevan esimerkin for-loopin voisi - kuten sanottu -kääriä funktion sisään, ja funktiolle voisi antaa parametrina esimerkin tapaan jonkin muuttujan ja taulukon. Näin funktion tehtävä olisi tehdä "jollekin" muuttujalle "joitakin" asioita sen mukaan, mitä taulukossa olevat funktiot määräävät. On melkeinpä pelottavaa ajatella, kuinka voimakas kieli Javascript on ja toisaalta kuinka moni täysin ohjelmointitaidoton sitä käyttää.
Javascriptistä sanotaan, että se on helppo kieli, mutta dynaamisen tyypityksen tehokas hyödyntäminen vaatii ymmärrystä ja huolellisuutta. Kuten on tullut todetuksi, Javascriptiä käytettäessä vastuu on koodaajalla itsellään. Ei dynaaminen tyypitys itsessään aiheuta ongelmia - niitä syntyy vain, jos koodaaja tekee jotakin väärin. Javan tyyppisten kielten etu tässä on lähinnä se, että kielen asettamat rajat pitävät ohjelmoijan kurissa. Onkohan kuitenkin niin päin, että Javan tyyppiset kielet ovat aloittelijoille parempia? Seuraavassa katsomme esimerkin voimalla funktionaalisen ja imperatiivisen ohjelmoinnin eroa.
Alla olevat esimerkkiohjelmat hakevat taulukosta kaikki objektit joiden arvo on < 10.
Esimerkki 1. Imperatiivinen ohjelmointityyli:
function bestPrices(inputArray) {
var outputArray = [];
for(var i = 0; i < inputArray.length; i++) {
if(inputArray[i].price < 10){
outputArray.push(inputArray[i]);
}
}
return outputArray;
}
function bestPrices(inputArray){
return Array.filter(inputArray, function(value){
return value < 10;
}
}
On vaikeaa määritellä kumpi tyyli on parempi, koska molemmissa on puolensa. Funktionaalinen koodi on usein lyhyempää, tosin raskaampaa ja vaatii aivan toisenlaista ajattelutapaa. Imperatiivisesti kirjoitettu koodi on usein pidempi, mutta hieman tehokkaampi.
Kukin oppii ohjelmoimaan eri tyyleillä, joka tekee "oikean tyylin" valitsemisesta mahdotonta. On kuitenkin hyvä tiedostaa projektin tarpeet ja pitää huoli että koodi pysyy helposti ylläpidettävänä.
Sulkeuma luodaan kirjoittamalla funktio toisen funktion määrittelyn sisään. Tällöin muodostuu näkyvyysalue jossa sisemmällä funktiolla on pääsy ulomman funktion muuttujiin. Eli ulompi funktio sulkee sisäänsä omat muuttujansa ja sisällään määritellyt funktiot. Sulkeumat tallentavat viittauksen ulomman funktion muuttujiin eivät itse muuttujan arvoa. Kun ulomman funktion suoritus päättyy niin sisemmän funktion elämä ei pääty vaan sisempää funktioita voidaan kutsua ja sillä on yhä pääsy ulomman funktion muuttujiin. Sulkeumat ovat osa Javascriptin module pattern -suunnittelumallia.
Alla olevassa esimerkissä muodostetaan kaksi sulkeumaa increment
ja get
joita käyttäen päästään käsiksi count
muuttujaan sijoitettuun arvoon vaikka Counter
funktion suoritus on jo päättynyt.
function Counter(start) {
var count = start;
return {
increment: function() {
count++;
},
get: function() {
return count;
}
}
}
var foo = Counter(4);
foo.increment();
write(foo.get()); // 5
JavaScriptin virhetilanteet voidaan jaotella loogisiin virheisiin, syntaksivirheisiin ja ajonaikaisiin virheisiin. Loogisilla virheillä tarkoitetaan tilanteita, joissa ohjelman looginen toiminta on suunniteltu väärin. Syntaksivirheillä tarkoitetaan JavaScriptin kielen syntaksin vastaisia muotoja, jäsennysvirheitä, jotka JavaScript tulkki havaitsee. Ajonaikaiset virheet tapahtuvat ohjelman suorituksen aikana ja niitä kutsutaan myös nimellä poikkeukset. Tarkastelemme seuraavaksi ajonaikaisia virheitä tarkemmin.
Ajonaikaiset virheet voidaan käsitellä JavaScriptin poikkeuskäsittelyn, eli try
-lauseen avulla. Try
lause koostuu try
, catch
ja finally
lohkoista. Ajonaikaisia virheitä, jotka laukaisevat poikkeuskäsittelyn on EcmaScriptin määrittelyssä [1] mainittu kuusi:
function poikkeusDemo(){
try{
olematonfunktio()
alert('On olemassa...')
} catch(err){
alert('Funktiokutsu meni pieleen: '+err.message)
} finally{
alert('Tämä suoritetaan aina!')
}
}
Kun try
lohkossa olevaa koodia suoritetaan ja tapahtuu ajonaikainen virhe, niin suoritus siirtyy välittömästi catch
lohkoon ja err muuttuja saa arvokseen Error objektin. Error objektin avulla voi hakea virheestä generoidun virheviestin, tai esimerkiksi virheen nimen. Finally
lohkossa oleva koodi suoritetaan aina, tapahtui poikkeuksen heittävä virhe tai ei.
Useita try
lausekkeita voi sijoittaa sisäkkäin. Tämä on tarpeen, jos catch
lohkossa tehdään toimenpiteitä, jotka voivat johtaa uuteen poikkeustilanteeseen. Jos sisemmässä try
lauseessa ei ole catch
lohkoa, niin suoritetaan ulomman try
lausekkeen catch
lohko, jos sellainen on olemassa. Jotkin selaimet, kuten Chrome ja Firefox tosin kieltäytyvät toimimasta ilman sisempää catch
tai finally
lohkoa, vaan generoivat SyntaxError virheen tästä tilanteesta.
function poikkeusDemoSisempiTry(){
try{
olematonfunktio()
alert('On olemassa...')
} catch(err){
alert('Funktiokutsu meni pieleen: '+err.message)
try{
write(virheviesti)
}catch(err){
alert('Yritetään jatkaa: '+err.message)
}
} finally{
alert('Tämä suoritetaan aina!')
}
}
Poikkeuksen voi heittää throw
lauseen avulla. JavaScriptissä voi käyttää joko valmiiksimääriteltyjä poikkeuksia tai generoida virhetilanteeseen sopivan poikkeuksen itse. Itsemääritellyissä poikkeuksissa on tärkeää tarjota mahdollisimman tarkka kuvaus siitä mitä on tapahtunut, jotta tieto ongelmatilanteesta saadaan välitettyä eteenpäin mahdollisimman yksityiskohtaisesti tarvittavia toimenpiteitä varten. Lisäksi on suotavaa määritellä poikkeukset hierarkisesti, jotta ongelman käsittelyn tarkkuutta voidaan säätää ohjelmakoodista käsin kutakin tarvetta vastaavaksi ilman, että usean virhetilanteen kaappaamiseksi tarvitsee listata useita poikkeuksia.
Poikkeuksia pitäisi mielestämme käyttää vain erityisen poikkeuksellisissa tilanteissa. Tälläisiä tilanteita voisivat olla esimerkiksi että ohjelman käyttämä tiedosto on varattu tai korruptoitunut. Tai tilanne jossa ohjelma tarvitsee suorittaakseen jotakin tiettyä resurssia, mutta ei ole onnistunut tätä varaamaan toistuvista yrityksistä huolimatta. Jonkinlaisena ohjenuorana kehoittaisimme pohtimaan voiko ohjelma toipua tilanteesta itse, jos voi, niin silloin ei poikkeusta tarvitse heittää.
Poikkeusten käyttäminen normaaleissa virhetilanteissa, kuten käyttäjän syötteen tarkastamisessa ei mielestämme ole suositeltavaa. Poikkeusten heittäminen ja käsittely on suhteellisen raskas toimenpide, sillä poikkeus aiheuttaa muuttujan luomisen ja tuhoamisen ajonaikaisesti. Kaikki selaimet eivät myöskään toimi poikkeustenkaan suhteen samalla tavalla ja myös virheilmoitukset voivat olla erilaisia. Ohjelman suorituskyvyn kannalta parempi vaihtoehto on käyttäjän syöttäjän tarkastaminen ehtolauseella ja tarvittavan statustiedon näyttäminen käyttäjälle mahdollisia korjaavia toimenpiteitä varten.
Lähteet:
[1] Standard ECMA-262 5.1 Edition / June 2011, http://ecma-international.org/ecma-262/5.1/#sec-12.14
[2] Web-selainohjelmointi / syksy 2012 http://www.cs.helsinki.fi/group/java/s12-weso/
[3] Closures / November 2013 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures
[4] JavaScript-Garden / 2011 http://bonsaiden.github.io/JavaScript-Garden/fi/#function.closures
[5] Imperative vs Functional / http://kriszyp.name/2010/01/16/imperative-functional-and-declarative-programming/