Staattiset datatyypit vs. proprietaariset DOM-mallit
Aiemman blogikirjoituksen kommenteissa on käyty kiihkeää keskustelua, joka on kilpistynyt osittain siihen, onko staattisesta tyypityksestä haittaa koodaamisessa vai ei. Apo argumentoi eräänä vaihtoehtona, että tekemällä oman DOM-APIa muistuttavan rajapinnan voidaan ohittaa tiettyjä staattisen tyypityksen ongelmia.
Vedän tähän kuitenkin yhteen syitä, miksi ohjelmointikielen natiivit, dynaamiset datatyypit ovat parempia kuin omatekoinen DOM-malli. Käytän esimerkkinä alkuperäistä caseani eli tekstimuotoisten JSON-objektien muuntamista ohjelmallisesti käsiteltävään muotoon ja takaisin tekstimuotoon. Samalla kuitenkin tarkastelen ongelmaa yleisessä tapauksessa, eli muutenkin kuin pelkkien JSON-objektien käsittelyssä.
1. Yhtenäisyyden puute
Oman rajapinnan tekeminen JSON-objektien käsittelyyn tarkoittaa, että jokaisen ohjelmakoodiin koskevan pitää opiskella kyseinen proprietaarinen rajapinta. Rajapinta täytyy myös erikseen lisätä jokaiseen projektiin, joka käsittelee JSON-objekteja.
Hyvässä tapauksessa ohjelmointikielelle löytyy valmis kirjasto yksittäiseen asiaan (kuten JSONin käsittelyyn), mutta sekään ei ratkaise ongelmaa yleisessä tapauksessa. Jokainen case vaatii oman kirjastonsa ja oman APInsa, joilla staattisen tyypityksen rajoituksia kierretään tapauskohtaisesti.
Esimerkiksi Javassa JSON-objekteja käsitellään yhdellä tavalla, JDBC-datarivejä toisella tavalla, ja niin edelleen. Dynaamisesti tyypitetyissä kielissä näitä eri API-rajapintoja ei tarvitse keksiä jokaiseen tarkoitukseen uudelleen, vaan kieli itsessään taipuu minkä tahansa datan mallintamiseen ja tarjoaa sen "yhden oikean tavan". Silloin ohjelmoinnissa tarvitaan vähemmän luokkia, vähemmän koodia ja vähemmän opiskelua.
2. Käytön helppous ja luontevuus
Vahvasti JSON-rakenteisiin nojaavan sovelluksen pitää voida käsitellä dataobjekteja läpinäkyvästi. Ohjelmoijaa ei pidä vaivata sillä, onko dataobjekti peräisin ajaxilla lähetetystä JSON-stringistä vai onko se luotu paikallisesti ohjelmointikielen omalla syntaksilla. Muunnokset tehdään verkkorajapinnoissa, kun tekstimuotoista dataa luetaan tai lähetetään, mutta muuten dataobjektien pitää toimia ohjelmointikielelle luonnollisella tavalla.
Anti-esimerkiksi sopii JSONObject. Se enkapsuloi JSON-objektin sisältämät kentät siten, että ohjelmoijan pitää joka kerta kutsua getDouble()-, getInt()-, getString()-, getJSONArray()-, getJSONObject()- jne. metodeja datatyypistä riippuen. Käytännössä siis muuttujat joudutaan joka kerta typecastaamaan. Tämä ei ole yhtä läpinäkyvää ja luontevaa, kuin tavallisten muuttujien käyttäminen.
3. Suorituskyky
Jos JSON-objekteja käsitellään DOM-tyyppisillä enkapsuloivilla rajapinnoilla, täytyy miettiä myös suorituskykyä. Mikäli oletetaan, että lähes kaikki sovelluksen käsittelemä data muodostuu dataobjekteista, niin käsittelyrajapinnan suorituskyky on suorastaan kriittistä. Esimerkiksi Apon ehdottaman XPath-tyyppisen merkkijonon parsiminen ja evaluoiminen jokaisen property-viittauksen kohdalla ei voi mitenkään vastata natiivin dictionary/HashMap-datatyypin suorituskykyä. Olen melko varma, että mikä tahansa suorituskykyinen rajapinta päätyy natiivien container-luokkien käyttöön ja typecastaukseen.
PS. Suuret kiitokset aiemmille kommentoijille pitkästä ja mielenkiintoisesta keskustelusta. Välillä väittely käy kuumana, mutta varmaan lopputuloksena kukin osapuoli saa hiukan ajattelemisen aihetta omaan näkökulmaansa. Ja eihän kukaan voisikaan olla oikea softa-arkkitehti, ellei uskoisi omien näkemyksien ja ratkaisujen olevan juuri niitä oikeita. :-)
11 Comments
http://dev.gueck.com/code-examples/wiki/KennuIsWronger
Mikael, mielestäni esimerkkisi ei vastannut kirjoitukseni kohdan 2. vaatimuksiin.
Tuon voi tulkita kahdella tavalla:
"Minusta Java on ikävän näköistä"
"Minusta o1['authors'][0] on kätevämpi kuin ['authors'][0]"
Osuuko jompi kumpi oikeaan?
Eikun tarkoitin, että jos joka kerta, kun haluan sanoa koodissani article['authors'][0] tai vaikka blog['user'][1], minun pitäisi ensin rekisteröidä kyseinen lauseke ja sitten kutsua sitä näin:
final Expression expression = new SpelExpressionParser().parseExpression("['authors'][0]"); final String author = expression.getValue(m1, String.class);
Niin tuo käy pitemmän päälle aika työlääksi tavaksi ohjelmoida...
Casenahan (edellistä ketjua lukemattomille) tässä oli se, että teillä on tosi dynaaminen tiimi, joka rakentaa seuraavaa Facebookkia, ja siksi ette voi käyttää faktojen säilyttämiseen validoivassa skeemassa relaatiokantaa, koska MySQL on ainoa cool relaatiokanta, ja MySQL lukitsee taulun ALTER TABLEn ajaksi, ja se on ongelma koska ette syystä tai toisesta voi partitioida tietojanne useaampaan kantaan, vaan teillä olisi kymmeniä teroja tietoa yhdessä MySQL-kannassa, ja ALTER TABLE kestäisi tunteja yhdellä isolla tietokantakoneella (tai sekunteja jos tietoa voisi partitioida usean relaatiokantainstanssin välillä, mutta sitähän emme voi tehdä.)
Koska casenne oli tälläinen, tarvitsette EHDOTTOMASTI tietokannan, joka ei millään tavalla validoi kenttien nimiä ja sisältöjä, puhumattakaan siitä että se tukisi skeemojen muutosta pyörivässä kannassa.
Ja koska selvästi ainoa tapa ratkaista ongelma on tämä, luonnollisesti siitä seuraa se, että joudutaan käsittelemään ihan asiakaskoodissa (koska ollaan niin dynaamisia ettei rajapintoja voida määritellä) kentät "author", "authors" ja "authros". On tietysti dynaamisuuden riemuvoitto, ja pelkästään positiivinen asia, että kaikki tietoa hakevat joutuvat kirjoittamaan samat häkit koodiinsa. Kaikki staattinen tyypityshän on pahaa, haitallista ja vähintään rumaa.
Luonnollisesti lukijoille alkaa selvitä ratkaisun väjäämättömyys. Jokainen linkki kuvatussa ketjussa johtaa yhteen, ja vain yhteen, ratkaisuun.
Lopullisena ratkaisuna koodauksen ongelmiin on kaikkinainen validoinnin, määriteltyjen rajapintojen, ja erittäin haitallisen staattisen tyypityksen poistaminen.
Onhan selvää, että se, että määriteltäisiin tietotyypille luokka, jota voitaisiin suoraan käyttää, sen sijaan että säilytetään tieto glorifioidussa hashmapissa, olisi fataali isku tiimin dynaamisuudelle, ja hidastaisi huomattavasti featureiden lisäämistä projektiin.
Kaikkihan me ohjelmistoprojekteja tehneet tiedämme, että kaikkinainen kiintopisteiden puuttuminen nostaa projektin vauhdin huippuunsa.
Ei! Olisi fataalia, jos joutuisimme sanomaan vaikka:
public class Book { public String title, text; public List authors = new LinkedList();
}
ja tulostamaan objektin JSON-formaatista author-tiedon koodilla
System.out.println(new Gson().fromJson(message1, Book.class).authors);
Pelasta, Kennu, meidät määritellyiltä rajapinnoilta!
Aijai, kyllä tämä blog-väittely voittaa vanhat irkkiflametukset 10-0.
Tarkastelen tämän uuden postituksen argumentteja käänteisessä järjestyksessä, koska siinä järjestyksessä päästään konkretiasta melko humanistiseen lopetukseen.
Ironista että tuot suorituskyvyn mukaan argumentiksi kun alkuperäiset esimerkkisi ovat Pythonilla... (sori, vähän vyön alle mutta mun mielestä tässä keskustelussa suorituskyky ei ole olennaista)
Jos suorituskyky on toteutuksen kannalta ratkaiseva tekijä, niin hyvinkin monien operaatioiden toteutus on optimoitavissa niin että operaation suoritusaika on O(n) tai jopa vakio O(1). Datarakenteen esikäsittelyllä (esim. vaikka juuri dictionaryksi) ja parilla tempulla XPath tyylinen navigointi on optimoitavissa O(n) nopeuteen missä on n haettujen tasojen määrä.
Eikä JSON datan kanssa datan haun optimointi ole edes kovin raskas operaatio. XML-parserien kehittymisestä on nähtävissä että parserin suorituskyvyn suhteen on hyvinkin paljon kiinni siitä miten parseri parseroi dataa, eikä niinkään siitä miten käyttäjä hakee parseroitua dataa. ts: O(parserointi) >> O(aksessointi)
2) Käytön helppous ja luontevuus
Anti-esimerkkisi on yksi rajapintatoteutus ja esimerkeistä päätellen ainakin ohjelmoijan kannalta huono.
Itse pitäisin luontevimpana rajapintana sellaista rajapintaa, joka mahdollistaisi sekä tyypitetyn että tyypittämättömän käytön. Olisi hienoa jos pystyisi halutessaan sanomaan että "alusta luokka lapsineen tästä nodesta lähtien", jolloin saisi halutessaan täysiverisen objektigraafin domain-luokkineen käyttöön. Toisaalta taas jossain toisessa käyttötapauksessa olisi kiva tulostaa yksittäisen noden kentät suoraan. Nämä eivät ole millään tavalla toisensa poissulkevia käyttötapoja ja suunnilleen samanlaisen rajapinnan pystyy tarjoamaan sekä vahvasti että dynaamisesti tyypitetyllä kielellä:
json["authors/apo"].name // haetaan name kenttä json["authors/apo", Author].foo() // missä foo() on Author-luokan tärkeä metodi
Ilman tyypitysmahdollisuutta JSON-merkkijonosta otetut "objektit" ovat aneemisia ( http://en.wikipedia.org/wiki/Anemic_Domain_Model ) ja niiden käyttöalue rajautuu lähinnä kenttien arvojen tulostamiseen. Se kyllä taitaa kattaa 99,9% JSON:n käyttöalueista, mutta muiden serialisointiformaattien kanssa tuon tyylinen rajapinta voisi olla melko vahva työkalu.
Tästä olen kyllä täysin samaa mieltä. Ohjelmistokehityksen saralla on tapahtunut viime vuosina huimasti parannusta ja ainakin jotkin ohjelmointirajapintojen kehittäjät ovat tuntuneet viisastuvan aiemmista virheistä. Uudemmilla kielillä kehitys on ollut ketterää, vanhemmilla kielillä menneisyyden painolasti tuntuu ikävästi hidastavan helppokäyttöisten rajapintojen leviämistä.
Menneisyyden painolastia löytyy tosin kyllä rajapintojen käyttäjienkin puolelta, kuten aiempi blogikirjoitus osoitti. Vanha ohjelmointikieli taipuu sellaiseen mihin se ei kirjoittajan mielestä pystynyt, luontevuuteen. Menneisyyden kautta tuollainen väärinkäsitys on kuitenkin helppo ymmärtää, Javalla monissa kirjastoissa on ollut tapana nojata tyypitykseen hyvinkin vahvasti. "Näin on aina tehty". Ajat onneksi muuttuvat. Tietämys kasvaa, hyvät ja toimivat käytännöt ja ratkaisut leviävät, mutta suunnan muuttaminen voi olla hidas prosessi varsinkin jos väärinkäsityksiin ei puututa.
Itseasiassa menneisyyden painolastin takia omien rajapintakuorrutusten teko on joissain tapauksissa perusteltua. Jos teknologiaksi on valittu sellainen pino mikä ei tarjota "luontevia rajapintoja", niin sopivasti kuorruttamalla rajapinnoista voidaan saada hyvinkin käytettäviä. "Sweet spot" voi löytyä melko vähäiselläkin koodimäärällä.
Itse en lähtisi julistamaan vahvaa tyypitystä kuolleeksi ja dynaamista tyypitystä kuninkaaksi, koska molemmilla on tällä hetkellä heikkouksia. Onneksi molemmilla kielityypeillä voidaan tehdä koodia ja rajapintoja jotka tarjoavat tyypityksen ja dynaamisuuden vahvimmat puolet ohjelmoijille, joten yhteen silverbullettiin ei tarvitse turvautua.
Apo: Pidän kyllä edelleen kiinni siitä, että jonkin stringin esikääntämisellä saavutettu suorituskyky on ristiriidassa käyttömukavuuden kanssa. Esikääntäminen sopii ihan hyvin sovellukseen, jossa esikäännetään vaikkapa muutama vakioitu regexpi tai XPath-lause. Mutta jos ohjelman jok'ikinen property-accessori tarvitsee esikääntämisen, niin hankalaksi menee..
Kiinnittäisin muuten myös huomiota siihen, että tässä keskustelussa on esitelty aika paljon puolusteluja ja kikkoja siihen, miten staattisen tyypityksen haittoja voidaan kiertää, mutta ei ole puhuttu mitään sen eduista. Kukaan ei ole esittänyt vahvaa argumenttia sille, että staattisesta tyypityksestä olisi oikeasti jotain hyötyä. Uskallan väittää, että se on Java-miehille lähinnä psykologinen juttu, ihan niinkuin const-määre (ja siitä luopuminen) oli joskus C/C++-porukalle.
Olen jo moneen kertaan tässä keskustelussa päättänyt, että sai olla viimeinen kommentti, mutta aina vaan trollaannun takaisin. Mutta tämä sai nyt olla ihkuoikeasti viimeinen, ja kirjoitan sitten pitemmän sepustuksen omaan blogiin heinäsirkkojen luettavaksi, jos vielä tunnen tarvetta avautua.
Viimeinen uteliaisuuteni kohde, ja tärkeä kontekstin lähde minulle, on Kennethille osoitettu kysymys: koska en tunnista käyttämiäni työkaluja kuvauksistasi, kun puhut staattisesta tyypityksestä, voisitko paljastaa mikä on se staattisesti tyypittävä ohjelmointityökalu jota olet viimeksi käyttänyt vaikka minimissään kolme, mutta mielellään ainakin kuusi, kuukautta putkeen tuotantoprojektissa, ja milloin?
Ehkä me puhumme ristiin, koska jokin anomalinen staattisen tyypityksen työkalu myrkytti kokemuksesi, mutta se ei vielä ole minun kokemuspiirissäni.
Uskokaa tai älkää, olen oikeasti nähnyt tuotantokäytössä tietokantarakenteen, jossa kannan kaikki kentät olivat määritelty VARCHAR2(4000) tai BLOB -kentiksi. Okei, taisi siellä olla mukana joku counter- ja ID-kenttäkin, mutta kaikki datakentät olivat näitä. Mukaan lukien valuutat, numeeriset kentät sekä päivämääräkentät.
Näihin kenttiin sitten tallennettiin data aina XML-tagien ympäröiminä. Siis <data tyyppi = "etuNimi"> Ossi < /data >.
Kun kyselin, miksi ihmeessä moiseen rakenteeseen on päädytty, oli selitys että XML-rakennetta on huomattavasti helpompi muuttaa kuin kannan rakennetta jos & kun täytyy tehdä muutoksia. Lisäksi kuulemma ei taatusti tule aluekoodausongelmia.
Kantana tässä muuten ei ollut MySQL, vaan Oracle.
Ossi: Tuo kuvastaa hyvin SQL-tietokantojen rajoittuneen tyyppi/schemasysteemin ongelmia, ja miten niitä päädytään kiertämään kaikenlaisilla virityksillä, jotka rikkovat relaatiomallin idean. Siksipä kasvava kysyntä schemattomille NoSQL-kannoille.
You can use Markdown to format your comment:
Separate paragraphs in your text with two newlines