ITI0011:Säuts - koodijuhend

Allikas: Kursused
Mine navigeerimisribale Mine otsikasti

Kirjeldus

Siin lehel on kirjeldatud, kuidas koodi kirjutada 2. kodutöö "Säuts" jaoks. Ülesande kirjeldust loe siit (Säuts).

Kui tõmbate alla malli, siis see sisaldab päris mitut interface'i. Interface'id on antud ette seepärast, et meil oleks võimalik teie koodi testida. Teine variant oleks see, et oleksime andnud ette n-ö mallid (klassid), kus peate meetodite sisud ära kirjeldama. Interface on selle jaoks sobiv, kui tahame kokku leppida meetodite nimetuses ja struktuuris. Teie peate looma klassid, mis implementeerivad neid interface'e. Kui kogu ülesande lahendate ära, peaks teil arvatavasti 11 java faili tekkima (lisaks interface'i failidele). Esialgu on nõue, et interface'ide failid oleksid samas kaustas (paketis) kogu ülejäänud lahendusega. Võimalik, et jõuame tehniliselt valmis lahenduse, mis oskaks interface'e ka mujalt otsida. Võite interface'ide jaoks eraldi alamkausta (ehk package'i) luua. Kui lahendus on teil pakis "twitter", siis interface'id võib panna näiteks "twitter.interfaces".

Nagu selles juhendis näete, on ülesanne jagatud väga väikesteks osadeks. See ei pruugi alati praktiline olla. Kindlasti veendute ise, et selle ülesande lahendamise teeb liigne tükeldamine palju egamugavamaks ja võib-olla isegi ebaloogiliseks. Tükeldamine on aga hea testimise jaoks. Kui teil oleks vaid üks meetod "main", mis saab ette linna nime ja tagastab selles linnas viimased säutsud, siis me saaksime hinnata teie lahendust vaid meetodi väljundi põhjal. Kui te eksite ühe sammuga selles meetodis, on kogu tulemus vale. Automaatselt hinnates võiks tulemus olla 0. Kui me aga jagame ülesande väikesteks tükkideks, saame igat väikest tükki (ehk sammu) eraldi testida. Kui ülejäänud sammud töötavad, viga on ühes sammus, on võimalik kokku saada näiteks 95%. See number on suvaline, aga mõte on ehk arusaadav. Samamoodi on mõistlik päris projektides jagada mingi funktsionaalsus väiksemateks osadeks, et seda testida ning avastada võimalikke vigu.

Arhitektuur

Süsteemiarhitektuur on järgmine.

      main app           (external) service


                     /-- LocationSearch
                    /
TwitterApplication  ---- Cache
                    \
                     \-- TwitterSearch

Põhirakendus on TwitterApplication, selle küljes on erinevad teenused, teenused on eraldi klassid. Analoogselt võib mõelda, et TwitterApplication on nagu reisibüroo, kes vahendab mingeid teenuseid. Näiteks lennuinfot küsitakse mingilt väliselt teenusel mõne lennufirma käest. Hotellide kohta infot saab mõnest hotelliportallist jne. Antud ülesandes kasutatakse teenustena asukohaotsingut, puhverdamist ja säutsude otsimist (mõne boonusülesande korral võib veel selline teenus lisanduda). Kui programm tahab asukoha informatsiooni saada, pöördub see vastava teenuse poole, mis tegeleb konkreetselt sedatüüpi probleemidega.

Miks need teenused peavad selliselt eraldi olema? Kas need võiksid ka TwitterApplication'i sees meetoditena olla? Põhimõtteliselt küll. Programmis võib olla asukohapäringu jaoks üks meetod, puhverdamiseks eraldi meetodid jne. Me võime täpselt sama funktsionaalsuse saavutada. Küsimus on aga testimisest. Meetodi testimine on mõnevõrra keerulisem. Ülal näidatud arhitektuuri korral on võimalik testida igat teenust eraldi. Samuti on testimise ajal võimalik mõni teenus välja vahetada. LocationSearch'i asemele paneb testimootor näiteks FakeLocationSearch'i. Sellega saab testida, kas TwitterApplication'i suhtlemine asukohateenusega on korrektne. Kui meetodi korral saame kontrollida meetodi töötamist, siis väga keeruline on testida meetodi väljakutsumist. Eraldi teenuste (ehk siis eraldi klassid, mis on seotud põhiklassiga) puhul on võimalik testida ka klassidevahelist suhtlust.

Konstruktor

Eraldi tuleb rääkida TwitterApplication konstruktorist (täpsemalt siis klassi, mis implementeerib TwitterApplication'it, konstruktorist). Nimelt tuleb selles valmis teha kõik teenuste objektid. Ehk siis asukohaotsing, säutsude otsing jne. Need tuleb salvestada objekti muutujasse, eelistatul läbi setX meetodi (setLocationSearch jne). Peale instantsieerimise (uue objekti loomise) ei tule midagi rohkem teha. Samuti kõik need teenuste objektid, mida te loote, ei tohiks loomise hetkel mingeid päringuid ega muud tegema hakata.

Kui konstruktoris on objektid loodud, siis hiljem pannakse programm käima run meetodiga. Selline jagamine annab testsüsteemil võimaluse näiteks mõni teenus välja vahetada testteenuse vastu. Samuti saab testsüsteem kontrollida mõnda teie teenust eraldi (kuna nüüd on see objekti küljes). Mõnda konkreetselt teenust saaks eraldi testida ka nii, et testsüsteem teeb uue objekti ja testib seda. Selle puhul on keeruline see, et klassi nimetused pole meil fikseeritud. Seega mugavam on lahendada asi nii, et me saame TwitterApplication.getLocationService() meetodiga juba kätte konkreetse instantsi (ehk meil pole vaja new MyLocationService() konstruktorit välja kutsuda, aga sellegipoolest on mul loodud objekt, mis peaks võimaldama päringuid teha).

Programmi käivitamine

Nagu juttu oli, peate looma TwitterApplication interface'i jaoks klassi, mille konstruktor loob teenuste objektid. Programm ise läheb käima selliselt, et loodud objektil kutsutakse välja meetodit run. run on kirjeldatud TwitterApplicationis ära koos sisuga. Vaikimisi (default) sisu teeb seda, et vastavalt etteantud argumentide kogusele käivitatakse kas runInteractive() või runWithArgs(args). Nende sisu tuleb juba teil ise kirjutada. Põhiosas piisab runWithArgs meetodid kirjeldamisest.

Meetodid ja nende sisu

runWithArgs(String[] args) meetod saab ette käsurea argumendid (peaksid olema muutmata kujul). See meetod peaks kasutama getActionsFromArguments(String[] args) meetodid selleks, et saada tegevuste (Action) nimekiri. Selleks tuleb viimatinimetatud meetodit käivitada, ette antakse jälle argumentide massiv (muutmata kujul args). Sellest, mida getActionsFromArguments meetod teeb, räägime allpool. runWithArgs meetodis, kus saime kätte tegevuste nimekirja, peaksime need tegevused nüüd käivitama. Selleks peaks kasutama meetodit executeActions(List<Action> actions), mis paneb tegelikult käsud käima. Sellega runWithArgs sisu ka lõppeb. Küll tasub täiendada sisendikontrolli jms.

getActionsFromArguments(String[] args) meetod saab ette käsurea argumendid ja peab neist tekitama nimekirja (List) tegevustest (Action). Seda infot te enam kuhu edasi ei saada - kogu loogika panete siia. Soovitatavalt tuleb siis kõik elemendid saadud args massiivis läbi käia ja vastavalt sisule teha erinevad Action alamobjektid. Iga alamobjekt tuleb lisada tegevuste listi, mis hiljem tagastatakse selle meetodi poolt. Võib juhtuda, et teil argumentidest on välja lugeda vaid üks tegevus. Ka sellisel juhul peate tegema listi, kuhu panete selle ühe elemendi. Veel märkuseks, et põhiosas teil pole eraldi print tegevust (nagu interaktiivses on). Te võite sellegipoolest kõige lihtsama päringu (ainult piirkonna otsin)jagada: query ja print tegevusteks. Muul juhul tehke lihtsalt query tegevus. Hiljem peate sellega arvestama ExecuteAction meetodis. Lisaks peaks tulemuse salvestama meetodiga setTweets, mis võtab ette nimekirja Tweet objektidest. See tuleks ka kindlasti põhiosas (ja ka teistes) ära teha, kuna sedasi on meil mugavam testida, mis tulemused te saite. Kui prindite lihtsalt välja, siis meil selle väljundi testimine on oluliselt ebatäpsem (kuna võite väga erinevalt väljaprinte teha).

executeActions(List<Ation> actions) on määratud sisuga meetod. See käib läbi kõik etteantud tegevused ja iga tegevuse kohta käivitab executeAction meetodi.

executeAction(Action action) võtab ühe tegevuse (action parameeter) ja teeb ära vastava tegevuse. Näiteks tuleb QueryAction tüüpi tegevus, siis otsite asukohta, (võib-olla kasutate cache'i), tehakse vastavalt piirkonnale twitteri päringu ja vastavalt vajadusele prinditakse kõik välja. Igat tüüpi tegevuse käivitamine toimub erinevalt.

Action.execute() - Action interface kirjeldab ära ühe tühja meetodi, mida võite kasutada selleks, et konkreetne action käivitada. Seega teil on valida, kas kirjutada QueryAction'i tegevus iga erinevat tüüpi objekti juurde või executeAction meetodisse, nagu eelnevas lõigus oli juttu.

Lisaks on vaja get ja set meetodid ära implementeerida. Need teevadki vaid seda, et salvestavad mingi väärtuse kohaliku objekti muutujasse ja vastupidi tagastavad objekti küljes oleva muutuja väärtuse.

Failide paigutamine

Tõmba alla zip-fail, mis sisaldab etteantud malli (interface'ide failid). Loo endale projekt "HW2". Tee sinna sisse näiteks package "rename". Selleks parem klick "src" kausta peal -> new -> package, avanevasse aknasse kirjutad sobiva nime. Sinna alla hakkad oma koodi kirjutama. Põhimõtteliselt võib ka kõik interface'id sinna kopeerida, aga sedasi võivad nad segama hakata. Pigem pane need eraldi alampakki/alamkausta (pakk asub eelnevalt loodud paki sees). Selleks tee uuesti parem klikk näiteks "src" või loodud package'i "rename" peal -> new -> package, avanevasse aknasse kirjuta nüüd sobiv nimi koos eelnevalt loodud paketi nimega, näiteks "rename.interfaces". Seejärel kopeeri kõik zip-failis olevad java failid sinna. Kuna zip-failis olevatel failidel "package" puudub, siis peab igale failile vastava "package" info juurde panema (Eclipse saab siin ka aidata, aga saate seda üks fail haaval teha).

Hiljem, kui hakkad looma mõnda oma klassi, siis kui teed new -> class, siis avanevas aknas vaata üle "Package" väärtus. Kõik kood, mis sa ise kirjutad peaks minema ühte ja samma paketti. Meie näite puhul siis peaks package väärtus olema "rename".

rename asemel peaks kasutama mingit mõistlikku nime.

Sammud põhiosa tegemiseks

Loogika põhiosa lahendamiseks:

  1. Loo klass, mis implementeerib TwitterApplication'it (see tähendab näiteks public class RocketTwitterApplication implements TwitterApplication). Kuna TwitterApplication on interface, peab kõiki temas kirjeldatud abstraktseid meetodeid kirjeldama loodud klassis. Eclipse suudab seda automaatselt teha nii, et kõik vajalikud meetodid on tühjad. Esialgu sellest piisab, et need on tühjad - hakkad neid ükshaaval täitma.
  2. Tee loodud klassi sisse static main, mille sisu on (kui näiteks sinu loodud klassi nimi on RocketTwitterApplication): <source lang="java"> TwitterApplication t = new RocketTwitterApplication(); t.run(args); // main meetodisse antav argumentide massiiv, mis tuleb edasi anda run meetodisse </source>
  3. Loo klassid, mis implementeerivad järgmisi interface'e: LocationSearch, TwitterSearch, TwitterQuery, QueryAction, Tweet. Samamoodi, näiteks public class SuperLocationSearch implements LocationSearch jne. Iga loodav klass peab implementeerima kõiki abstraktseid meetodeid, mis on kirjeldatud vastavas interface'is - esialgu las jäävad tühjaks, jooksvalt hakkad neile sisu kirjutama.
  4. Sinu TwitterApplication (edaspidi nimetan klasse, mis implementeerivad mingit interface'i X järgmiselt: Sinu X) klassi konstruktoris loo uus instants Sinu LocationSearch klassist ja Sinu TwitterSearch klassist. Need tuleb ära salvestada Sinu TwitterApplicationi külge vastavalt setLocationSearch ja setTwitterSearch meetoditega.
  5. Kirjuta sisu meetoditele setLocationSearch, getLocationSearch, setTwitterSearch, getTwitterSearch. Selleks tee kaks muutujat oma loodud TwitterApplication alamklassi, millest üks on vastavalt LocationSearch tüüpi, teine on TwitterSearch tüüpi. set-meetodid võtavad argumendina sisse vastavalt LocationSearch ning TwitterSearch tüüpi objekti. Meetodi sisus toimub näiteks järgmine tegevus: this.myLocationSearchInstance = locationSearch;, ehk siis etteantud objekt salvestatakse teie loodud instantsi muutujasse - sama siis mõlemas set-meetodis.
  6. Sinu TwitterApplication'is meetodisse runWithArgs kirjuta umbes selline kood: <source lang="java"> List<Action> actions = getActionsFromArguments(args); executeActions(actions); </source>
  7. Sinu TwitterApplication'is meetodisse getActionsFromArguments peaks minema käsurea argumentide töötlemine. Töötlemise tulemusel peaks tekkima nimekiri Action-tüüpi objektidest. Põhiosas on ainuke action QueryAction. Seega, kui päring on korrektne, loo uus instants Sinu QueryAction klassist ja pane see List'i. Selle listi tagastadki. Arvesta, et QueryAction'ile tuleb külge anda ka parameetrid kasutades setLocation, setCount meetodeid (linn ja säutsude kogus). <source lang="java"> List<Action> actions = new ArrayList<Action>(); FakeQueryAction queryAction = new FakeQueryAction(); // TODO: add values to queryAction using queryAction.setLocation etc. actions.add(queryAction); return actions; </source>
  8. Selleks, et eelnevalt juttu olnud QueryAction'i parameetrid saaksid salvestatud, on vaja vastavad meetodid valmis kirjutada. Selleks võta lahti oma loodud QueryAciton'i realisatsioon (näiteks UnrealQueryAction) ja loo sinna kaks muutujat: int count, String location. set-meetodid määravad vastavad väärtused (this.location = location), get-meetodid tagastavad vastavad väärtused (return location).
  9. Kui programmi saadud argumendid pole korrektsed või need puuduvad, peaks väljastama abiteksti, kuidas programmi käivitada. Selleks prindi välja vajalikud juhised. Peale juhiste väljaprintimist programm peab lõpetama. Programmi lõpetamiseks kasuta järgmist koodi: System.exit(1);. See lõpetab programmi töö. Etteantav argument (antud juhul 1) tähendab programmi lõpetamise koodi. Vaikimisi, kui programm lõpetab töö tavapäraselt, on selle väärtus 0 (seda kõikide programmide puhul). Kui see arv on 0-st erinev, on tekkinud mingi viga. Erinevate numbritega saab märku anda, mis viga tekkis. Me tahame oma programmi puhul eristada lihtsalt korrektset lõpetamist (kood oleks 0, seda ei pea eraldi programmis ütlema, või kui on vaja, siis System.exit(0);) või ebakorrektset lõpetamist (kood 1).
  10. Sinu TwitterApplication'is meetodisse executeAction (ainsuses) peaks minema kood, mis oskab etteantud action'it käivitada. See, kuidas sa selle lahendad, on sinu otsustada. Aga üldiselt võiks olla nii, et tingimuse if (action instanceof QueryAction) <source lang="java"> Sinu TwitterQuery twitterQuery = getLocationSearch().getQueryFromLocation([location actioni küljest]); List<Sinu Tweet> tweets = getTwitterSearch().getTweets(twitterQuery); </source> põhiosa puhul võib säutsud nüüd välja printida: <source lang="java"> for (Tweet tweet : tweets) { // TODO: implement } </source>

Vaata ka materjalid.git alt praktikum15 koodinäiteid.

Koodi käivitamine

sequence diagram:

ITI0011-hw2 main flow.png

Kui programm käima panna, siis kuidas täielikult lahendatud põhiosa töötamine välja näeb (kõik interface'e realiseerivate klasside nimes on esimene täht A, millele järgneb interface'i nimi):



staatiline meetod ATwitterApplication.main
argumendiks "args" on käsurealt tulevad parameetrid
meetodi sees luuakse uus instants ATwitterApplication klassist.

1.0)
konstruktor ATwitterApplication
argumente pole
meetodi sees luuakse instantsid erinevatest "teenustest", vt 2.1, 2.2

1.1)
konstruktor ALocationSearch
argumente pole
kutsutakse välja ATWitterApplication konstruktori seest
meetodil sisu ei pea olema (ehk siis konstruktorit ei peagi eraldi kirjutama)

1.2)
konstruktor ATwitterSearch
argumente pole
kutsutakse välja ATwitterApplication konstruktori seest
meetodil sisu ei pea olema (ehk siis konstruktorit ei pea eraldi kirjutama)

1.3)
(ATwitterApplication konstruktor lõpetas ära, jätkub main meetod)
ATwitterApplication.run(args) (siin ja edaspidi väljakutse pole staatiline, 
  klassi nimi annab märku, et seda tüüpi objekti küljest tuleb meetod "run" välja kutsuda. 
  Muutuja nimi sõltub sellest, kuidas te selle nimetanud olete.)
Meetodi sisu (olemas interfaces'is) vaatab, kas argumente oli või ei olnud (args.length > 0). Kui argumendid on, pannakse käsurea variant käima

1.4)
ATwitterApplication.runWithArgs(args)
meetodi sisu parsib argumendid tegevuste nimekirjaks (1.5); seejärel käivitab tegevused (1.9).

1.5)
ATwitterApplication.getActionsFromArguments(args)
meetodi sisu: parsib läbi kõik argumendid ja koostab nende põhjal tegevuste nimekirja (List<Action>).
Kuna põhiosas saab tulla vaid tavapäring, loob QueryActioni ja paneb selle listi.
Kui parsimisel tekib viga, tuleb näidata kasutajale juhiseid programmi käivitamiseks.

1.6) 
new AQueryAction() (konstruktor)

1.7)
AQueryAction set params (konkreetselt setLocation, setCount)

1.8)
return List<Action>
Konkreetsemalt arvatavasti: return actions;
Kõik actionid pannakse listi ja see tagastatakse. 
Kuna põhiosas on vaid üks action, tuleb see sellegipoolest listi panna.

1.9)
ATwitterApplication.executeActions(List<Action>)
Meetod saab ette nimekirja tegevustest ja paneb need käima.
Lahendatud on see nii, et iga elemendi kohta rakendatakse meetodit.
See on default realisatsiooniga interface'is olemas -
arvatavasti seda ei pea muutma
ATwitterApplication.executeAction(Action)

1.10)
ATwitterApplication.executeAction(Action)
Meetod saab ette ühe tegevuse objekti (kus on vajaliku tegevuse jaoks kõik väärtused olemas)
ja käivitab selle tegevuse. 
Selle meetodi sisu pead täiesti ise kirjutama.
Tasub kontrollida kõigepealt seda, millist tegevust käivitatakse. 
Selleks kasuta instenceof'i:
if (action instanceof QueryAction) {
    ... execute query action
}

1.11)
ALocationSearch.getQueryFromLocation(String)
Meetod võtab ette kasutaja poolt määratud asukoha,
leiab sellele vastavad geo-koordinaadid, salvestab need
TwitterQuery objekti ja tagastab saadud objekti.
Selle meetodi sisu pead ise kirjutama.

1.12)
Eelnevalt mainitud meetodi sees pead looma objekti:
TwitterQuery q = new ATwitterQuery();

1.13)
.. määrama loodud objektile väärtused
q.set....
...

1.14)
.. ja tagastama loodud objekti.
return q;

1.15, 1.16, 1.17, 1.18) 
ATwitterSearch.getTweets(TwitterQuery)
Eelnevalt saadud TwitterQuery objekt tuleb anda säutsude otsingusse.
(Mainitud väljakutse toimub executeAction'i sees.)
Selle meetodi sisu peate ise kirjutama.
Kasutada tuleb twitter4j teegi funktsionaalsust, mis tagastab nimekirja Status objektidest.
Saadud Status objektid tuleb võtta aluseks, et luua Tweet'ide nimekiri, mis lõpuks tagastatakse.