2
3
p r o d u c t >
4
5 6
7
Codevoorbeeld 26: Resultaat van de RXML template
Merk op dat er ook een conventie bestaat omtrent precedentie van templates indien meerdere templates voldoen aan de naamconventie voor views. Zo hebben RHTML templates voorrang op RXML templates, en hebben RXML templates op hun beurt voorrang op RJS templates.
3.5.2
View-helpers: reductie van code in templates
De technieken beschreven in vorige sectie zijn handig, maar zijn niet speciek voor het Rails framework. Rails voegt echter een bonte verzameling van zogenaamde
3 http://builder.rubyforge.org/ 36
view-helpers toe aan de viewlaag en laat
bovendien de mogelijkheid open om eenvoudig zelf view-helpers te deniëren.
Eenvoudig gezegd zijn
view-helpers methodes die kunnen opgeroepen worden in een template om de viewcode tot een minimum te bepreken. Het schrijven van code in de view, leidt namelijk gemakkelijk tot het schrijven van té veel code in de views. In principe kan men zelfs de databankconnectie en alle andere logica in de view stoppen via een ERb tag. Dit is echter geen goeie aanpak, omwille van volgende redenen:
•
Het opsplitsen van logica in modellen, controllers en views heeft als grootste voordeel het
herge-
bruik van componenten. Dankzij de duidelijk aijning tussen de deelcomponenten kan men namelijk makkelijk componenten wisselen en de impact van veranderingen beperken tot de component zelf. Zo is het bijvoorbeeld mogelijk om eenzelfde controller te gebruiken voor RHTML, RXML of RJS templates, zonder dat de controller hier weet van heeft. Indien alle logica in de viewlaag zou bevat zitten, is dit uiteraard niet meer mogelijk.
•
RHTML, de meest gebruikte optie, is eigenlijk veredelde HTML. In vele bedrijven wordt de view ontwikkeld door professionele designers, die meestal geen programmeurkennis hebben. Het beperken van de hoeveelheid code in de view is dus ook positief om zulke organisatie werkbaar te houden.
•
Ten slotte is het testen van views niet zo eenvoudig als het testen van pure logica. Door de logica te bundelen in view-helpers, kan men deze view-helpers afzonderlijk testen zonder de belemmering van een view.
De collectie van view-helpers die gedenieerd zijn door Rails, zijn te talrijk om hier op te sommen. We
4 voor meer informatie hieromtrent. Het codevoorbeeld hieronder illu-
verwijzen graag naar de Rails API
streert de werking van een view-helper. De uitdrukking in dit voorbeeld, link_to, genereert een HTML-link naar een bepaalde actie van een bepaalde controller. De analoge HTML code is
producten>Bekijk lijst van producten. Het is duidelijk dat de eerste uitdrukking veel eenvoudiger te onthouden en onderhouden is dan de tweede.
1
<%= l i n k _ t o
2 4
lijst
{: controller
3
" Bekijk
van =>
producten " ,
' voorbeeld ' ,
: a c t i o n => " l i j s t _ p r o d u c t e n " } %>
Codevoorbeeld 27: Een eenvoudige view-helper
3.6 Concreet voorbeeld: wisselwerking tussen controller en view Een kort voorbeeld maakt veel duidelijk omtrent de interactie tussen controller en views. Veronderstel dat we een model Student hebben met als attributen naam en telefoonnummer.
Veronderstel dat we
wensen een lijst van studenten te visualiseren en nieuwe studenten toe te voegen. De controllercode staat hieronder weergegeven.
4 http://api.rubyonrails.org/
37
1
class
StudentController <
ApplicationController
2 3
def
4 5
lijst_studenten
@studenten = Student . f i n d ( : a l l ,
: o r d e r =>
' naam ' )
end
6 7
def
8 9
nieuw
@ s t u d e n t = S t u d e n t . new end
10 11
def
maak_student_aan
12
@ s t u d e n t = S t u d e n t . new ( params [ : s t u d e n t ] )
13
if
14 15
: a c t i o n => l i j s t _ s t u d e n t e n
else
16
render
17 18
@student . s av e redirect_to
: a c t i o n => n i e w
end end
19 20
end
Codevoorbeeld 28: StudentController
Het tonen van een lijst van studenten is vrij eenvoudig: de controlleractie haalt alle studenten op en steekt deze in de collectie @studenten. De view heeft toegang tot deze variabele en toont alle studenten door te itereren over de collectie. Dit wordt hieronder geïllustreerd.
1 2 3 4 5
student
in
@ s t u d e n t s%>
<%= s t u d e n t . naam %> <%= s t u d e n t . t e l e f o o n _ n u m m e r %>
6
t a b l e >
7
<%= l i n k _ t o
' Nieuwe
student ' ,
: a c t i o n =>
' n i e u w ' %>
Codevoorbeeld 29: View voor de lijst van studenten
Zoals te zien is in de laatste regel wordt de lijst afgesloten met een link naar de actie 'nieuw '. In deze actie zien we op regel acht van de controllercode dat een object @student wordt aangemaakt, dat echter geen attribuutwaarden bevat. We doen dit omdat dit object gebruikt wordt door de view-helpers om de ingevulde data voorlopig in te steken. Na het uitvoeren van de actie 'nieuw ' wordt bij conventie de view
'nieuw.rhtml' gerendered. Onderstaande code toont deze template. Merk op dat we hier uitvoerig gebruik maken van view-helpers om een HTML-formulier, tekstvelden en de knop aan te maken. Merk ook op dat de informatie verstuurd zal worden naar de actie 'maak_student_aan ', zoals te zien is op de tweede regel.
38
1
<%= e r r o r _ m e s s a g e s _ f o r
2
<% f o r m _ t a g
: a c t i o n =>
3
Naam : <%= t e x t _ f i e l d
4
Telefoon
' s t u d e n t ' %> ' maak_student_aan '
do %>
' student ' ,
%>
Nummer : <%= t e x t _ f i e l d
' naam '
' student ' ,
' telefoon_nummer '
%>
5
6
<%= s u b m i t _ t a g
"Maak aan " %>
<% end %>
Codevoorbeeld 30: nieuw.rhtml
De input van het formulier wordt door Rails automatisch in een hashobject 'params ' gestoken dat toegankelijk is in de controller. Op regel twaalf van de controller zien we dat deze hash kan gebruikt worden om een nieuwe student aan te maken (de eerste parameter van de view-helper text_eld geeft de sleutel van de hash aan). De reden waarom dit kan werken, is omdat we de tekstvelden in de view dezelfde namen hebben gegeven als de werkelijke attributen van het model.
Door deze conventie te volgen kunnen we
alle atrributen van een model met één coderegel instellen. In regel dertien wordt dan nagegaan of het persisteren van de nieuwe student lukt. Indien bijvoorbeeld validatieregels geschonden worden, wordt het formulier voor een student aan te maken opnieuw getoond (regel zestien). Indien het wel lukt, wordt de lijst van studenten getoond, nu aangevuld met de nieuwe student (regel veertien). De reden waarom we gebruik maken van een render in het geval van falen, is omdat een render de toestand van het object
@student bijhoudt. Zodanig kunnen de foutboodschappen van de validatieregels opgeslagen worden in dit object, waarna ze uitgeschreven worden op regel één van de pagina met het aanmaakformulier. Een
redirect op zijn beurt houdt geen toestand bij, wat net vereist is hier.
3.7 Conclusie Wanneer men voorgaande hoofdstukken analyseert, kan men concluderen dat het Rails framework uitblinkt in simpliciteit en compactheid. Men heeft slechts beschikking over enkele componenten, die echter perfect op elkaar afgesteld zijn. Door zich enkel te richten op webapplicaties kan men de vereiste functionaliteit van het framework minimaal houden. Het betekent ook dat het aanleren van Rails vrij eenvoudig is, de overgang naar Ruby buiten beschouwing gelaten. Conventies bepalen waar welk onderdeel van de applicatie moet terecht komen en beperken de conguratietijd tot een absoluut minimum. Wat uniek is aan Rails is dat slechts één manier de goede is om een webapplicatie te ontwikkelen, namelijk die vooropgesteld door Rails. Men heeft de keuzemogelijkheden bewust beperkt, zodat men zich kan concentreren op de werkelijke logica. Het feit dat Rails zich alleen op de webapplicatie-niche richt, is de grote kracht maar meteen ook de achillespees van Rails. Aangezien steeds meer bedrijven echter de evolutie naar webgebaseerde applicaties volgen, gaat Rails ongetwijfeld een positieve toekomst tegemoet. Een tweede groot voordeel is het feit dat Rails een full-stack of een all-in-one framework is. Rails voorziet mogelijkheden voor elke laag of fase. Zo bezit Rails een volwassen data access laag (ActiveRecord), een volwaardig testing framework, een compleet geïntegreerde webserver, etc. Indien men dus vertrouwd is met Rails dient men geen enkel ander pakket te leren of te gebruiken. Ook dit heeft uiteraard implicaties op de productiesnelheid. Doordat alles bovendien voorzien is door Rails zijn deze componenten dan ook volledig op elkaar afgestemd. In vele andere frameworks dient men vaak zelf de verschillende componenten van verschillende producenten te congureren zodat met elkaar kan gecommuniceerd worden. 39
De naam Ruby on Rails is in feite nog niet zo slecht gekozen. Rails vernoemen zonder Ruby zou te veel eer geven aan het framework zelf.
Hoewel het framework vele eindjes aan elkaar knoopt en reeds vele
zaken in eigen beheer neemt, dient de werkelijke logica nog steeds geschreven te worden in Ruby. Daar de productiviteitswinst met Rails zo hoog is, zet Rails Ruby werkelijk op rails, om aan een hogere snelheid vooruit te schrijden (op het vlak van webapplicatie-ontwikkeling).
40
Hoofdstuk 4
Rails praktisch toegepast "After you nish the rst 90% of a project, you have to nish the other 90%." -
Michael Abrash
De bespreking van een framework is nooit volledig zonder een toetsing aan de praktijk. Een framework kan conceptueel nog zo goed in elkaar zitten, maar wanneer het praktisch gebruik ervan tegenvalt is het zo goed als waardeloos. We zullen daarom een case study uitwerken om Rails praktisch te evalueren. De case study bestaat uit de ontwikkeling van een applicatie volgens de Agile Development methodiek. De bevindingen hieromtrent kunnen teruggevonden worden in het volgende hoofdstuk, de beschrijving van de applicatie in dit hoofdstuk.
4.1 Doelstelling Het doel van de implementatie bestaat er in een applicatie na te bouwen die oorspronkelijk ontwikkeld en onderhouden werd en wordt binnen de vakgroep Ghislain Homan Software Engineering Lab (GH-SEL). Meer bepaald bestaat het doel erin aan te tonen dat een soortgelijke functionaliteit bereikt kan worden met Ruby on Rails. Dezoorspronkelijke applicatie werd ontwikkeld met behulp van J2EE in opdracht van de dienst medische nierziekten (nefrologie) van het UZ Gent. Naar analogie en respect ten overstaan van de originele applicatie, werd de applicatie Dieet-S(t)imulatie gedoopt.
4.2 Bespreking van de originele applicatie 4.2.1
Situering
De ontwikkeling van de applicatie is gestart om nierpatiënten (en mogelijks ook personen met andere aandoeningen) de mogelijkheid te geven om hun eetgedrag vast te leggen en op te volgen. Nierpatiënten mogen namelijk bepaalde grenzen van nutriënten niet overschrijden in een bepaalde tijdspanne.
Door
ingenomen voedsel in te geven kan men nagaan wat en hoeveel men nog in de tijdspanne kan eten. De projectomschrijving van de originele applicatie vermeldt het volgende: 41
Binnen het kader van chronische ziektes worden patiënten met heel uiteenlopende pathologieën geconfronteerd met strenge dieetvoorschriften. Deze voorschriften zijn voor tal van patiënten, alsook voor hun naasten, een belangrijke stressfactor. Veel patiënten slagen er dan ook niet of onvoldoende in om zich aan een voorgesteld dieet te houden. (...) Nu blijkt dat een interactieve informatievoorziening aan patiënten een betere dieetopvolging met zich meebrengt. Een verhoogde opvolging op het gebied van hun dieet zal indirect ook aanleiding geven tot minder medische kosten in de vorm van medicatie, minder snel ziekteverloop, betere algemene gezondheid, minder ziekenhuisopnames... Ook een groot aantal mensen zonder medische klachten kan voordeel halen hebben uit een goed inzicht in hun dagelijkse voedingsgewoonten. Dit komt ten goede aan zowel de patiënt als de gemeenschap (terugbetalingen via ziektekostenverzekering), en brengt zeker een meerwaarde met zich mee, naast de wetenschap dat de levenskwaliteit sterk kan verbeterd worden voor een grote groep van mensen.
4.2.2
Functionaliteit
In grote lijnen kan men in de originele applicatie volgende functionaliteiten onderscheiden:
• Authenticatie:
het is van groot belang dat patiënten en begeleidend personeel pas toegang krijgen
tot de private gegevens na authenticatie.
Het zou onaanvaardbaar zijn dat derden de medische
gegevens kunnen raadplegen. Verbonden met authenticatie is het
• Ingeven van voedselopname:
beheer van gebruikers.
patiënten kunnen ingeven wat ze hebben gegeten en dit opslaan.
Het ingeven van een maaltijd verloopt door het kiezen van bepaalde voedingsmiddelen uit een lijst, die onderverdeeld is in categorieën en subcategorieën. Op dit moment wordt gebruik gemaakt van informatie verkegen van het NUBEL (Nutriënten België) agentschap.
• Grasche visualisatie van een maaltijd:
de gekozen voedingsmiddelen worden door middel
van grasche symbolen afgebeeld op een bord. Het voordeel van deze eenvoudige doch intuïtieve voorstelling is dat deze toegankelijk is voor alle lagen van de bevolking.
• Samenstelling van een maaltijd:
men kan van de voedingsmiddelen apart, of van de maaltijd
in het geheel bekijken wat de samenstelling onder de vorm van nutriënten is.
Wanneer bepaalde
grenzen overschreden worden, wordt dit ook grasch weergegeven.
• Historiek en statistieken:
de reeds ingegeven maaltijden kunnen geraadpleegd worden via zowel
tekstuele als grasche visualisatie.
Op die manier kunnen patiënten en hun zorgverstrekkers het
eetgedrag over een bepaalde periode nagaan.
Merk op dat deze lijst slechts summier de belangrijkste functionaliteit aangeeft van de applicatie. Een volledige business analyse is reeds verricht door Dr. K. De Schutter, S. Van Wonterghem, B. Adams en D. Matthys, waardoor het nutteloos zou zijn hun werk hier te herhalen. Deze businessanalyse is wel het vertrekpunt geweest in de ontwikkeling van onze implementatie. Het is de functionaliteit in bovenstaande lijst die we ook zeker wensen terug te vinden in onze uiteindelijke implementatie.
Onderstaande screenshot geeft een beeld vanuit het standpunt van een patiënt in de
originele applicatie. 42
4.2.3
Architectuur
De oorspronkelijke applicatie is opgebouwd als een client-server systeem. Dit is vandaag de dag de meest gekozen architectuur voor applicaties, waarbij het grootste voordeel de scheiding tussen de client en server is.
De server voorziet in een bepaalde dienst waarvan een aantal clients gebruik maken om een
functionaliteit aan een gebruiker aan te bieden. Aan clientzijde wordt gebruik gemaakt van lichte (thin) client.
Dit betekent dat het meeste van de
logica zich aan serverzijde bevindt, in plaats van aan clientzijde.
Met Rails en met webframeworks in
het algemeen, is men sowieso beperkt tot een ultra-thin client (de browser), dus veel verschil tussen de originele en onze applicatie zal er op dit vlak niet zijn. Aan serverzijde kunnen we twee lagen onderscheiden:
een
business laag en een data laag.
de naam laat vermoeden is de functie van de data laag het aanbieden van data. dit hier de data uit de Nubel-tabel, gebruikersinformatie en statistische informatie. gebruikt men het
Zoals
Concreet betekent In deze data laag
DAO (Data Access Object) design pattern om wijzigingen aan het datamodel in de
toekomst eenvoudig mogelijk te maken. Het DAO pattern bestaat uit een interface welke de data-gedreven functionaliteit aanbiedt naar buiten toe. Deze interface encapsuleert de databron, zodanig dat men slechts de interface dient te kennen en niet de implementatie. De werkelijke code die ervoor zorgt dat de juiste data gelezen, gemanipuleerd of verwijderd wordt is een klasse die dan de DAO interface implementeert. De consequenties van deze eenvoudige design pattern zijn verregaand. Dankzij het DAO pattern kan men de databron wijzigen, door een nieuwe klasse te schrijven die de DAO interface implementeert, zonder dat aan de rest van de applicatie aanpassingen dienen gedaan te worden. De rest van de applicatie werkt namelijk met de DAO interface, en deze is ongewijzigd.
Op die manier kan men de data opslaan in
databanken, XML-les, objecten, etc. en blijft de applicatiecode ongewijzigd. De tweede laag aan serverzijde, is zoals gezegd de business laag. Deze laag wordt gevormd door een set van
services. Deze services bestaan in de eerste plaats uit een interface welke een bepaalde functionaliteit
aanbiedt naar de clients toe. Deze interfaces, en deze interfaces alleen, vormen het aanspreekpunt van de clients. Wederom komt dit de uitbreidbaarheid meer dan ten goede. Men kan namelijk de implementatie van de interfaces wijzigen, zonder dat de clientcode dient te wijzigen, aangezien deze enkel de interfaces kennen. In de architectuur kan men vier verschillende services onderscheiden. De implementatie van de
43
meeste van deze services maken op hun beurt gebruik van de DAO interfaces. Hierdoor krijgt men in de globale architectuur een duidelijke scheiding in drie lagen: client, business en data; waarbij zich op de scheiding de besproken interfaces bevinden om de implementatie van de lagen onafhankelijk van elkaar te maken. De services zijn:
• Foodservice:
deze service biedt functionaliteit aan om informatie over voedingsmiddelen op te
zoeken. Bijvoorbeeld het opvragen van alle voedselcategorieën gebeurd via deze service.
• Dietservice:
functionaliteit betreende het dieet. Bijvoorbeeld het ingeven van een geconsumeerde
maaltijd is een functionaliteit van de Dietservice.
• Nephrologyservice: • Patientservice:
geeft aan welke de grenzen voor bepaalde nutriënten zijn voor een patiënt.
biedt functionaliteit omtrent de patiënt aan.
Authenticatie en beveiliging van
gebruikersgegevens verloopt via deze service.
4.3 Agile development in de praktijk In 1.4.2 zagen we reeds dat Rails uitermate geschikt is voor Agile Development, onder meer door een zo goed als directe feedback loop en het feit dat men van in het begin en op ieder moment in de tijd een werkende applicatie heeft. Vermits Rails deze manier van ontwikkelen promoot, hebben we besloten de applicatie te ontwikkelen volgens de Agile Development methodiek. Dit betekent dat we naast Ruby on Rails ook Agile Development kunnen evalueren naar het einde toe. De klant waarvoor we de applicatie zullen ontwikkelen is de vakgroep GH-SEL. De ontwikkeling door middel van Agile Development houdt voor ons grosso modo volgende zaken in:
•
Het werken in
iteraties: dit wil zeggen dat we in een bepaalde (korte) tijdsperiode werken aan één
welgedenieerde functionaliteit of doel. Bij het begin van iedere iteratie komen we samen met de klant en bespreken het volgende doel. Iedere iteratie bestaat uit een implementatie, testen en een
evaluatie op het einde. Na iedere iteratie is een (beperkt) werkend product voor handen.
• Communicatie,
met en naar de klant toe. In Agile Development stelt men de afnemer van het
uiteindelijke product centraal.
Er is zeer veel (mondelinge) communicatie, waardoor men precies
weet wat de klant wil of niet wil.
•
Een
werkend product na iedere iteratie, hetzij met beperkte functionaliteit. Door na iedere iteratie
een werkend product te hebben kan de klant ook ingrijpen wanneer iets niet is zoals gewenst.
• De klant centraal.
Het werken in iteraties heeft als voordeel dat op elk ogenblik een werkend
product kan getoond worden. Het tonen van het product aan de klant heeft als voordeel dat de klant de vooruitgang met eigen ogen kan aanschouwen. De klant kan bovendien ingrijpen, commentaar en kritiek leveren wanneer dit nodig zou blijken. In Agile Development poogt men de klant zo dicht mogelijk te betrekken bij de ontwikkeling van het project. Zowel de klant, die zich betrokken voelt, als de programmeurs, die beter op de wensen van de klant kunnen inspelen, hebben er baat bij.
44
Hoofdstuk 5
Implementatie van de Dieet-S(t)imulatie applicatie "Programs must be written for people to read, and only incidentally for machines to execute." -
Abelson & Sussman , Massachusetts Institute of Technology
In dit deel bekijken we de werkelijke implementatie van de applicatie beschreven in hoofdstuk 4. Het is niet de bedoeling om hier de code van de applicatie in detail toe te lichten. Dit zou niet veel bijbrengen en we richten onze pijlen liever op de software-ontwikkeling zelf zoals voorzieningen en gebruikte werkwijzen alsook de sterke of zwakke punten in Rails. De volledig broncode en applicatie kan worden teruggevonden op de CD-ROM. De bespreking is chronologisch opgebouwd en gaat van de eerste iteratie tot de laatste iteratie zoals deze in werkelijkheid werd afgehandeld.
Dit lijkt ons e beste aanpak om het iteratie-gebaseerde Agile
Development uit te werken. Per iteratie wordt ingegaan op hoe de zaken werden aangepakt om de gewenste functionaliteit te bereiken. Wanneer tijdens de iteratie een voordeel, nadeel of nieuwe techniek van Rails werd ontdekt, wordt deze besproken in die iteratie waar de ontdekking ook daadwerkelijk voorkwam.
5.1 Architectuur Een kritiek die vaak geopperd wordt bij het gebruik van Agile methoden is dat men het ontwerp van de architectuur negeert. Men heeft weliswaar een idee van waar men uiteindelijk naartoe wil, maar de architectuur wordt pas zichtbaar na enkele iteraties en wordt steeds verjnder. Dit heeft alles te maken met het hoofdoel van Agile Development: snelheid. Men wil de klant zo snel mogelijk een werkend product leveren in een zo kort mogelijke tijd. Het is correct dat Agile methoden minder de nadruk leggen op een architectuurontwerp vooraf, maar dit sluit een architectuur als dusdanig niet uit.
Men dient de gulden middenweg te volgen, en het gezond
verstand te gebruiken bij het ontwikkelen via Agile methoden. Als men bijvoorbeeld ziet dat men zeer
45
veel data heeft, dient men niet te beginnen met een gewoon bestand, om dan later te refactoren naar een databank. Bovendien stelt Fowler, dient een Agile programmeur zeker en vast rekening te houden met toekomstige uitbreidingen. Het is niet omdat men werkt in iteraties, dat de code uit deze iteraties niet toekomstbestendig kan zijn. Volgens hem zijn bewezen design patterns onmisbaar om dit te verwezenlijken. Wanneer we het bovenstaande toepassen op Rails, zien we dat Rails de Agile denkwijze goed ondersteunt. In Rails is een globale architectuur reeds vastgelegd zoals gezien in 1.4.3. Het werken in iteraties is dus zeer eenvoudig met Rails, vermits elk codedeel één welgedenieerde plaats heeft in de MVC architectuur en we dus geen architectuurontwerp hoeven te doen vooraf. Bovendien komt het ontwerp van de architectuur van de originele applicatie vrij goed overeen met de MVC architectuur:
•
De view laag komt overeen met de GUI aan clientzijde. We wensen een soortgelijke visualisatie te bereiken, echter met een andere technologie.
•
De controller laag vormt de plaats waar de business logica zich bevindt.
De business laag in de
originele applicatie komt hiermee overeen.
•
De model laag kan gezien worden als de data laag in het origineel ontwerp.
Merk op dat Rails op het eerste zicht geen absolute scheiding van de lagen biedt zoals het geval is met interfaces. De controller laag gebruikt de model laag zonder tussenkomst van een interface. Wijzigt een model, dan wijzigen ook de controllers die dit model gebruiken. Dit is zeker en vast een probleem. We gaan dieper in op dit probleem in 5.2.7.
5.2 Iteratie 1: databankontwerp en modellen voor voeding 5.2.1
Doelstelling
De applicatie is een zeer data-gedreven applicatie. Centraal staat de informatie omtrent de voedingsmiddelen. Deze informatie wordt getoond aan de gebruiker, toegepast in de berekeningen van de nutriëntenwaarden, aangewend om de maaltijd mee samen te stellen en historieken op te slaan. Zonder deze data zou de applicatie niet bestaan of allerminst van betekenis zijn. Het is dan niet meer dan verwonderlijk dat het doel van de eerste iteratie het modelleren van deze voedingsmiddelen is. Het ontwerp van een rudimentair model is ook de meest gekozen werkwijze om een Rails project aan te vangen.
5.2.2
Databankontwerp
De Nubel tabel werd ons geleverd als een csv bestand. Elke rij van deze tabel stelt een bepaald voedingsmiddel voor.
Voor elk van de voedingsmiddelen zijn de waarden voor 27 nutriënten gegeven (energie,
eiwitten, vet, suikers, etc.). Deze waarden gelden voor 100 gram van het betreende voedingsmiddel. Elk van de voedingsmiddelen behoort tevens tot een bepaalde categorie, waarbij subcategorieën mogelijk zijn. Deze informatie indachtig, bekomen we de databankstructuur zoals te zien op guur 5.1. Belangrijk is allereerst op te merken dat we de conventies van Rails volgen voor het ontwerp van de tabellen. Deze conventies zullen later werk besparen door bepaalde zaken automatisch te laten verlopen net omdat de conventies gevolgd worden: 46
Figuur 5.1: Databankontwerp voor iteratie 1
•
De namen van tabellen zijn in het meervoud.
•
De primaire sleutel is 'id'.
•
Vreemde sleutels hebben als naam de naam van de gerefereerde tabel in het enkelvoud, gevolgd door een underscore en 'id'.
•
De zelf-refererende tabel categories heeft als sleutel naar zichzelf de naam parent_id.
Dit is
belangrijk om Rails automatisch voor ons een boomstructuur te laten opbouwen van categorieën en subcategorieën (zie 5.2.4).
Elke categorie heeft een naam, een url naar een icoon en een vreemde sleutel naar een andere categorie. De tabel foods bevat alle voedingsmiddelen, voorzien van een naam, de url naar een icoon en een vreemde sleutel naar de categorie waartoe het voedingsmiddel behoort.
Voor het opslaan van waarden van de
nutriënten per voedingsmiddel wordt gebruik gemaakt van de jointabel foodcontents (die weliswaar ook een model zal vormen). Een rij in deze tabel linkt één bepaald nutriënt met één bepaald voedingsmiddel, en slaat hiervoor een hoeveelheid (quantity ) op. Door deze vorm van normalisering te gebruiken kunnen nieuwe nutriënten onafhankelijk van de voedingsmiddelen toegevoegd worden en omgekeerd.
Tenslotte
is een nutriënt gelinkt met een bepaalde eenheid, afkomstig uit de tabel units met een vreemde sleutel vanuit nutrients. Voor elke eenheid slaan we de naam en de afkorting ervan op.
5.2.3
Migraties
Er zijn twee valabele opties bij het aanmaken van de vereiste tabellen. De eerste mogelijkheid is gebruik te maken van de DDL (Data Denition Language) van de databank. Over het algemeen genomen is dit standaard SQL, hoewel er tussen verschillende databankproducten subtiele verschillen kunnen zitten. Zo is een geheel getal bij MySQL int(11), terwijl het bij postgresql integer is en bij een Oracle product number. Dit is slechts één voorbeeld uit de vele, subtiele verschillen. Het is dan ook duidelijkdat de portabiliteit van de denitie van de databankschema's niet gegarandeerd is. Een tweede probleem met databankspecieke DDL is versiebeheer. vermijdelijk veranderingen met zich mee.
Het werken in iteraties brengt on-
Ook het databankschema ontsnapt hier niet aan: een tabel
verwijderen, toevoegen, kolommen hernoemen, kolommen toevoegen, etc. zijn nu eenmaal eigen aan een iteratie-gebaseerde aanpak. Bij applicatiecode wordt dit probleem opgelost door gebruik te maken van een versiecontrolesysteem zoals CVS of SVN. Verschillende programmeurs kunnen aan dezelfde applicatie
47
werken en hun code toevoegen aan een centrale repository. Deze repository zal de verschillende stukken code samenvoegen en een versienummer geven. In het geval van problemen kan men dan eenvoudig teruggaan (rollback) naar een oudere, correcte versie van de applicatie. Voor databankschema's bestaat echter zo geen elegante oplossing. De meest benaderende manier is het DDL schema ook onder versiecontrole te houden en bij problemen het gehele databankschema verwijderen en de oudere DDL opnieuw toepassen. Het is duidelijk dat hierdoor ook alle data in de databank verloren gaat, wat meestal niet wenselijk is. Sinds recent (versie 1.1.2) zijn migraties toegevoegd aan het Rails framework. Migraties leveren onder meer een oplossing voor bovenstaande problemen. Migraties zijn het best uit te leggen aan de hand van een voorbeeld. De migratie voor de tabel nutrients ziet er bijvoorbeeld als volgt uit:
1
class
2
CreateNutrients < ActiveRecord : : Migration extend
MigrationHelpers
3 4
def
s e l f . up
5
create_table
6
t . column
: name ,
7
t . column
: unit_id ,
8
: nutrients
do
| t|
: string ,
: n u l l =>
: integer ,
false
: n u l l =>
false
end
9 10
foreign_key
: nutrients ,
: unit_id ,
: units
11 12
add_index
13
: nutrients ,
: name
end
14 15
def
16 17 18
s e l f . down drop_table
: nutrients
end end
Codevoorbeeld 31: Migrate voor de nutrients tabel
Dit voorbeeld toont een aantal interessante zaken aan. methoden:
de methode up en de methode down .
We zien we dat een migratie bestaat uit twee De methode up wordt toegepast wanneer we
migreren (vandaar de naam) naar een hogere versie dan de huidige versie van de databank.
In dit
voorbeeld zien we vooraleerst dat een tabel nutrients wordt gecreeërd met twee kolommen. Merk op dat we geen kolom id speciceren zoals vereist volgens de conventies van Rails. Rails zal automatisch een primaire sleutel genaamd id aanmaken indien we niet expliciet de primaire sleutel opgeven. Wederom een voorbeeld van Convention over Conguration. Zoals uit het voorbeeld blijkt, wordt een kolom aangemaakt door een naam en type op te geven. Voorts zijn een aantal optionele parameters zoals hier het feit dat null verboden is mogelijk. In wezen is een migratie niet meer dan een in Ruby geschreven subklasse van de klasse Migration van de module ActiveRecord.
We drukken met andere woorden het databankschema uit in Ruby.
In feite
zijn migraties een perfect voorbeeld van een DSL, zoals beschreven in 2.4. Door deze DSL te gebruiken kunnen we op een
databankonafhankelijke manier de verschillende tabellen speciceren, waarbij Rails
dan deze DSL converteert naar databankspecieke DDL. Door migraties te gebruiken bereiken we dus in de eerste plaats databankonafhankelijkheid. Maar migraties lossen ook het probleem van versiecontrole op. De manier waarop dit gebeurt, is werkelijk 48
simpel. Iedere migratie krijgt een nummer. Maken we een nieuwe migratie aan, krijgt deze het volgende beschikbaar nummer. In de databank houdt Rails een tabel bij met één rij en één kolom met daarin de huidige versie van de databank. Wanneer we het migratiecommando ingeven, controleert Rails het verschil tussen de huidige versie en de beschikbare migraties die benoemd zijn volgens conventie (de nummer van de migratie vooraan). De migraties die hoger zijn dan de huidige versie worden toegepast en de huidige versie wordt verhoogd. Analoog kunnen we ook zeer eenvoudig teruggaan naar een vorige versie van de databank via het omgekeerde commando. Daartoe dient de methode down . In deze methode dienen we de omgekeerde operaties uit te voeren dan diegene die zijn toegepast in het migreren naar een hogere versie via de methode up . In het voorbeeld is de omgekeerde versie van het aanmaken van een tabel het verwijderen van die tabel. Zoals ook uit het voorbeeld blijkt is de DSL van migraties vrij uitgebreid. Merk op dat we in het voorbeeld niet helemaal eerlijk zijn geweest. De uitdrukking foreign_key behoort namelijk niet tot de lijst van uitdrukkingen van migraties. Migraties bieden geen ondersteuning voor het opgeven van vreemde sleutels. De reden die hiervoor wordt aangehaald, is dat men deze constraint op modelniveau dient aan te pakken. We delen echter de mening van Dave Thomas zoals beschreven in [17], waarin hij aangeeft de opgave van vreemde sleutels beter behoort tot de denitie van het datamodel. Naar zijn voorbeeld hebben we dus de DSL van migraties uitgebreid met volgende methode:
1
module
2
def
f o r e i g n _ k e y ( from_table ,
from_column ,
to_table )
3
c o n s t r a i n t _ n a m e = " fk_#{f r o m _ t a b l e }_#{from_column } "
4
execute
5 6 8
%{ a l t e r
t a b l e #{ f r o m _ t a b l e }
#{ c o n s t r a i n t n a m e }
foreign
key
add
constraint
(#{ from_column } )
r e f e r e n c e s #{ t o _ t a b l e } ( i d ) }
7
MigrationHelpers
end end
Codevoorbeeld 32: Uitbreiding van de migratie DSL voor vreemde sleutels
Merk op dat dit MySQL specieke code is. Vreemde sleutels zijn dus een zwak punt van migraties, maar de verwachtingen zijn dat in volgende versies dit wel zal toegevoegd worden. Dit voorbeeld toont echter wel het dynamische karakter van goed Ruby aan. We hebben zonder weinig code de DSL kunnen uitbreiden met onze eigen code door middel van mix-ins. Als conclusie kunnen we dus stellen dat migraties een zeer welkome toevoeging zijn aan het Rails framework. De voordelen die migraties bieden negeren, zou niet verstandig zijn. We hebben dan ook migraties gecreeërd voor alle tabellen beschreven in 5.2.2. Migraties zijn ook een waardevolle partner bij het toepassen van Agile Development. Niet alleen zijn ze zeer goed in het omgaan met verandering, maar ook het systeem van versienummering maakt het werken in iteraties makkelijker. De denitie van migratie is namelijk het gradueel aanpassen van de databankschema's, en dit gradueel aanpassen is net waar het bij Agile methoden om draait.
5.2.4
Creatie van modelklassen
Door het volgen van de Rails conventies tijdens het databankontwerp (zie 5.2.2), wordt ons heel wat werk bespaard bij de creatie van de modelklassen. Zo is alle datafunctionaliteit reeds voorzien door intelligente 49
toepassing van de metadata in de databank, wat betekent dat een modelklasse in deze fase slechts bestaat uit twee lijnen code (zoals gezien in 3.3.1) plus de validatie- en relatiedeclaraties, aangezien we in deze iteratie enkel tot doel hebben de data voor te stellen en weer te geven. Dit betekent dan ook dat het meeste werk bestaat uit het databankontwerp en dat de modellaag in feite hier uit voortvloeit. Onderstaande code toont bijvoorbeeld het model voor voedsel.
1
class
Food < A c t i v e R e c o r d : : B a s e
2
belongs_to
3
has_many
: foodcontents
: category
4
has_many
: nutrients ,
: t h r o u g h =>
: foodcontents
5
6
validates_presence_of
7
validates_uniqueness_of
8
: name ,
: category_id
: name
end
Codevoorbeeld 33: Modelklasse voor voedsel
Deze code, hoewel slechts zeven lijnen lang, laat volgende zaken toe:
•
Het opvragen van de categorie van een voedingsmiddel (food.category )
•
Het opvragen van de collectie van nutriënten en de bijhorende hoeveelheden (food.nutrients en
food.foodcontents ). Merk op dat de declaratie in lijn vier aangeeft dat een voedingsmiddel meerdere nutriënten bezit, met als join-tabel foodcontents.
Wanneer foodcontents een pure join-tabel zou
geweest zijn (enkel vreemde sleutels) zou een gewone has_many declaratie al genoeg zijn.
Nu
dienen we echter aan te geven dat het model foodcontent tevens dienst doet als join-tabel via de
through optie omdat een voedingsinhoud een volwaardig domeinobject is.
•
Validatie van aanwezigheid van een naam en categorie en uniekheid van naam bij het persisteren van een modelobject van deze klasse.
•
Alle functionaliteit bekomen via metadata, zoals beschreven in sectie 3.3.1: (dynamische) nders, CRUD operaties en getters/setters voor attributen.
De rest van de modelklassen is compleet analoog. Enkel bij de tabel categories hebben we een attribuut
parent_id voorzien. De naam voor het attribuut is niet zomaar gekozen maar volgt wederom een conventie. Deze zorgt ervoor dat automatisch een boomstructuur kan worden opgebouwd met de verschillende categorieën, wat betekent dat we de ouder en de collectie van kinderen van een bepaalde categorie kunnen opvragen alsof het om gewone attributen gaat. Hadden we de naamconventie niet gevolgd had dit vrij veel zelf geschreven logica betekend.
5.2.5
Data migraties: inladen van de Nubel tabel
Migraties zijn niet meer dan Ruby code. Maar tegelijk zijn ze verweven in het Rails framework en hebben dus toegang tot alle componenten ervan.
Meer bepaald hebben ze toegang tot de modelklassen, wat
toelaat data in te laden met behulp van migraties.
50
Hoewel migraties in principe niet ontworpen zijn voor deze taak, is er geen enkele reden waarom we ze niet zouden gebruiken.
In de eerste plaats biedt het gebruik van de eenvoudige modelsyntax een véél
hogere productiesnelheid dan zelf een script in eender welke taal te schrijven welke hoogstwaarschijnlijk gebruik maakt van SQL. Ten tweede zorgen de modelobjecten er voor dat de integriteit van de data reeds verzekerd is bij het inladen, aangezien de validatieregels gedeclareerd zijn in de modellaag. Als laatste voordeel is uiteraard het inherente versiebeheer van migraties een pluspunt: indien er fouten in de data zitten is dit slechts een kwestie van terug neerwaarts migreren.
Onderstaande code toont het gedeelte
omtrent categorieën van de datamigratie die gebruikt werd om de data uit de Nubel tabel in te laden in de databank. Het is duidelijk uit regels acht tot veertien dat ActiveRecord het schrijven van dit script enorm vereenvoudigd.
1
class
2
def
LoadNubelData < A c t i v e R e c o r d : : M i g r a t i o n s e l f . up
3
...
4
while ( l i n e
5
= inputFile . readline )
splittedLine =
6
line . split (" ; ")
c a t e g o r y N a m e = s p l i t t e d L i n e [ 0 ] . g s u b ( / NBS3
−
(([0
− 9 ] + \ . ) +) .
chomp 7
categoryIcon =
' / i c o n s / c a t e g o r i e s / ' << c a t e g o r y N a m e <<
' .
png ' 8
c a t e g o r y = C a t e g o r y . find_by_name ( c a t e g o r y N a m e )
9
if
! category # categorie
bestaat
10
c a t e g o r y = C a t e g o r y . new
11
c a t e g o r y . name = c a t e g o r y N a m e
12
category . icon = categoryIcon
13
category . save
14
end
15
end
16
end
17
def
18 19 20
niet
s e l f . down
Category . d e s t r o y _ a l l end end
Codevoorbeeld 34: Inladen van categorieën via een datamigratie
Hoewel datamigraties in de literatuur slechts summier vermeld worden ([17]), verdienen ze volgens ons toch meer aandacht. Zogoed als elke webapplicatie is datacentrisch en zal dus op een bepaald punt data dienen in te laden.
Zoals in het codevoorbeeld te zien is, werken we op domein- of modelniveau, wat
veel minder regels code tot gevolg heeft. We kunnen dus zonder aarzelen stellen dat migraties een zeer belangrijke component van Rails zijn of zullen worden.
5.2.6
Business logica in modellaag
Een heikel punt is het feit dat in Rails de business logica ingebed is in de modellaag.
Men kan zich
afvragen of deze aanpak wel de goede is. In de praktijk zijn er tal van mogelijkheden om business logica te plaatsen. Wanneer we Rails bekijken, zijn er concreet twee manieren om business logica te implementeren: 51
•
Business logica in de databank, door bijvoorbeeld gebruik te maken van stored procedures.
•
Een aparte business tier die de business logica onafhankelijk van de databank encapsuleert, naar analogie met de originele J2EE applicatie.
Algemeen wordt de eerste manier afgeraden, omdat zo teveel logica verweven wordt met het datamodel. Indien deze data ooit nog herbruikt dient te worden, is de tweede manier de beste oplossing.
In de
originele J2EE applicate wordt ook gebruik gemaakt van een business tier bovenop de data access tier. In de tegenwoordige n-tier architecturen is dit de meest gekozen optie. In Rails lijkt het echter alsof er geen onderscheid is tussen de data access- en business tier. De data access tier is echter zeker en vast aanwezig in Rails, zij het transparant doordat dit alles geautomatiseerd wordt. De modelklassen kunnen dan praktisch volledig tot een business tier gerekend worden, want uiteindelijk bevat zulke klasse enkel validatieregels en eigen logica. We kunnen dus stellen dat Rails de best practice uit de softwareontwikkeling volgt waarbij businesslaag en data access laag van elkaar gescheiden zijn. Door de transparante automatisatie is het onderscheid niet echt duidelijk, maar het is er zeker en vast wel. Opgesomd kunnen we dus in een Rails applicatie onderscheiden:
•
Het
datamodel: bevat fundamentele constraints zoals datatypes, sleuteldeclaraties en de naleving
van referenties.
•
De (transparante)
data access laag: zorgt voor de standaard DAO functionaliteit zoals nders,
CRUD operaties en relaties. Merk op dat deze data access laag automatisch mee evolueert met het datamodel, wat uiteraard heel wat werk uitspaart en perfect past in de Agile losoe om veranderingen makkelijk op te vangen.
•
De
5.2.7
business laag: dit zijn de modelklassen waarin domein-specieke logica staat.
Verandering: Services vs Rails
Een terechte vraag is hoe Rails zou omgaan met veranderingen in de databank. In de originele applicatie werd dit elegant opgelost door de scheiding tussen de DAO laag en de services (business laag). Indien van vandaag op morgen beslist wordt om een andere databron dan de Nubel tabel te gebruiken, moet dit kunnen zonder al te veel werk. Deze vraag is een perfecte toetsing van een extreem geval van verandering, en dus ook van hoe Agile Rails op dat vlak wel is. De oplossing voor dit probleem lag niet voor de hand, zo bleek uit de kleine discussie die ontstond in de vakgroep GH-SEL. Het probleem is dat controllers afhankelijk zijn van veranderingen in het model, en dus afhankelijk zijn van zowel de data access- als de businesslaag in het geval van Rails. Deze afhankelijkheid zou desastreus kunnen blijken voor mogelijke veranderingen. Gesteld dat bijvoorbeeld één attribuut in de databank zou veranderen, kunnen zowel het model als een controller hier hinder van ondervinden. Het probleem komt in feite neer op een gemis van expliciete interfaces in Ruby, wat Java wel heeft. Een mogelijke oplossing die voorgesteld werd, was de originele applicatie te emuleren en een facade boven de modellaag te plaatsen. De controllers zouden dan met de facade praten in plaats van met de modellen. Hoewel dit hoogstwaarschijnlijk een correcte implementatie zal opleveren, is het duidelijk dat hier toch iets wringt aangezien we dan manueel sleutelen aan de vaste MVC architectuur van Rails.
52
De oplossing werd uiteindelijk aangegeven door Richard Conroy op de Ruby on Rails mailinglist.
Hij
stelde dat het uiteindelijk doel eigenlijk een applicatie met verschillende datasources is die structureel verschillen (schema), maar semantisch gelijk zijn (voedingsmiddelen). Deze uitdrukking kan dienen als schooldenitie voor Duck Typing (zie sectie 2.3.5), wat de uiteindelijke oplossing voor het probleem is. Bemerk dat de gelijkaardige semantiek de sleutel is waarom Duck Typing werkt. Bemerk echter ook dat wanneer de semantiek zou wijzigen dan ook de betekenis van de applicatie verandert, wat uiteraard niet de bedoeling kan zijn (bijvoorbeeld onze applicatie gebruiken om boeken te bestellen, hoe absurd dit ook klinkt). Er is op dit moment geen enkele taal of framework die zulke verandering kan opvangen, als het überhaupt ooit mogelijk zou zijn of moeten zijn.. Beschouw bijvoorbeeld het model Category, die het attribuut name bezit. Veronderstel dat deze kolom moet veranderen naar category_number, waarbij een bedrijfsspecieke identicatiecode gebruikt wordt. Er zijn nu twee mogelijkheden om de veranderingen in de controllers te beperken:
•
Er wordt gewerkt van bij het begin met het attribuut name. Bij de attribuutsverandering implementeren we aan modelzijde de
getter
name
, die het category_number retourneert. Het nadeel is
dat het eigenlijk niet meer een naam is en dus verwarrend kan werken in latere onderhoudswerkzaamheden.
•
Van bij de creatie van het model wordt een getter identication voorzien, die in eerste instantie de naam teruggeeft. Wanneer het attribuut in de databank wijzigt, verandert enkel de implementatie van deze getter naar het nieuwe attribuut.
We kunnen Duck Typing in feite beschouwen als
transparante interfaces. Het gebruik van Duck Typing
vraagt enkel van de ontwikkelaar een meer lososche omschakeling, maar het resultaat is uiteindelijk hetzelfde. Dankzij Ruby is Rails dus opgewassen tegen dit soort van problemen. Het J2EE framework biedt echter wel een functionaliteit aan die Rails momenteel niet kan evenaren. Wanneer verschillende services dezelfde interface implementeren maar verschillen qua implementatie laat J2EE toe om at runtime deze service op te zoeken en te gebruiken. Zo kan eenvoudig de producent van de voedingsmiddelentabel gewijzigd worden indien de interface gerespecteerd wordt door de producent. Rails bezit op dit moment nog niet zulke high-level enterprise functionaliteit. Het is ook twijfelachtig of Rails dit ooit zal ondersteunen, aangezien dit absoluut niet de markt is waarop Rails zich richt.
5.2.8
Eenvoudige visualisatie: scaolding
Het doel dat we voor ogen hadden bij de aanvang van deze iteratie is op dit moment bereikt, maar we hebben niets om aan de klant te tonen. Klanten zijn doorgaans minder of niet geïnteresseerd in de achterliggende code, maar des te meer in de mogelijke functionaliteit en performantie. Vanuit het standpunt van de ontwikkelaar is dit eerder ongelukkig, maar dit is nu eenmaal de realiteit. Men vraagt zich ook niet af hoe een wasmachine werkt, enkel of de machine goed werkt en dat ook snel doet. Als ontwikkelaar dient men in de eerste plaats te kiezen voor een degelijke, onderhoudbare en toekomstbestendige implementatie, maar het Agile manifest in het achterhoofd, mag de klant zeker en vast niet vergeten worden. Het zou handig zijn moesten we op dit moment een eenvoudige GUI ontwikkelen via enkele eenvoudige controllers en views. Maar dat zou betekenen dat we buiten de scope van de huidige iteratie gaan, iets wat in Agile Development ten stelligste wordt afgeraden. Rails voorziet een standaard oplossing onder de vorm
53
van
scaolding voor dit probleem. Een scaold, vrij vertaald een werk in steigers, is in Rails-context
een automatisch gegenereerde verzameling van controllers en views en laat toe alle CRUD operaties op de modellen grasch uit te voeren. Scaolds kunnen statisch of dynamisch zijn. In het eerste geval bestaat het scaolding uit het uitvoeren van een script dat aan introspectie van de modellen doet en op basis daarvan controller- en viewcode genereert. Zo zal het datatype onderzocht worden om bijvoorbeeld een tekstveld voor een string-attribuut te genereren in de view. Het resultaat van een statische scaold is fysieke Rails code. Dynamische scaolds bestaan op hun beurt uit scaolddeclaraties voor een model in een controllerklasse. Deze controller zal dan de CRUD operaties voor dat model aanbieden door at runtome de logica te generern. Het voordeel ten opzichte van statische scaolds is dat de controllers en views mee evolueren met aanpassingen in databank en model. Statische scaold vormen op hun beurt dan een basis die men manueel kan aanpassen en uitbreiden indien de stap naar een volwaardige GUI wordt gezet. Figuur 5.2 toont het resultaat van statische scaolding toegepast op het model voor een Categorie. Voor de andere modellen werd een soortgelijke scaold gegenereerd, zodat de klant de vruchten van deze iteratie al kan aanschouwen.
Figuur 5.2: Resultaat van een statische scaold: een listing van de categorieën in de databank
5.2.9
Testing
Iedere iteratie dient het testen van de nieuwe code te bevatten. Algemeen kan testen omschreven worden als de fase waarin de correctheid en volledigheid van software wordt nagegaan.
Tests bestaan uit een
collectie van vragen wiens antwoord wordt nagegaan om te bewijzen dat de applicatie zich gedraagt zoals we vereisen of verwachten.
Men moet steeds onthouden dat testen inherent subjectief is aan de
ontwikkelaar(s) die de tests schrijven en zodoende (onbewust) vaak niet alles getest wordt. Belangrijker in het opzicht van Agile Development is dat (goede) tests garanderen dat niet meer naar voltooide iteraties moet gekeken worden. Indien we een set van tests voor een voltooide iteratie hebben 54
geschreven, kunnen deze tests in een verder stadium herhaald worden om na te gaan of de verandering die inherent is aan Agile Development - geen impact heeft op vroegere implementaties. Rails maakt onderscheid tussen drie soorten test:
unit tests, functionele tests en integratie tests.
Deze drie testen respectievelijk modellen, controllers en de ow in of over één of meerdere controllers. Testing is volledig geïntegreerd in het framework en het schrijven wordt tot op een zekere hoogte aangemoedigd. De creatie van een model of controllerklasse heeft onmiddelijke ook de creatie van een unit- of functionele test tot gevolg. Het feit dat Rails een testframework bezit, dat bovendien geheel geoptimaliseerd is om te werken met de componenten van het framework, kan enkel als een groot voordeel gezien worden. In vele frameworks dient men namelijk de tests te schrijven met een afzonderlijk testframework, zoals bijvoorbeeld Jakarta Cactus of JUnit (zoals in de originele applicatie). Het gebruik van een inherent testframework betekent het verdwijnen van conguratie om de componenten en het testframework met elkaar te laten samenwerken. Het schrijven van tests wordt meestal gedreven door requirements. In de eerste plaats zijn er de
customer
requirements, waaronder vereiste functionaliteit, performantie en workow. Ten tweede zijn er de implementatie requirements, waarbij de implementatie een bepaald gedrag moet vertonen. De vereisten van de klant beperken zich in deze iteratie tot de soort data die moet worden opgeslagen en opgehaald. Meestal zullen de vereisten van een klant uitgedrukt worden in termen van een user interface, wat in deze iteratie van ondergeschikt belang is. Belangrijker in deze iteratie zijn de implementatie requirements: we moeten nagaan of de modellen zich gedragen zoals we wensen, want de correcte werking van de controllers in de volgende iteraties zal in de eerste plaats steunen op een correcte werking van de modellen. Voor deze iteratie kunnen we ons dus beperken tot unit tests, gezien de huidige GUI een tijdelijke is. Voor unit tests maakt Rails gebruik van de standaard Test:Unit module van Ruby, weliswaar aangepast om samen te werken met ActiveRecord. Een unit test is niets meer dan een Ruby klasse die overerft van de klasse Test:Unit:TestCase en waarvan de methodes de conventie volgen dat ze allemaal beginnen met test_. Een Ruby testcase bestaat dus uit meerdere methodes of tests. De verzameling van een aantal testcases noemt men een testsuite. Net zoals de meeste unit testing frameworks zijn de unit tests in Ruby opgebouwd rondom het idee van een setup methode en asserties. In de setup methode dient code te komen die algemeen geldt voor alle tests.
Een assertie is een methodeoproep in een test waarvan de waarheidswaarde van de argumenten
wordt nagegaan. Daarnaast zijn en er een heleboel aangepaste asserties die bijvoorbeeld testen op nil, gelijkheid van argumenten, waarden binnen intervalgrenzen, geworpen excepties, etc. zoals beschreven in [17]. Voor alle modellen werden unit tests geschreven die alle CRUD operaties en extra logica testen. Het testen nam ongeveer de helft van de iteratie in, omdat tests schrijven nu eenmaal uitgebreider is dan de logica zelf schrijven. Men moet namelijk alle mogelijke (rand)gevallen testen om te verzekeren dat de applicatie correct werkt. Ter illustratie staat hieronder de code uit de testcase voor categorieën die de Create operatie controleert. Meer bepaald wordt nagegaan of de validatieregels die de aanwezigheid en uniekheid van een naam vereisen worden gerespecteerd. Merk op dat de variabele @name_stealing_category een variabele is die geïnitialiseerd wordt tijdens de setup (de @ duidt aan dat het hier om een instantievariabele gaat).
55
1
def
2
test_save v a l i d _ c a t e g o r y = C a t e g o r y . c r e a t e ( : name=> ' The_Valid_Category ' )
3
assert
4
n o n _ v a l i d _ c a t e g o r y = C a t e g o r y . new
v a l i d _ c a t e g o r y . save ,
' Saving
5
assert
! non_valid_category . save
6
assert
! @name_stealing_category . save
7
assert_equal
of
valid
category
failed . '
ActiveRecord : : Errors . default_error_messages [ :
taken ] , 8
9
@ n a m e _ s t e a l i n g _ c a t e g o r y . e r r o r s . on ( : name ) end
Codevoorbeeld 35: Unit Test voor het opslaan van een categorie
We kunnen ook eigen logica testen. Beschouw bijvoorbeeld onderstaand voorbeeld. Het gaat hier om een callback (zie 3.3.6) die er voor zorgt dat subcategorieën worden verwijderd bij de verwijdering van een categorie.
1
def
2
before_destroy s e l f . c h i l d r e n . each
3 4
5
do
| child |
child . destroy end end
Codevoorbeeld 36: Callback voor het verwijderen van een categorie
Onderstaande code toont de test voor deze callback. Eerst wordt een random boomstructuur van categorieën opgebouwd in regel vijf, waarna de wortel van deze boom wordt verwijderd in regel acht. Als de callback correct is, moet de gehele boom verwijderd zijn wat getest wordt op regel negen.
1
def
test_cascading_delete
2
n r _ o f _ i n i t i a l _ c a t e g o r i e s = Category . count
3
n r _ o f _ c h i l d r e n = 10
4
nr_of_grandchildren = 5
5
parent = generate_tree ( nr_of_children ,
6
assert_equal
7
+ nr_of_grandchildren + 1 ,
8
assert
9 10
nr_of_grandchildren )
n r _ o f _ i n i t i a l _ c a t e g o r i e s + nr_of_children Category . count
parent . destroy
assert_equal
nr_of_initial_categories ,
Category . count
end
Codevoorbeeld 37: Unit test voor het controleren van de callback in vorig voorbeeld
Testen is hét perfecte middel om ontbrekende of dubieuze logica te ontdekken. Tijdens het testen zijn sommige delen van de modellen herschreven, omdat de tests aantoonden dat het gebruik ervan te complex is. Tijdens het testen is ook gebleken dat bepaalde logica ontbrak in de modellen. Zo is bijvoorbeeld de callback zoals hierboven beschreven in de code geraakt in de test fase, omdat het testen ons deed nadenken over wat het verwijderen van een categorie nu juist betekent.
56
5.2.10
Fixtures
In algemene test-termen bedoelt men met een xture een omgeving waarin tests uitgevoerd worden. In Rails echter, is een test xture een eenvoudige specicatie van de inhoud van een model.
Test xtu-
res worden geschreven volgens het CSV (Comma-Separated Value) of YAML (YAML Ain't Markup Language) formaat. Een xture voor een categorie ziet er in YAML formaat bijvoorbeeld als volgt uit:
1: vers_vlees_categorie id: "1" name: Vers Vlees icon: /icons/categories/vers_vlees.png parent_id:
In testcases kan een xturesbestand gebruikt worden door de naam van de betreende modelklasse op te geven. Rails zal voor elke testmethode de betreende tabel leegmaken en opvullen met de data uit de xtures. Gezien een testsuite loopt onder de test environment (met eigen databank) is de developmenten productiedata automatisch beschermd. Dit betekent dat men in de testdatabank vrij spel heeft en data kan invoeren om alle randgevallen te testen, zonder werkelijke data te bevuilen. In het voorbeeld is ook te zien dat elke xture een unieke naam kan krijgen, die automatisch een lokale variabele met diezelfde naam genereert in de testmethodes. Fixtures zijn uitermate handig omwille van het feit dat ze door Rails automatisch herladen worden voor elke testmethode. Zo is de invloed van elke testmethode geïsoleerd van de andere testmethoden, wat heel wat werk bespaart. Extra handig is dat elk ActiveRecord object zichzelf kan uitschrijven in YAML formaat en het dus een koud kunstje is om een script te schrijven die alle data uit de databank omzet naar xtures zodat we de volledige Nubel tabel in xtureformaat voor handen hadden. Het lijkt misschien vreemd om deze data te gebruiken, maar van die data zijn we zeker dat ze consistent is door de validatieregels. Fixtures omzeilen namelijk ActiveRecord en gebruiken achterliggend rechtstreeks SQL, waardoor geen validatie van de data optreedt.
5.2.11
Het probleem van xtures
Hoewel we in eerste instantie onder de indruk waren van de mogelijkheden van xtures, ging het enthousiasme snel over.
Nadat de gehele databank werd omgezet naar xtures via een eenvoudig Ruby
script, schoot de uitvoeringstijd van de testsuite naar 44 seconden. In zeker zin is dit niet te verwonderen, aangezien er voor bijvoorbeeld foodcontents meer dan 25000 xtures zijn. Dit van de harde schijf halen, parsen en voor elke methode uit de databank halen en herladen heeft onvermijdelijk een overhead. Maar het mag ook duidelijk zijn dat 44 seconden in de eerste iteratie onaanvaardbaar is om werkbaar te zijn. De oplossing bestaat er uit om een nieuw script te schrijven die niet de hele databank omzet naar xtures, maar slechts een fractie ervan. Hierdoor behouden we toch nog de voordelen van xtures. Het feit dat we onze xtures opzettelijk moesten beperken is zeker en vast een nadeel omdat we zo niet zeker zijn dat de tests conform zijn met de werkelijke data. In het ontwikkelingsteam van Rails hebben ze al meerdere malen met deze kritiek te maken gekregen en ze zijn nog steeds op zoek naar oplossingen of alternatieven. 57
5.2.12
Evaluatie van de iteratie
Het is van uitermate groot belang dat bij het voltooien van een iteratie, deze geëvalueerd wordt. evaluatie verloopt het best in samenspraak met de klant.
De
De klant kan zo tijdig ingrijpen wanneer de
applicatie dreigt te ontsporen, wat ook voordelig is voor de producent welke kosten bespaart door de reikwijdte van veranderingen te beperken tot de net voltooide iteratie. De evaluatie komt in feite neer op nagaan of de vooropgestelde functionaliteit verkregen is en het binnen de wensen van de klant valt. De evaluatie van deze iteratie verliep samen met dr. Kris De Schutter. De iteratie werd over het algemeen positief ontvangen en er dienden geen noemenswaardige aanpassingen doorgevoerd te worden. Dit is ook logisch, gezien deze iteratie amper werkelijk zichtbare functionaliteit bevat, wat uiteindelijk het enige interessepunt is van de klant. De nog vrij primitieve visualisatie werd als zeer positief ontvangen. Het is zeker een pluspunt om zo vroeg in de ontwikkeling al een resultaat, hoe eenvoudig ook, te kunnen tonen aan de klant. De iteratie werd afgewerkt op amper negen dagen, waarvoor de eer volledig voor het framework is. Hoewel we geen praktische ervaring hadden met Rails, blijkt het gebruik van conventies en vooral het consequent doorvoeren ervan al voor een hoge productiesnelheid te zorgen. Er werd wel steeds gekleurd binnen de lijnen van de niche-applicatie waarvoor Rails uitermate geschikt is. Het framework neemt dan een heleboel werk in eigen handen, waardoor er in deze iteratie eigenlijk zeer weinig code geschreven moest worden. In feite is negen dagen zelfs nog lang voor de hoeveelheid code, maar het gros van de tijd ging naar de testing en het vertrouwd raken met Rails. Er werd overeen gekomen dat de volgende iteratie om gebruikers zou gaan. Het gaat om basisoperaties op gebruikers dat toevoegen, verwijderen, editeren en bekijken omhelst. Bovendien werd gevraagd na te gaan of het mogelijk was om authenticatie en authorizatie van deze gebruikers te implementeren, want medische gegevens beveiligen is uiteraard een topprioriteit.
58
5.3 Iteratie 2: gebruikersbeheer, authenticatie en authorizatie 5.3.1
Doelstelling
Naast de data omtrent voeding is ook de data omtrent gebruikers even belangrijk in deze applicatie. Het is belangrijk dat deze informatie, zoals alle medische informatie goed beveiligd wordt tegen derden. Er moet dus met andere woorden gezorgd worden voor mogelijkheden om gebruikers te authenticeren om de applicatie te kunnen gebruiken. De volgende logische stap is de authenticatie om te vormen tot een authorizatiesysteem.
Hierbij is er
naast authenticatie ook nog een verschil tussen verschillende groepen van gebruikers. Zo kan de ene groep bepaalde delen van de applicatie zien terwijl de andere dit niet kan. Dit moet toelaten om in een latere iteratie bijvoorbeeld dokters toe te voegen die bepaalde delen van de applicatie kunnen gebruiken terwijl gewone patiënten dit niet kunnen.
5.3.2
Gebruikersbeheer: model
Als we ons eerst even rationeel focussen op het beheer van gebruikers, zien we dat de wensen van de klant hieromtrent in feite zeer eenvoudig zijn. Bekijken, toevoegen, aanpassen en verwijderen van gebruikers is praktisch niets meer dan de standaard CRUD operaties. We weten uit de vorige iteratie dat Rails hier zeer geschikt voor is. We consulteerden de klant nogmaals om te bepalen welke informatie allemaal moest opgeslagen worden over gebruikers. Dit is ook iets dat eigen is aan Agile Development, bij de minste twijfel over iets dient de klant zo snel mogelijk geraadpleegd te worden. Ontwikkelaars hebben vaak een andere visie dan de klant, en een kleine moeite nu spaart misschien veel werk later. Het resultaat is de tabel zoals te zien op guur 5.3. De attributen voor het wachtwoord worden verder uitgediept in sectie 5.3.6
Figuur 5.3: Tabel voor gebruikers
Het model voor deze tabel is analoog aan alle andere modellen die we al aangemaakt hebben. Het enige extra dat we voor het User model hebben gedaan is de implementatie van een
facade attribuut. Voor de
gebruikers van de modelklasse is dit een attribuut zoals de andere, maar in feite bestaat er geen persistent equivalent voor dit attribuut zoals voor de andere wel.
We hebben de techniek van facade attributen
gebruikt om ervoor te zorgen dat men aan een gebruikersmodel de leeftijd van die gebruiker kan vragen. Dit is gewone Ruby code zoals hieronder weergegeven, die toelaat om user.age te gebruiken op dezelfde manier als user.name.
59
1
def
2
age if
3
return
4
s e l f . birthdate ( ( Date . t o d a y
−
s e l f . b i r t h d a t e ) / 3 6 5 . 2 5 ) . to_i
end
5
return
6
end
nil
Codevoorbeeld 38: Implementatie van een facade attribuut (age)
5.3.3
Gebruikersbeheer: controller en views
Nu het model geïmplementeerd is, kunnen we de CRUD operaties implementeren door de controllers en views aan te maken. Aangezien het echter gaat om een tabel zonder relaties en we op dit moment enkel en alleen CRUD functionaliteit nodig hebben, biedt een scaold alles wat we momenteel vereisen via slechts één commando. Scaolds zijn werkelijk een handig hulpmiddel daar zowat elke webapplicatie nood heeft aan CRUD operaties, die nu geheel automatisch geïmplementeerd worden. Scaolds genereren makkelijk uitbreidbare controllercode die we zelf niet anders zouden geïmplementeerd hebben, maar de views zijn vrij eenvoudig. Dit is ook logisch aangezien de scaolds alle mogelijke modellen moeten kunnen ondersteunen en de view dus zo algemeen mogelijk moet zijn. Hoewel het geen vereiste is voor deze iteratie, zal de klant het des te meer apprecieren als het resultaat van deze iteratie in een mooier jasje gestoken wordt. Dé standaard voor de opmaak van webpagina's is W3C Cascading Style Sheets (CSS). Zonder dieper in te gaan op CSS, is het voordeel van CSS de scheiding van opmaak en inhoud.
Concreet betekent dit
dat de inhoud gegenereerd door de Rails applicatie opgemaakt wordt door regels vastgelegd in minstens één CSS bestand. Hoewel een goede layout zeer belangrijk is voor onze applicatie, wilden we er niet te veel tijd insteken omdat het ons zou aeiden van Rails. Een degelijke CSS-layout maken kost namelijk aanzienlijk wat tijd. In vele bedrijven hebben ze naast ontwikkelaars daarom ook team van designers die gespecialiseerd zijn in zulke zaken. We hebben daarom beroep gedaan op de artistieke kunsten van Luka Cvrk (http://www.solucija.com/) die zijn sobere doch elegante CSS-template vrijgegeven heeft via Open Source Web Design (http://www.oswd.org/) onder een open licentie. Rails maakt het zeer eenvoudig via één declaratie om een bepaalde CSS template te gebruiken in de views. Het zou echter on-DRY zijn, moesten we de declaratie in elke view moeten herhalen. Rails lost dit mogelijk door zogenaamde
layouts toe te laten. Een layout is een gewone RHTML le, die echter het
keyword yield ergens op de pagina bevat. Een layout heeft een vaste plaats in de Rails directorystructuur en zal automatisch voor elke view gebruikt worden, tenzij expliciet opgegeven wordt om dit niet te doen. Op de plaats waar yield staat, wordt bij het renderen dan de output van de view gezet. Op deze manier kunnen we de algemene opmaak van onze pagina's vastleggen in de layout en hoeven de views hier geen rekening mee te houden. De view behorende bij een bepaalde controllermethode is dus meestal zeer kort en aanpassingen aan de layout voor elke pagina dienen slechts op één plaats te gebeuren. Door de CSS te includeren in de layout respecteren we het DRY principe en bekomen we een zeer elegante scheiding van opmaak, layout en views. De functionaliteit van vorige iteratie brachten we onder in de FoodController, die van deze paragraaf onder de AdminController, daar we van plan zijn om later enkel administrators gebruikersbeheer te laten doen. De scaolds uit vorige iteratie werden aangepast om samen te werken met de layout. 60
Figuur 5.4 toont bijvoorbeeld de lijst van gebruikers, gegegeneerd door de methode list_users van de
AdminController.
Om het geheel wat op te vrolijken hebben we tevens gebruik gemaakt van de open
source Silk iconen set
1 zoals te zien is op de guur.
Figuur 5.4: Screenshot: lijst van gebruikers
Tot slot hebben we nog één techniek gebruikt, welke we nog niet besproken hebben. Dankzij het gebruik van layouts wordt de view code al aanzienlijk gereduceerd, maar het kan nog beter. Naast de layoutcode (die echter voor alle views gelijk is), is de code voor de screenshot op guur 5.4 werkelijk zeer klein:
1
2
U s e r s
3
<%= r e n d e r ( : p a r t i a l
4
( total
= <%= @ u s e r s . l e n g t h =>
' user ' ,
%>)
: collection
=> @ u s e r s ) %>
t a b l e >
Codevoorbeeld 39: View-code voor de lijst van gebruikers in de databank
De tweede lijn is voor de blauwe titelbalk. De derde lijn zet alle gebruikers op het scherm door gebruik te maken van een
partial template (ook partials genoemd). Partials zijn RHTML fragmenten die kunnen
ingevoegd worden in andere RHTML views en die mogelijks parameters meekrijgen. Hun hoofddoel is om de view logica eenvoudiger te maken. Zoals de declaratie aangeeft wordt in het voorbeeld de collectie van gebruikers meegegeven als parameter aan de partial. De partial zelf ziet er als volgt uit:
1 http://www.famfamfam.com
61
1
2
<%= i m a g e _ t a g ( ' u s e r . png ' ,
3
<%= u s e r . name %>
4
<%= u s e r . r e a l _ n a m e %>
5
a g e : <%= u s e r . a g e %>
6
: size
=>
' 1 6 x 1 6 ' ) %>
. . . . . .
Codevoorbeeld 40: Partial template voor de visualisatie van een gebruiker
We hadden even goed geen partials kunnen gebruiken, de output zou er net hetzelfde uitzien. Partials hebben echter het voordeel dat ze extreem herbruikbaar zijn.
Vele webapplicaties tonen vaak dezelfde
informatie over bepaalde objecten op verschillende pagina's. Door gebruik te maken van dezelfde partials wordt duplicatie vermeden, een uiting van DRY dus.
Partials kunnen het best beschouwd worden als
een soort van subroutine die kan opgeroepen worden uit eender welke template.
Zoals te zien in het
voorbeeld zijn partials zeer geschikt voor het renderen van collecties van objecten. Partials kunnen ook gebruikt worden in controllers, waar ze vooral een rol spelen bij het gebruik van Ajax, waar we in de volgende iteratie dieper op in gaan.
Partials zijn een eenvoudig, maar toch zeer krachtig mechanisme.
Het laat werkelijk toe de view te ontkoppelen en hergebruik te stimuleren, wat niet zo een evidentie is in omgevingen die met HTML dienen te werken.
5.3.4
Agile Development: Rails, architectuur en consistentie
Zoals de losoe van Agile Development voorschrijft, zijn we vertrokken zonder een initieel architectuurontwerp.
Door echter enkel de conventies voorgesteld door Rails te volgen, heeft zich al een soort van
architectuur ontwikkeld voorgesteld op guur 5.5. Deze architectuur is in wezen niet meer dan het klassieke MVC, maar geforceerd toegepast door Rails. Hier is niets mis mee, deze architectuur is degelijk en zeer modulair, wat steeds een pluspunt is naar onderhoud en teamwerk toe. Een kanttekening die we er echter nog dienen bij te plaatsen, is die van consistente keuzes.
In Agile
Development is het belangrijk om consistent best practices te volgen en deze ook toe te passen.
We
hadden bijvoorbeeld kunnen kiezen om het gebruiksbeheer in te voegen bij de vorige controllerlogica. Dan volgen we nog steeds de Rails conventies, maar het mag duidelijk zijn dat hier het separation of concerns principe allesbehalve is toegepast.
Laat het duidelijk zijn dat men in Agile Development
misschien meer gevaar loopt om inconsistent te werken, omdat men zich steeds beperkt tot de scope van de iteratie. In ons geval blijkt dat nog mee te vallen, maar gesteld dat men in teamverband werkt, lijkt ons dat niet zo een evidentie. Het lijkt ons ook van groot belang dat ontwikkelaars die Agile ontwikkelen, goed op de hoogte moeten zijn van design patterns en best practices.
In Agile Development is het
namelijk vaak makkelijker om aan Cowboy Coding te doen om iets snel te laten werken, maar dat dit intern een puinhoop is. Daar waar men in traditionele ontwikkelingsmethoden een architectuurontwerp rekening houdt met onderhoudbaarheid en toekomstbestendigheid, is dit bij Agile Development moeilijker te behalen zonder een gezonde portie vastberadenheid. Geconcludeerd vereist Agile Development dus een zekere discipline en ervaring in applicatie-ontwerp van de ontwikkelaar.
62
Figuur 5.5: MVC architectuur op dit moment in de ontwikkeling
5.3.5
Persistentie van het wachtwoord
Zoals te zien op guur 5.3 op bladzijde 59, slaan we niet gewoon het wachtwoord op.
Indien we de
wachtwoorden van de gebruikers zouden opslaan als gewone tekst in de databank, zou het kunnen dat malade gebruikers deze wachtwoorden kunnen achterhalen door gebruik te maken van fouten in de implementatie (bijvoorbeeld door SQL injection). Om dit te vermijden, maken we gebruik van de techniek die een wachtwoord salt en hashed. Het User model wordt hiertoe aangevuld met een facade attribuut password waarvoor we volgende setter deniëren:
1
def
2
s e l f . password_salt = random_string ( 2 5 5 )
3 4
p a s s w o r d =( p a s s ) s e l f . password_hash = User . e n c r y p t ( p as s ,
s e l f . password_salt )
end
5 6
def
7
8
s e l f . encrypt ( pass ,
salt )
D i g e s t : : SHA256 . h e x d i g e s t ( p a s s +
salt )
end
Codevoorbeeld 41: Setter methode voor het facade attribuut password
In plaats van het wachtwoord te persisteren, genereren we op regel twee een random string (de salt) en wordt de concatenatie van het werkelijke wachtwoord en de salt gehashed via de SHA256 encryptie techniek (regel drie). SHA256 is een encryptiestandaard die vaak gebruikt wordt in de industrie. De reden waarom dit werkt is vrij wiskundig, maar bestaat eruit dat het nemen van de hashfunctie van eenzelfde string steeds dezelfde hash oplevert, maar dat het onmogelijk blijkt om uit de hash de oorspronkelijke string te halen. Eenvoudig gezegd zal de hashfunctie van een wachtwoord steeds dezelfde hash bekomen,
63
maar zal vanuit de hash het wachtwoord niet gereconstrueerd kunnen worden. Door het wachtwoord eerst nog eens te salten met een random string vermijden we dat zwakke wachtwoorden gemakkelijk gekraakt worden.
Uiteraard dienen we de salt wel te persisteren om later de hash te kunnen berekenen, maar
hiermee is een kraker praktisch niets, omdat deze ook niet weet hoe deze salt gecombineerd wordt met het wachtwoord. Indien we nu willen nagaan bij het inloggen of een gebruiker correcte gegevens ingeeft, dient het ingegeven wachtwoord geconcateneerd te worden met de salt uit de databank en hiervan de hash te berekenen. Indien de bekomen hash en de hash in de databank identiek zijn, zijn de logingegevens correct. Onderstaande code toont hoe dit principe zich vertaalt in Railslogica:
1
# Geeft
de
gebruiker
terug
indien
correcte
logingegevens ,
anders
nil 2
def
s e l f . a u t h e n t i c a t e ( username ,
3
user =
4
if
user
pass )
s e l f . find_by_name ( u s e r n a m e ) !=
nil
and
u s e r . password_hash
!=
User . e n c r y p t ( pass ,
user . password_salt ) 5
user =
6 7
8
nil
end return
user
end
Codevoorbeeld 42: Authenticatie van een gebruiker
5.3.6
Authenticatie
De volgende stap bestaat er nu in om er voor te zorgen dat niet eender wie toegang heeft tot de applicatie. Authenticatie komt er op neer dat voor bepaalde delen te bekijken de gebruiker correcte credentials moet opgeven. Er bestaan tal van plugins voor het Rails framework om authenticatie te verwezenlijken, zoals
2
de act_as_authenticated plugin , maar we hebben er voor gekozen om het authenticatiesysteem zelf te schrijven. We doen dit omdat we zo een perfect zicht hebben op de beveiliging van de medische gegevens, waar met plugin de controle veel lager ligt.
Had het niet om medische gegevens gegaan, hadden we
hoogstwaarschijnlijk om tijd te besparen geopteerd voor een plugin. Voor de implementatie van het authenticatiesysteem baseren we ons op de mechanismen zoals beschreven door Chad Fowler en Tom Moertel in [4].
Het grootste probleem dat we dienen te overbruggen is de
inherente toestandsloosheid van het web, wat zou betekenen dat op iedere nieuwe of ververste pagina de gebruiker opnieuw zou moeten authenticeren. Dit is uiteraard onaanvaardbaar, maar dit kunnen we makkelijk oplossen door gebruik te maken van sessies en lters (lters werd reeds besproken in sectie 3.4.3). Per gebruiker is er namelijk juist één sessie-object, dat in feite niets meer is dan een container voor allerhande soorten informatie. Rails zorgt achter de schermen ervoor dat een inkomende request, die in feite niet kan onderscheiden worden van een willekeurig andere, gelinkt wordt met het juiste sessie-object. De manier waarop Rails dit doet, is door gebruik te maken van een client-side bestand (cookie) met een unieke id die mee wordt gestuurd met de request. Om het sessie-object aan serverzijde op te slaan, is er een scala aan mogelijkheden van bestandsgebaseerd tot oplossingen met distributed Ruby. We hebben gekozen voor de ActiveRecordStore manier, waarbij de
2 http://wiki.rubyonrails.com/rails/pages/Acts_as_authenticated 64
sessie-objecten opgeslagen worden in de databank. De reden voor deze keuze is dat deze oplossing makkelijk schaalt naar meerdere Rails servers toe en niet al te complex is. De sessie-objecten zelf steunen op het gebruik van een unieke identicatiecode en cookies aan clientzijde. De werking van authenticatiesysteem wordt voorgesteld op guur 5.6.
Figuur 5.6: Authenticatie
Omdat de logica voor het inloggen niet behoort tot de Admin- of FoodControllers hebben we een aparte controller (LoginController ) aangemaakt met daarin de login en logout methoden. Gesteld dat de gebruiker de normale weg volgt en via de login pagina van de login methode probeert toegang te krijgen tot de applicatie kunnen we volgende stappen onderscheiden (zie guur 5.6).
• Stap 1:
de gebruiker vraagt via de url /login de login pagina op. Indien de corresponderende login
methode een HTTP GET binnenkrijgt, zal enkel de view gerendered worden (zie code stap 2).
• Stap 2:
de gebruiker geeft zijn gebruikersnaam en wachtwoord in via het formulier in de view van
stap één. Onderstaande code toont de logica voor de login methode en wordt verder besproken in de volgende stappen. De request.post? declaratie op regel drie gaat na of het om een HTTP-POST bericht gaat. Indien het een HTTP-GET bericht zou zijn, wordt de conditionele code niet uitgevoerd en wordt gewoon de view gerendered. Deze view toont een loginpagina met een loginformulier. Indien gegevens worden doorgegeven via dat formulier, zal het zoals standaard is voor HTML formulieren via een HTTP-POST bericht verstuurd worden naar dezelfde login methode.
Deze werkwijze is
typisch voor Rails controllermethoden, om de hoeveelheid acties te beperken zonder complex te worden.
65
1
def
login
2
session [ : user ]
3
if
=
nil
request . post ?
4
u s e r = U s e r . a u t h e n t i c a t e ( params [ : u s e r n a m e ] ,
params [ :
password ] ) 5
if
!=
nil
session [ : user ]
7
redirect_to ( : controller
8
= user . id =>
' food ' ,
: a c t i o n => ' i n d e x ' )
end
9 10
user
6
end end
Codevoorbeeld 43: Loginmethode van de LoginController
• Stap 3:
mogelijks ingelogde gebruikers op dezelfde client worden verwijderd (lijn twee).
• Stap 4:
De logingegevens worden gecontroleerd (lijn vier) door middel van de methode zoals be-
sproken in sectie 5.3.5.
Indien de logingegevens correct blijken, wordt de id van de gebruiker in
het sessie-object gestoken en wordt de gebruiker doorgestuurd (redirect op regel zeven naar een indexpagina).
• Stap 5:
Indien de logingegevens incorrect blijken te zijn, wordt de loginpagina herladen met een
foutmelding (niet getoond in code). Dit gebeurt doordat de view van de login methode automatisch gerendered wordt op het einde van de methode.
Merk tevens op dat het met deze code perfect
mogelijk is dat een gebruiker op verschillende fysieke systemen ingelogd kan zijn met dezelfde inloggegevens.
We dienen echter ook nog te voorzien dat een gebruiker een willekeurige URL kan intypen en zodoende de authenticatie omzeilen. Dit kan opgelost worden door gebruik te maken van het sessie-object en lters. De rechterzijde van guur 5.6 toont de werking aan.
• Stap a:
een gebruiker geeft een willekeurige URL in.
Aangezien een URL overeenkomt met een
bepaalde controller en methode, zal de request dus eerst de ApplicationController passeren daar dit de superklasse is van alle controllers.
• Stap b:
zoals in onderstaande code te zien is, deniëren we in de ApplicationController een before-
lter (die dus wordt opgeroepen voor elke methode) die nagaat of de gebruiker de pagina mag bekijken. Merk op dat enkel de login en logout pagina geen invloed ondervinden van de lter op regel twee).
66
1
class
2
ApplicationController <
before_filter
A c t i o n C o n t r o l l e r : : Base
: check_authentication , : except
= >[: l o g i n ,
: logout ]
3 4
def
5
check_authentication
unless
session [ : user ]
6
redirect_to
7
return
8
of
=>
' login ' ,
filter
chain
: a c t i o n => if
' login '
redirect
fails
end
9 10
: controller
f a l s e # Break
end end
Codevoorbeeld 44: Authenticate verloopt via een lter in de ApplicationController
• Stap c:
De lter gaat voor iedere methode na of er in de sessie een gebruikers-id zit.
• Stap d:
Indien dit niet zo is, wordt de gebruiker doorgestuurd naar de loginpagina.
• Stap e:
Indien wel, mag de gebruiker de gewenste pagina bekijken. De lter heeft dan geen eect.
Met bovenstaand mechanisme zijn we verzekerd dat onze webapplicatie alvast beschermd is tegen ongeregistreerde gebruikers.
Filters zijn bij de implementatie een prachtig hulpmiddel gebleken om het
DRY principe te blijven hanteren. Hadden we immers geen lters gehad, dienden we misschien in iedere methode een oproep naar check_authentication te plaatsen.
5.3.7
Authorizatie
Het authenticatiesysteem uit sectie 5.3.6 kan eenvoudig en met dezelfde mechanismen uitgebreid worden om naast authenticatie ook authorizatie van gebruikers toe te laten.
Authorizatie komt neer op een
onderscheid maken tussen groepen van gebruikers, waarbij de ene groep meer of minder mogelijkheden heeft dan andere. Om authorizatie te implementeren, maken we gebruik van het klassieke rollen-rechten systeem zoals beschreven in [4]. Op het laagste niveau zijn er rechten, waarbij een recht de toegang tot een bepaald gedeelte van de applicatie voorstelt. We hebben geopteerd om de authorizatie toe te passen op het niveau van controllers. Dit betekent dat een recht bijvoorbeeld het recht op gebruik van de FoodController voorstelt. We zullen de applicatie opdelen in een aantal modules (AdminModule, DieetModule) zodanig dat de functionaliteit van de module overeenkomt met die van een controller en we dus in feite rechten toekennen op het gebruik van modules. Op het volgende niveau zijn er de rollen. Een rol is in feite een benoemde collectie van rechten, die op het hoogste niveau worden toegekend aan een gebruiker. Zo kunnen we de rol 'Administrator' met rechten op alle modules aanmaken en deze toekennen aan een gebruiker. Deze gebruiker zal dan toegang hebben tot de gehele applicatie.
Een gebruiker is niet beperkt tot één rol, maar kan zoveel rollen hebben als
nodig. Uiteraard dienen we deze informatie persistent bij te houden in de databank. De structuur van de tabellen is te zien op guur 5.7. De implementatie van de tabellen werd wederom uitgevoerd via een migratie. De tabellen van guur 5.7 voldoen allen aan de Rails conventies. Een conventie die voorheen nog niet gebruikt is de conventie van de pure jointabellen (foodcontents was geen pure jointabel maar ook een 67
Figuur 5.7: Tabellen voor authorizatiesysteem
entiteit op zich, hier is de tabel enkel aanwezig om een veel-veel relatie uit te drukken). Zowel roles_users en rights_roles hebben ten eerste geen primaire sleutel en hebben enkel en alleen vreemde sleutels als attribuut. In Rails conventies betekent dit dat het hier niet gaat om een domeinentiteit en er dus ook geen overeenkomstig model voor bestaat. Ten tweede is de naamgeving wederom niet toevallig. De naam voor een jointabel voor de relatie tussen twee tabellen in Rails is samengesteld uit de naam van de eerste en de tweede tabel, gescheiden door een underscore en alfabetisch geordend. Indien we deze conventies volgen zullen we na de implementatie van de migratie nooit meer te maken krijgen met de jointabellen. Rails zal achter de schermen de hele logica regelen, zoals bijvoorbeeld automatische join operaties. Nadat we in de modelklassen de relatiedeclaraties hebben ingevoerd, kunnen we bijvoorbeeld de rollen van een gebruiker opvragen (user.roles) alsof het om gewone attributen gaat. Hetzelfde geldt uiteraard voor de rechten van een rol. Rails maakt dus het vaak niet zo triviale werken met jointabellen wel zéér eenvoudig door zoals gewoonlijk de juiste conventies te volgen.
Doorheen de vorige en deze iteratie is het nu al meermaals
gebleken dat het Convention over Conguration principe in Rails veelvuldig kan gebruikt worden, maar tegelijk ook voor een grote productiviteitswinst zorgt. Het nagaan of een gebruiker recht heeft op het gebruik van een bepaalde controller is pure business logica. We brengen dit dan ook onder in de User model klasse. Om echter toekomstige aanpassingen aan het authorizatiesysteem aan te kunnen, zullen we de beslissing delegeren naar de rollen. Op die manier schermen we het User model af van de kennis van rechten en zijn de modellen niet rechtstreeks gekoppeld. De logica staat hieronder weergegeven. Dit is eenvoudige Ruby code die alle rollen overloopt en detecteert of één van de rollen de juiste rechten heeft. Zoals te zien is, wordt de beslissing inderdaad doorgegeven naar het Role model.
1
def
2
3
has_right_for ?( controller_name ) roles . detect {
| role |
r o l e . has_right_for ?( controller_name )
}
end
Codevoorbeeld 45: Controle of een gebruiker een juiste rol heeft
Om de modellen nog minder te verweven in elkaar, wordt ook in het Role model de beslissing doorgegeven naar het lager niveau. Op deze manier kunnen we de implementatie van het rechtensysteem later wijzigen (bijvoorbeeld naar methodeniveau in plaats van controllerniveau) zonder dat de bovenliggende lagen er last van zullen hebben:
1
def
2
3
has_right_for ?( controller_name ) rights . detect {
| right |
r i g h t . has_right_for ?( controller_name )
end
}
Codevoorbeeld 46: Controle of een rol het juiste recht heeft
68
Op het laagste niveau, het niveau van de rechten, is de beslissing triviaal. Er dient enkel nagegaan te worden of de controller meegegeven als parameter gelijk is aan de controller in de databank voor dat recht:
1
def
2
3
has_right_for ?( controller_name ) return
c o n t r o l l e r == c o n t r o l l e r _ n a m e
end
Codevoorbeeld 47: Controle of een recht toegang geeft tot het gebruik van een controller
De implementatie van authorizatie is nu triviaal. Op geheel analoge wijze als het authenticatiesysteem in sectie 5.3.6 kunnen we nagaan of de huidige gebruiker de nodige rechten heeft om de gewenste module te gebruiken. We maken wederom gebruik van een before_lter in de ApplicationController die de hierboven beschreven logica gebruikt.
De code staat hieronder weergegeven, maar toont geen nieuwigheden ten
opzichte van authenticatie. Merk op dat we authenticeren voor authorizatie, wat logisch is. Merk ook op dat de lters false teruggeven bij faling.
Dit is nodig om de ketting van lters af te breken, zodat
bijvoorbeeld geen authorizatie meer wordt gedaan nadat de authenticatie mislukt is.
1 2
class
ApplicationController <
before_filter
3
6
: check_authentication , : check_authorization ,
4 5
A c t i o n C o n t r o l l e r : : Base
: e x c e p t =>
[ : login ,
def
check_authorization
7
u s e r = User . f i n d ( s e s s i o n [ : u s e r ] )
8
unless
9
= "You
redirect_to ( : controller
11
return
f a l s e # Break
are
not
authorized
=> " f o o d " ,
filter
to
view
this
page "
: a c t i o n => " i n d e x " )
chain
end
13
end
14
end
user . has_right_for ?( s e l f . c l a s s . controller_path )
f l a s h [ : message ]
10 12
: logout ]
protected
Codevoorbeeld 48: Authorizatie via een lter in de ApplicationController
Vervolgens hebben we enkele standaard rechten, rollen en gebruikers aangemaakt via een eenvoudige datamigratie. We implementeerden alle CRUD operaties voor rechten en rollen in de AdminController. We pasten de view aan om de nieuwe functionaliteit aan te bieden. Hier is toch een drietal dagen werk in gegaan, maar daar dit alles niets nieuws aanbrengt in de discussie, gaan we dit hier niet verder uitwerken. De code is steeds te inspecteren op bijgeleverde CD-ROM bij deze scriptie.
5.3.8
Functional Testing
In de vorige iteratie hebben we ons toegespitst op unit testing, aangezien het bestaan van de toenmalige controllers en corresponderende views toen nog onzeker was. Deze beslissing is opportuun gebleken, daar we in deze iteratie de controllers hebben herschreven en een echte view hebben ontwikkeld. Tests zijn in zekere mate afhankelijk van implementatie en zijn dus niet sterk in het opvangen van verandering. In software-ontwikkeling en in Agile Development in het bijzonder kan men nooit voor 100% zeker zijn van 69
toekomstige veranderingen, maar we menen dat deze controllers en views in grote lijnen toch de nale versie zullen halen. In deze iteratie hebben we vooral controllercode geschreven, die we dan ook willen getest zien. Controllers dirigeren het orkest: ze ontvangen een request van een gebruiker, halen data op via modelobjecten en beantwoorden de aanvraag door de corresponderende view aan te roepen. Het testen van controllers is dus van een geheel andere soort dan het testen van de modellaag. Daar waar voor modellen de business logica dient getest te worden, moet voor controllers nagegaan worden of een bepaalde request het juiste antwoord als resultaat heeft. Het testen van controllers gebeurt in Rails door middel van zogenaamde functionele testen (Functional Tests). We hebben voor de nieuwe modellen (User, Role & Right) ook voldoende unit tests geïmplementeerd. We hebben echter Unit Tests reeds besproken in sectie 5.2.9, waardoor we deze hier niet meer behandelen. Waar Unit Tests de logica testen vanuit het standpunt van de ontwikkelaar, kunnen functionele testen eerder gezien worden vanuit het standpunt van de gebruiker. Aangezien de controllerfunctionaliteit voor de gebruiker bereikbaar is via URL's moet een soortgelijke aanpak vanuit het testing framework mogelijk zijn.
Het Rails testing framework biedt voor functionele testen alle mogelijkheden van unit tests aan.
Dit betekent dat functionele tests ook gebaseerd zijn op asserties, een setup en xtures. de functionele testen zijn methoden die HTTP-berichten emuleren (get, post, etc.)
Extra voor
en de specieke
controllerasserties. Voor de drie controllers schreven we voldoende functionele testen om ons ervan te verzekeren dat de controllers correct werken. Onderstaand codevoorbeeld toont de functionele test voor de creatie van een gebruiker. Op regels dertien en negentien is duidelijk te zien dat een HTTP-bericht geëmuleerd wordt: de parameters die anders zouden verzameld worden via een HTML-formulier worden nu rechtstreeks doorgegeven via methoden van het testframework. Het voorbeeld toont ook duidelijk aan dat alle asserties van unit tests gebruikt kunnen worden (assert_equal bijvoorbeeld), maar er ook specieke asserties voor functionele tests zijn (assert_response bijvoorbeeld). Merk op dat we in de setup-methode een ingelogde gebruiker simuleren. Aangezien de loginmethode getest wordt in de functionele test voor de LoginCon-
troller, kunnen we in deze test er van uitgaan dat het inloggen correct verloopt.
70
1
def
setup
2
@ c o n t r o l l e r = A d m i n C o n t r o l l e r . new
3
@ r e q u e s t = A c t i o n C o n t r o l l e r : : T e s t R e q u e s t . new
4
@ r e s p o n s e = A c t i o n C o n t r o l l e r : : T e s t R e s p o n s e . new
5
# Fake
6
@admin = U s e r . find_by_name ( ' admin ' )
7 8
the
login ,
by
manually
@request . s e s s i o n [ : user ]
setting
the
session
= @admin . i d
end
9 10 11
def
test_create_user
# 1)
correct
user
12
nr_of_users = User . count
13
post
14
: create_user ,
: u s e r => { : name => ' so me _us er ' ,
: p a s s w o r d => ' e e n w a c h t w o o r d ' ,
15
assert_response
: success
16
assert_template
' list_users '
17
assert_equal
18
# 2)
19
post
nr_of_users + 1 ,
incorrect
: create_user ,
20 21 22 23
user
( password
= >[1]
}
User . count too
short )
: u s e r => { : name =>
: p a s s w o r d => assert_response
: role_ids
' blaat ' ,
' incorrect_user ' , : role_ids
= >[1]
}
: redirect
assert_redirected_to
: a c t i o n =>
' add_user '
end
Codevoorbeeld 49: Setup en testmethode van de functionele test voor AdminController
Agile Development draagt het omgaan met veranderingen hoog in het vaandel.
Tests zijn echter vrij
gevoelig voor veranderingen, daar ze noodzakelijk dicht bij de implementatie aanleunen. Veranderingen die problemen introduceren die voorheen ongekend waren, zullen dus zeker invloed hebben op de tests. In deze iteratie hebben we de modellen uit vorige iteratie licht aangepast op sommige plaatsen om consistent te zijn met naamgevingen. Door de tests opnieuw uit te voeren en na te gaan of er geen foutmeldingen ontstaan , zijn we verzekerd dat de veranderingen geen invloed hebben gehad op de functionaliteit. Hoe groter de applicatie en hoe meer iteraties, hoe belangrijker dit wordt. Enig heikel punt blijft dan nog dat men nooit zeker weet dat men alles getest heeft. Maar gesteld dat geautomatiseerd testen niet bestond, zou men na iedere iteratie alles manueel moeten testen, plus dat men nog steeds niet mag vergeten om alle gevallen te testen.
5.3.9
Integration Testing
Zoals Rails core ontwikkelaar Jamis Buck opmerkt in [2], bevatten zowel unit als functionele tests een inherent nadeel:
één model.
•
Unit tests zijn gefocust op het testen van
•
Functionele tests hebben als doel het testen van
één controller en de gebruikte modellen.
Maar wat met bugs die ontstaan door interacties over verschillende controllers heen?
Wat als bugs
veroorzaakt worden doordat twee of meer concurrente gebruikers inwerken op dezelfde data? De oplossing
71
hiervoor werd geleverd in versie 1.1 van het Rails framework met de toevoeging van integratie tests (Integration Tests). Merk op dat integratie testen geen vervanging vormen voor unit en functionele tests. Integratie tests opereren op een hoger niveau en zijn pas zinvol wanneer de functionaliteit apart grondig getest is in de unit en functionele tests. Integratie tests bevatten dezelfde mogelijkheden als beide op het vlak van asserties, HTTP-emulatie en xtures. Nieuw in integratie tests is de mogelijkheid om verschillende sessies (niet te verwarren met het sessie-object) op te starten die de interactie met applicatie door verschillende concurrente gebruikers voorstellen. Weinig van de door ons gekende testframeworks bieden heden soortgelijke functionaliteit aan. We hebben geen integratie tests zoals hier beschreven geïmplementeerd om redenen die duidelijk zullen worden in sectie 5.3.10. Als dusdanig zullen we hier ook geen articieel voorbeeld tonen. In principe is er praktisch bijna geen verschil ten opzichte van functionele tests, behalve het gebruik van de sessies en het testen van meerdere acties in een testmethode. Zeer goede codevoorbeelden kunnen terug gevonden worden in [17] en [2].
5.3.10
Scenario Testing
Integratie tests zijn uitermate handig om customer requirements te testen. Vaak zijn deze requirements gegeven in de vorm van use-case scenario's na het uitvoeren van een functionele analyse. Beschouwen we bijvoorbeeld volgend mogelijk scenario:
Een administrator logt in. De pagina die hij nu te zien krijgt, is die van het overzicht van de categorieën van voedsel. Wanneer hij op de 'Admin Module' link klikt, ziet hij de index pagina van deze module. Vervolgens klikt hij op de link 'add user' en voegt een nieuwe gebuiker toe. Hij logt dan uit.
Dit scenario is perfect te implementeren en testbaar met een integratietest daar het gaat over verschillende controllers heen.
Bij de implementatie hadden we echter het gevoel dat we vaak tests uit de unit en
functionele tests aan het herhalen waren, behalve nu na elkaar. Doordat integratietests daarbij ook vrij lang zijn (net doordat ze verschillende controllers en modellen testen) lijken ze allerminst nog op de scenario's waar van vertrokken werd, maar zijn ze eerder een te lange opsomming van asserties en is het geheel moeilijk onderhoudbaar door de onoverzichtelijkheid. De oplossing, aangegeven door Mike Clark in [17], bestaat eruit gebruik te maken van Ruby's aangeboren capaciteit om eenvoudig DSL's te implementeren (zie sectie 2.4). De techniek die we hiertoe gebruiken is het at runtime uitbreiden van objecten, zoals beschreven in sectie 2.3.7. Voor we dieper ingaan op het implementeren van de DSL, beschouwen we eerst het eindresultaat:
72
1 2
class
AdminUseCasesTest <
include
ActionController : : IntegrationTest
AdminUseCasesDsl
3 4
FILIP = {
5
: name =>
' Filip ' ,
: p a s s w o r d =>
: r e a l _ n a m e =>
' pilif123 '
' Filip
Blondeel ' ,
}
6 7
def
test_admin_logs_in_and_adds_user
8
bram = admin
9
bram . g e t
' / nephrology '
10
bram . i s _ v i e w i n g ( ' l o g i n / l o g i n ' )
11
bram . l o g s _ i n _ w i t h ( ' admin ' ,
12
bram . i s _ r e d i r e c t e d _ t o ( ' f o o d ' ,
13
bram . c l i c k s _ o n _ l i n k _ t o ( ' admin / i n d e x ' )
14
bram . i s _ v i e w i n g ( ' admin / i n d e x ' )
15
bram . c l i c k s _ o n _ l i n k _ t o ( ' admin / a d d _ u s e r ' )
16
bram . i s _ v i e w i n g ( ' admin / a d d _ u s e r ' )
17
bram . a d d s _ u s e r ( FILIP )
18 19 20
' admin123 ' ) ' show_meal ' )
bram . l o g s _ o u t end end
Codevoorbeeld 50: Uitwerking van een scenario met de eigen gedenieerde DSL
Het bekomen resultaat is een zeer goed voorbeeld van een DSL en is op zijn minst indrukwekkend eenvoudig te noemen. In de eerste plaats valt de zeer hoge leesbaarheid en bijhorende onderhoudbaarheid op. Het is bijna geen programmacode meer, maar het lijkt al op spreektaal. Merk op dat het hier wel degelijk om tests gaat. Elke regel hierboven is in feite een vermomming voor een heleboel asserties. De sleutel om zulke functionaliteit op zo'n hoog niveau te kunnen bekomen ligt in de eerste coderegel van de testmethode. De 'admin' declaratie is in feite een methode-oproep die een sessie teruggeeft en waarin at runtime de mogelijkheden van deze sessie worden aangemaakt. Anders gezegd, het object 'bram' is een sessie, waar gebruikersachtige methoden voor gedenieerd zijn. De 'admin' declaratie wordt in een aparte le gedeclareerd, die ingemixt wordt in de testklasse (tweede regel). Dit klinkt misschien vreemd, maar de code van de module 'AdminUseCasesDsl' (waarvan hieronder slechts een relevant stuk getoond) maakt veel duidelijk.
Wanneer we 'bram = admin ' schrijven, zien we dat
achter de schermen een nieuwe sessie wordt gestart (open_session ).
Vervolgens deniëren we op deze
sessie at runtime een methode (admin.adds_user ) waarachter de normale asserties en operaties zitten. De methodes van deze gemodiceerde sessie (bram ) kunnen dan gebruikt worden in de oorspronkelijke testklasse (zoals logs_in_with, is_viewing, etc).
73
1 2
module def
AdminUseCasesDsl admin
3
open_session
4
def
do
| admin |
admin . a d d s _ u s e r ( u s e r _ d e t a i l s )
5
nr_of_users = User . count
6
post
7
assert_equal
8
assert_not_nil
9
assert_response
10
' n e p h r o l o g y / admin / c r e a t e _ u s e r ' , nr_of_users + 1 ,
: u s e r => u s e r _ d e t a i l s
User . count
U s e r . find_by_name ( u s e r _ d e t a i l s [ : name ] ) : redirect
assert_redirected_to
: controller
=>
' admin ' ,
: a c t i o n =>
'
list_users ' 11
end
12
...
Codevoorbeeld 51: Denitie van een DSL declaratie voor de scenario tests
Aangezien we nog steeds bezig zijn met integratietesten kan de bekomen DSL dus ook concurrent gebruikt worde, waardoor zelfs de meest complexe scenario's kunnen uitgewerkt worden.
Hieronder staat een
voorbeeld van hoe bovenstaande code in concurrente versie er uitzien. Dankzij het sessie-mechanisme is dit amper extra werk.
1
def
2
bram = admin
3
kris
4
bram . g e t
5
bram . l o g s _ i n _ w i t h ( ' bram ' ,
6
k r i s . get
7
test_with_2_admins
8
= admin ' / nephrology ' ' bram123 ' )
' / neprhology '
. . . . . . . end
Codevoorbeeld 52: Unit test voor het controleren van de callback in vorig voorbeeld
Hoewel deze uitleg niet echt meer om Rails ging, vonden we dit zeker en vast vernoemenswaardig daar het ons een serieuze productiviteitswinst bij het schrijven van integratietests heeft opgeleverd. De algemene term voor het manipuleren van zichzelf of andere programma's is metaprogramming. Hoewel bovenstaand
3 van
voorbeeld vrij eenvoudig is, is Ruby in staat tot meer ingewikkelde zaken zoals blijkt uit de weblog Ola Bini (hoofddeveloper van JRuby).
5.3.11
Evaluatie
De deadline voor deze iteratie werd nipt gehaald, omdat het testen een aanzienlijk deel van de tijd had ingenomen. De evaluatie verliep wederom met dr. Kris De Schutter. Het resultaat van de iteratie werd algeheel positief ontvangen. De overgang naar de professionelere layout was onverwacht, maar dat was geen probleem aangezien de deadline gehaald werd. Er dienden wel kleine aanpassingen aan de layout gedaan te worden, maar niets wereldschokkends. hoogvliegers.
Ontwikkelaars zijn nu eenmaal vaak geen artistieke
In feite was dit wel een beetje een overtreding ten opzicht van het Agile Development
3 http://ola-bini.blogspot.com/search/label/metaprogramming
74
principe, maar ons idee is dat het verder uitstellen van de layout later voor meer werk zou zorgen dan het nu in orde brengen. Volgens ons moet men niet blindelings de Agile Development losoën volgen, maar dient men ook de nodige portie gezond verstand te gebruiken. De nieuwe CRUD functionaliteit alsook de authenticatie en authorizatie werden aan een grondige test onderworpen. Alles bleek te werken zoals verwacht en de klant was tevreden. Een opmerking van de klant was dat nu eender welke gebruiker de links naar alle modules kan zien (bovenaan staat een menubalk met een link naar de verschillende delen van de webapplicatie), maar weliswaar er soms geen toegang toe had.
De klant had graag gezien dat de links naar modules die
ontoegankelijk zijn, verborgen worden. We zullen de wens van de klant naleven en dit implementeren.
75
5.4 Iteratie 3: maaltijd functionaliteit 5.4.1
Doelstelling
In de vorige iteraties zijn de basiscomponenten en bijhorende functionaliteit voor voeding en gebruikers volledig afgewerkt. In deze iteratie gaan we pogen deze twee te verbinden door de maaltijdfunctionaliteit toe te voegen. Concreet wil dit zeggen dat gebruikers na authenticatie voedingsmiddelen kunnen kiezen en hiermee een maaltijd samenstellen die gepersisteerd zal worden.
Het ontwerp van de databank en
modellen zou normaal geen problemen meer mogen opleveren na de ervaring met vorige iteraties. Tijdens de bespreking met de klant voorafgaande deze iteratie, bleek dat voor dit onderdeel een dynamische, moderne interface zou moeten ontworpen worden, naar analogie met de originele applicatie. Het probleem stelt zich dat een soortgelijke GUI voor het web moeilijker te bereiken zal worden, daar het statische HTML weinig ruimte voor dynamiek laat. Samen met de klant werd het uitzicht van de grasche interface geschetst en de vereiste functionaliteit besproken. De iteratietijd werd vastgelegd op drie weken (de vorige iteraties was dit twee weken), aangezien de hoeveelheid werk toch aanzienlijk leek.
Gelukkig bleek na literatuurstudie dat Rails enkele
voorzieningen aan boord heeft om exibele interfaces te ontwikkelen, waarover in volgende delen meer.
5.4.2
Standaard Rails: Modellen, Controllers en Views
Het mag duidelijk zijn uit de vorige twee iteraties dat de implementatie van de basis van een iteratie steeds dezelfde werkwijze volgt in Rails.
Hoewel het toch om aanzienlijk wat code gaat, gaan we er
hier summier overgaan omdat er geen nieuwe zaken aan bod komen. Het is het vrij eenvoudig om snel resultaat te boeken wanneer men eenmaal vertrouwd is met de conventies van Rails. Voor deze iteratie was het slechts een kwestie van enkele uren voor de eerste vruchten van arbeid toonbaar waren. Op de tweede plaats betekent de consistentie ook een vereenvoudiging ten aanzien van toekomstig onderhoud of uitbreidbaardheid. Mits het volgen van de conventies, zal een ontwikkelaar die vertrouwd is met deze conventies snel zijn of haar weg vinden doorheen de code. In deze iteratie dienen we het mogelijk te maken om maaltijden per gebruiker te persisteren. Een maaltijd bestaat in feite uit een verzameling van voedingsmiddelen, met een bepaalde hoeveelheid. Deze vereisten vertalen zich in een vrij voor de hand liggend databankschema, zoals te zien op guur 5.8.
Figuur 5.8: Databankschema iteratie 3 De functionaliteit werd ondergebracht in de FoodController, daar het conceptueel bij de Dieet Module behoort. Er werd niet afgeweken van het klassieke Rails stramien en conventies, zodanig dat de vereiste functionaliteit op zeer korte periode kon worden geïmplementeerd. De functionaliteit werd in eerste plaats ontwikkeld met behulp van een zeer eenvoudige visualisatie (via scaolds).
De volgende stap bestond er dan uit het grasch aantrekkelijk en intuïtief maken van de 76
interface. Dit was echter makkelijker gezegd dan gedaan. Het betreft hier een dynamische component, de maaltijd, die groeit en krimpt naargelang de handelingen van de gebruiker. De oplossing werd uiteindelijk gevonden met hulp van dr. Kris De Schutter, door gebruik te maken van vrij ingewikkelde CSS. Voor dit onderdeel te implementeren zijn toch enkele dagen verloren gegaan. De fout ligt hier evenwel niet bij Rails, maar bij onze onervarenheid met webdesign. Het is dan ook niet te verwonderen dat vele bedrijven een apart departement voor design hebben. Het uiteindelijke resultaat is te zien op guur 5.9. De percentagebalken bovenaan geven de nutriëntengrenzen voor de dag aan. Dit is ook een pure CSS oplossing, grotendeels gebaseerd op code van Georey
4
Rosenbach , door ons omgezet naar een view-helper.
Figuur 5.9: Screenshot: dynamisch groeiende maaltijd
5.4.3
Een nieuwe generatie van het web: Web 2.0
Sinds enkele jaren is er een evolutie aan de gang in het weblandschap. Waar het oorspronkelijk ging om informatievoorziening, neigt het web tegenwoordig naar een platform voor functionaliteit. Naar analogie met de versienummering in de softwarewereld, noemt men deze transitie Web 2.0.
Hoewel dit meer
een buzzword of marketingterm is, zijn er toch duidelijke veranderingen aan de manier waarop het web gebruikt wordt.
Typisch voor Web 2.0 is gebruikersinteractie.
Het web evolueert steeds meer van een
hulpmiddel voor informatiewinning naar een platform voor allerlei zaken, dat op vrij korte tijd goed ingeburgerd is geraakt in de maatschappij. Men kan tegenwoordig terecht spreken over web applicaties, in plaats van de traditionele web sites. Enkele typische voorbeelden van Web 2.0 zijn weblogs, wikis, RSS feeds, podcast, social bookmarking en nog veel meer. Geen van deze toepassingen kan geïncorporeerd worden in onze applicatie, daar het niet echt past in de geest en doel van de applicatie. Typisch echter voor webapplicaties die bestempeld worden als Web 2.0, zijn de interactieve en moderne gebruikersinterfaces. In de komende secties zullen we pogen om de interface Web 2.0 te maken.
We zullen nagaan of Rails, die bij lancering de slogan Web 2.0
ready meekreeg, hiervoor voorzieningen aan boord heeft.
4 http://www.nubyonrails.com/ 77
5.4.4
Ajax
Het traditionele model voor browser-server interactie is het request-reply paradigma. Dit betekent concreet dat na gebruikersinteractie de browser een request naar een webserver stuurt, eventueel gevolgd door verwerking aan serverzijde waarna een volledige HTML pagina wordt teruggestuurd. Dit betekent ook dat de pagina fysisch (en visueel) ververst wordt, wat allesbehalve modern aanvoelt. Begin 2005 beschreef Jesse James Garrett in een invloedrijke publicatie [6] een techniek benoemd Ajax, bedoeld om het ouderwetse web een grasche facelift te geven. Ajax staat voor Asynchronous JavaScript and XML en laat toe om webpagina's aan te passen zonder dat een hele pagina dient ververst te worden. De technologie werd reeds tien jaar geleden geïmplementeerd door Microsoft, maar kent pas sinds kort een ongeloofelijke populariteit bij webontwikkelaars. Het idee werd later ook overgenomen in Netscape en door het Mozilla Team, zodat men er van kan uitgaan dat Ajax functioneert op de meest courante systemen. Hoewel XML voorkomt in Ajax, is dit geen absolute vereiste en Rails kan dan ook een breed scala aan dataformaten via Ajax verwerken. De voordelen van Ajax zijn vrij logisch:
•
Een webapplicatie met Ajax gedraagt zich meer zoals een klassieke desktop-applicatie, waar ook geen verversingen plaatsvinden.
•
Typisch is de grootte van de data die verzonden wordt met Ajax kleiner dan een gehele webpagina. Hierdoor zal de applicatie ook sneller reageren omdat er minder data dient verwerkt te worden. Voor de gebruiker voelt de applicatie als veel sneller aan.
Tegelijk wordt om zelfde reden ook
minder bandbreedte verbruikt.
Zoals op guur 5.10 te zien is, verschilt het gebruik van Ajax in feite niet zo veel met het traditionele web applicatie model.
Concreet komt het er op neer dat men aan browserkant een JavaScript object
(XmlHttpRequest) ter beschikking heeft waarmee men data in XML vorm kan uitwisselen met de webserver. Door middel van manipulatie van het DOM-object (Document Object Model) via JavaScript kan men het antwoord van de server inbrengen in de reeds gerenderede webpagina (via de Ajax engine op de guur). Het nadeel van Ajax of van JavaScript in het algemeen is dat het gebruik ervan repetitief en foutgevoelig is.
Er ontstonden echter vrij snel tal van Ajax bibliotheken om de ontwikkeling met behulp van Ajax
eenvoudiger te maken.
5
Rails gebruikt intern het Prototype JavaScript Framework , en voorziet view-
helpers die toelaten om Ajax te gebruiken zonder zelf ooit één letter JavaScript te hoeven schrijven. Het is zelfs zo dat elke actie van elke controller gebruikt kan worden met een asynchrone oproep via Ajax. De
het gebruik van Ajax
vertaling achter de schermen is dan geheel voor de rekening van Rails. In feite is dus
transparant vanuit het standpunt van de ontwikkelaar, het enige dat wijzigt zijn de methodenamen
van de view-helpers. Beschouw als voorbeeld onderstaande code. In dit voorbeeld wordt een HTML-link aangemaakt die, wanneer er op geklikt wordt, het onderdeel op de website genaamd meals zal invullen met de output van de show_saved_meal actie.
1
t e x t = l i n k _ t o _ r e m o t e ( d a t e . day , show_saved_meal ' ,
: u r l => { : a c t i o n =>
: m e a l _ d a t e => d a t e } ,
: u p d a t e =>
Codevoorbeeld 53: Ajax hyperlink
5 http://www.prototypejs.org/ 78
' ' meals ' )
Figuur 5.10: Traditioneel model versus het Ajax model voor webapplicaties (uit [6])
Deze regel is afkomstig uit de viewcode voor de kalender die men kan gebruiken om de historie van maaltijden te raadplegen. Gesteld dat bijvoorbeeld date in bovenstaande code gelijk is aan 10 april, zal de view-helper link_to_remote onderstaande (JavaScript- en HTML)code genereren (date.day in de code zorgt ervoor dat de huidige dag getoond wordt, in dit geval dus 10).
1
h r e f="#"
o n c l i c k=" new
Ajax . U p d a t e r ( ' m e a l s ' ,
show_saved_meal ? m e a l _ d a t e =2007 − 04 − 10 ' ,
e v a l S c r i p t s : true }) ;
return
'/ nephrology / food /
{ asynchronous : true ,
f a l s e ; ">10
Codevoorbeeld 54: Gegenereerde JavaScript
Het mag duidelijk zijn dat dit al veel minder leesbaar en onderhoudbaar is dan de eerste regel. Merk op dat Rails hier op zijn beurt gebruik maakt van het prototype framework (Ajax.Updater ) en er dus eigenlijk nog veel meer JavaScript gegenereerd wordt achter de schermen. Merk tevens op dat, indien men geen gebruik wil maken van Ajax, hier gewoon link_to_remote dient te vervangen door link_to. Op geheel analoge manier kunnen remote formulieren of Ajax-observers (controleren een expressie en voeren een actie uit indien de expressie een bepaalde waarde oplevert) geïmplementeerd worden zonder noemenswaardige inspanning. We hebben dan ook gretig gebruik gemaakt van Ajax voor de view in deze iteratie, omdat de inspanningen minimaal zijn en het resultaat des te groter. Het is jammer dat we Ajax niet ontdekt hadden in de vorige iteratie, want dankzij Rails is er absoluut geen reden om de view te implementeren op de ouderwetse manier. Het resultaat is dan ook dat de dieet-module bijzonder snel en modern aanvoelt.
5.4.5
Visuele eecten
Ajax op zich is al een serieuze verbetering om snelle en gebruiksvriendelijke GUI's te ontwikkelen, maar qua grasche eecten voegt Ajax niets toe aan HTML. Dankzij opnieuw JavaScript en het DOM, is het
79
mogelijk om visuele eecten te bekomen. Dit gaat van subtiele kleurikkeringen tot explosies van bepaalde delen van de pagina. Wederom zijn er tal van JavaScript bibliotheken beschikbaar die het gebruik van JavaScript tot een minimum herleiden.
Net zoals voor Ajax gebruikt Rails intern een bibliotheek en
biedt view-helpers aan om deze te gebruiken.
De bibliotheek die Rails gebruikt, is de Script.aculo.us
6
JavaScript bibliotheek. Deze bibliotheek gebruikt intern ook het Prototype framework, waardoor visuele eecten in combinatie met Ajax kunnen gebruikt worden. Beschouw hiertoe onderstaand voorbeeld uit de zoekfunctie voor gebruikers. Deze helper genereert in eerste plaats een standaard HTML tekstveld. Wanneer men echter iets invult in dit veld zal met Ajax een oproep naar de server gebeuren, die de data in de databank met de input vergelijkt en de overeenkomstige data terugstuurt. De resultaten worden dan onder het tekstveld opengeklapt met Script.aculo.us die hier kleurschakeringen en doorzichtigheid aan toe voegt.
1
<%= t e x t _ f i e l d _ w i t h _ a u t o _ c o m p l e t e
: user ,
: name %>
Codevoorbeeld 55: Tekstvelddeclaratie met auto completion
We zijn geen fan van al te veel visuele eecten, maar we zijn er van overtuigd dat subtiele eecten juist een meerwaarde kunnen zijn in het gebruik van de webapplicatie. In die zin hebben we vooral eecten gebruikt om bepaalde zaken te accentueren. Zo zal bij het toevoegen van een voedingsmiddel op guur 5.9 dit voedingsmiddel subtiel enkele keren ikkeren waardoor de gebruiker door heeft dat dit het nieuwe voedingsmiddel is. We hebben nog zulke eecten toegevoegd aan de applicatie, maar dit is uiteraard beter te zien in de applicatie zelf dan ze hier op te sommen. Beschouw onderstaande code ter illustratie. Het betreft hier een zoekfunctie van voedingsmiddelen via een eenvoudig tekstveld. Wanneer dit formulier met Ajax naar de server wordt gezonden (zie form_remote ), zullen de resultaten worden weergegeven in een lijst (:update => 'quick_search_results ') die langzaam naar onder uitbreidt (visual_eect(:blindDown,
...), wat aangenaam overkomt voor de gebruiker. Merk op dat dit stukje code zal vertaald worden naar een heleboel JavaScript die we nu niet meer zelf hoeven te schrijven.
1
<% form_remote_tag
: u r l => { : a c t i o n => " q u i c k _ s e a r c h " } ,
2
: u p d a t e =>
3
: c o m p l e t e => v i s u a l _ e f f e c t ( : blindDown ,
' quick_search_results ' , ' quick_search_results ' )
do %> 4
<%= t e x t _ f i e l d
5
<% end %>
6
: food ,
: name ,
: size
=> 1 8 %>
i d= ' q u i c k _ s e a r c h _ r e s u l t s '>
Codevoorbeeld 56: Viewcode voor de (Ajax) zoekfunctionaliteit van voedingsmiddelen
Hoewel Rails standaard reeds zeer veel voorzieningen aan boord heeft, zijn er ook vele enthousiastelingen die gebruik maken van het open-source karakter van Rails om eigen plugins te ontwikkelen. zijn eenvoudig te aan te maken, zolang (wederom) conventies gevolgd worden.
Plugins
Op die manier kunnen
zelfs heuse patches voor Rails via een plugin aangebracht worden. Overdraagbaarheid is geen probleem, omdat de plugin fysiek een onderdeel is van de applicatie en als dusdanig mee overgedragen wordt met de applicatie. Eén van de plugins die we in de applicatie gebruikt hebben , is de redbox plugin geschreven
6 http://script.aculo.us/
80
7 die een pop-up venster zoals op guur 5.11 mogelijk maakt. Doordat plugins meestal
door Craig Ambrose
open-source zijn, hebben we de code eenvoudig kunnen aanpassen om in de eerste plaats van toepassing te zijn op onze noden maar tegelijk ook zo licht mogelijk te zijn.
Figuur 5.11: Screenshot van de Redbox plugin in werking
5.4.6
RJS Templates
Zowel de visuele eecten als de Ajaxhelpers betreen één (DOM) element van de webpagina.
Dit is
een logische keuze, maar er bestaan zeker toepassingen die er baat zouden bij hebben dat verschillende elementen op dezelfde pagina tegelijk met andere inhoud worden aangepast. Sinds versie 1.1 bevat Rails daarom de mogelijkheid om naast RHTML en RXML ook RJS als antwoord naar de browser terug te sturen. RJS staat voor Ruby JavaScript en dat is meteen ook de beste denitie voor een RJS template.
In
plaats van de traditionele HTML naar de browser terug te sturen, wordt met RJS pure JavaScript code naar de browser gestuurd die onmiddelijk uitgevoerd wordt. Net zoals in vorige gevallen voorziet Rails wederom in een heleboel hulpmethoden die ervoor zorgen dat men nooit met JavaScript in contact hoeft te komen. Vandaar dat men in feite JavaScript schrijft, maar dan in Ruby. Dankzij RJS zijn er volgens ons bijna geen grenzen meer tussen de mogelijkheden van een desktop- en webapplicatie. Het is moeilijk om de kracht van RJS te beschrijven op papier, men dient dit met eigen ogen in werking te zien. Het is dan ook geen verrassing dat we meermaals in de implementatie gebruik hebben gemaakt van RJS. Als illustratie geven we hieronder een vrij uitgebreid voorbeeld (de meeste van onze RJS is 2 tot 3 regels) waar een consumptie wordt toegevoegd aan de maaltijd. Merk op dat deze code zowel kan opgenomen worden in controlleracties als in een apart .RJS bestand. In onderstaande code wordt de inhoud van het element rightcontent1 visueel verborgen via de Script.aculo.us shrink methode (regel één), gevolgd door het zichtbaar maken van rightcontent2 (regel twee). In de praktijk lijkt het alsof deze twee elementen worden verwisseld waarbij de eerste onder de andere schuift. In de volgende regels
7 http://blog.craigambrose.com/articles/2006/08/16/redbox-a-rails-compatible-lightbox 81
worden verschillende elementen van de pagina ingevuld met verschillende inhouden (page.replace_HTML). Bovendien voeren we een visueel eect uit op de voedingsteller van de pagina (regel zes). Merk op dat RJS in feite gewoon een toepassing is van Ajax, waarbij een asynchrone request een JavaScript antwoord als gevolg heeft. Het is ook duidelijk dat RJS templates zeer krachtige toepassingen kan hebben, maar onderstaand voorbeeld (dat het meest complexe uit onze applicatie is) toont ook aan dat het schrijven van RJS bijna kinderspel is.
1
page . v i s u a l _ e f f e c t
: shrink ,
' rightcontent2 ' ,
2
page . v i s u a l _ e f f e c t
: grow ,
3
f o o d = Food . f i n d ( params [ : i d ] )
4
if
' rightcontent1 ' ,
Consumption . c r e a t e ( : f o o d => f o o d ,
: d u r a t i o n => 0 . 4
: d u r a t i o n => 0 . 4
: q u a n t i t y => q u a n t i t y ,
: meal
=> @ c u r r e n t _ m e a l ) . s a v e 5
p a g e . replace_HTML
' nr_of_consumptions ' ,
: partial
=>
'
nr_of_consumptions ' 6
page . v i s u a l _ e f f e c t
7
p a g e . replace_HTML
: pulsate , ' flash ' ,
' nr_of_consumptions ' ,
" Added
'#{ f o o d . name } '
to
: p u l s e s => 5 meal
with
q u a n t i t y = #{ q u a n t i t y } " 8 9 10
else p a g e . replace_HTML
' flash ' ,
' Failed
to
add
consumption
to
meal '
end
Codevoorbeeld 57: RJS template voor het toevoegen van een consumptie aan de maaltijd
5.4.7
Testen & Code Coverage
In deze iteraties hebben we ons vooral toegelegd op het ontwikkelen van views en bijhorende controllers. In sectie 5.3.8 en 5.3.9 zagen we reeds dat functionele tests en integratie tests uitermate hiervoor geschikt zijn. Men zou kunnen denken dat Ajax of RJS moeilijk te testen is door het asynchrone karakter ervan. De waarheid is echter verrassend, want er is geen wezenlijk onderscheid tussen de Ajax en niet-Ajax acties in een controller, enkel de naam van de testmethoden wijzigen licht. Beschouw onderstaande testmethode als voorbeeld. Regels drie en acht zijn speciek voor Ajax en RJS. In regel drie gebruiken we xhr (Xml Http Request) in plaats van een gewone http get of post. Op regel acht wordt dan nagegaan of de RJS wel degelijk zijn werk heeft gedaan en het element van de pagina vervangen heeft. Het is duidelijk dat tests schrijven met of zonder Ajax in Rails dus amper verschil kent.
1
def
2
n r _ o f _ c o n s u m p t i o n s = Consumption . c o u n t
3
xhr ( : post ,
4 6
: add_quicksearched_consumption , : i d => @some_food . i d ,
5
test_add_quicksearched_consumption
: c o n s u m p t i o n => { : q u a n t i t y => assert_response
7
assert_equal
8
assert_select_RJS
9
' 1234 ' } )
: success
nr_of_consumptions + 1 , : replace_HTML ,
Consumption . c o u n t
' currentmeal '
end
Codevoorbeeld 58: Functionele test met Ajax-oproepen en RJS asserties
Tijdens het schrijven van de tests voor deze iteratie zochten we naar een manier om na te gaan of alle code
82
ook daadwerkelijk getest is. Standaard bevat Rails geen voorzieningen om dit te bereiken. We hebben gekozen om de standaard code coverage tool van de Rubywereld, RCov, onder de loep te nemen. RCov
8
is een ruby tool geschreven door Mauricio Fernandez , voor het bepalen van de code dekking van Ruby Unit Test zoals gebruikt voor Rails Unit tests. Gelukkig bleek dat sinds recent ook ondersteuning voor functionele- en integratietests is toegevoegd en dit alles beschikbaar is als plugin voor het Rails framework. RCov is zeker en vast een interessante tool om te gebruiken bij het testen. RCov geeft een visueel resultaat in HTML zoals te zien op guur 5.12. Op elk van de links kan geklikt worden om te zien welke coderegels getest worden door onze tests en welke niet.
Figuur 5.12: Resultaat van uitvoering RCov op de Unit Tests
Code coverage is echter geen absoluut middel bij het schrijven van tests. Het geeft weliswaar een bepaalde indicatie, maar meer dan aangeven of alle code eens aangeraakt wordt in de tests doet het niet. Men weet nog altijd niet of alle mogelijkheden zijn nagegaan en alle randgevallen getest zijn. Of dit echter ooit mogelijk zal zijn, durven we ten zeerste betwijfelen. Maar het kan dus zeker geen kwaad om naast het gezond verstand ook code coverage tools te gebruiken bij het schrijven van tests.
5.4.8
Evaluatie
De evalutie van deze iteratie verliep wederom in samenspraak met de klant, vertegenwoordigd door dr. Kris De Schutter. De visualisatie van de maaltijd werd goedgekeurd en het gebruik van Ajax werd als zeer positief ervaren. Het feit dat we onze tests konden bewijzen met RCov was zeker een pluspunt. Na een grondig nazicht van de applicatie, waren er toch enkele opmerkingen:
1. De klant verwachtte dat het klikken op de afbeelding van een voedingsmiddel betekent dat dit voedingsmiddel moet worden toegevoegd aan de maaltijd. In de huidige implementatie wordt echter informatie over de nutriënten van dat voedingsmiddel getoond. Dit moet aangepast worden.
8 http://eigenclass.org/
83
2. Er zat een bug in het berekenen van de leeftijd van een gebruiker. Blijkbaar werd een geboortedatum voor 1970 niet aanvaard. 3. Het zoeken van gebruikers gaf soms geen resultaten. 4. De klant had graag de mogelijkheid gehad om een top 10 van meest gekozen voedingsmiddelen te raadplegen. 5. De klant had graag gezien dat de patiënt de getoonde nutriënten (zie guur 5.9 op bladzijde 77) kan instellen.
Het mag duidelijk zijn dat deze evaluatie zeer nuttig was voor ons. Het is duidelijk dat de fouten in punt 2 en 3 te vermijden waren met nog meer tests. Hoewel de code coverage van de testen voor de betreende zeer hoog is (+ 90%), blijken er nog ongeteste gevallen te bestaan. Hiermee wordt nogmaals aangetoond dat er geen kant en klaar recept voor het schrijven van tests bestaat.
84
5.5 Intermezzo: Rails & webservices 5.5.1
Inleiding
In de loop van de derde iteratie werd ons gevraagd om te onderzoeken of Rails mogelijkheden bood omtrent webservices. Webservices worden steeds belangrijker in het bedrijfslandschap en het is dus een mooie kans om na te gaan of Rails hiervoor klaar is. Zoals gezien in sectie 3.1, bevat Rails een component genaamd Action Web Service (AWS) voor dit doel. In de volgende secties bekijken we eerst kort het idee achter webservices, gevolgd door een bespreking van de implementatie gebruik makende van AWS.
5.5.2
Service-georiënteerde architecturen & Webservices
De laatste jaren is het gebruik van service-geörienteerde architecturen (SOA) steeds populairder geworden. Zonder in detail te treden, is zo een architectuur gebaseerd op het gebruik van verspreide services (diensten) over een netwerk in plaats van een centralisatie van services.
Dit betekent geen algehele omschakeling
van architectuurmodellen, integendeel: SOA's zijn meer evolutie van bestaande architecturen dan een revolutie. De services kunnen in eender welke taal op eender welk platform geschreven worden, zolang de communicatie op een gestandaardiseerde manier verloopt. Webservices zijn zo een standaard, gedenieerd door het World Wide Web Consortium (W3C). De specicatie van webservices is modulair en omvat verscheidene protocollen en formaten. De twee meest gebruikte componenten zijn:
• SOAP (Simple Object Access Protocol):
een XML-gebaseerd formaat voor de berichten die
worden uitgewisseld tussen aanbieders en afnemers van services. SOAP kan gezien worden als een gestandaardiseerde enveloppe waarin een bericht met standaard regels voor opmaak zit.
SOAP
gebruikt voor het transport van de berichten meestal het populaire HTTP(S), vandaar de naam webservices.
• WSDL (Web Services Description Language):
een XML-gebaseerde taal die toelaat om de
interface van de services op een gestandaardiseerde manier te beschrijven. De interface zelf wordt ook de WSDL genoemd. Eenvoudig gezegd toont de WSDL van een applicate welke services naar buiten toe worden aangeboden en hoe deze moeten gebruikt worden.
De derde component,
UDDI (Universal Description Discovery and Integration), die vaak in één
adem met de vorige twee wordt genoemd is een gestandaardiseerde manier om centrale repositories voor WSDLs aan te maken. Via deze repositories kunnen webservices gepubliceerd en ontdekt worden, zonder dat aanbieders of afnemers elkaars adres kennen. UDDI is echter nog niet doorgebroken op het moment van schrijven en wordt als dusdanig amper gebruikt. Webservices zijn stilaan uitgegroeid tot een vaste waarde in vele bedrijven. De best gekende voorbeelden zijn zonder twijfel de productzoekdienst van Amazon en de vele applicaties van Google die te benaderen zijn via een open webservice.
85
5.5.3
Aanbieden van webservices via Rails
AWS ondersteunt zo goed als de gehele W3C specicatie voor SOAP en WSDL (en ook voor XML-RPC, een alternatief voor SOAP). Figuur 5.13 toont de werking van AWS, gezien vanuit het standpunt van de ontwikkelaar. Achter de schermen gebeuren een heleboel handelingen zoals XML- en SOAPparsing, maar dat is voor ons van geen belang. Merk op dat buiten AWS we maar één andere hoofdcomponent van Rails oproepen, namelijk ActiveRecord. De geïmplementeerde views en controllers zijn niet nodig, aangezien deze bedoeld zijn om de data op een scherm te visualiseren. Zoals echter op de guur 5.13 te zien worden wel nieuwe controllers gedenieerd, speciek voor de webservice.
Figuur 5.13: Werking van AWS De eerste stap om een Rails-webservice aan te maken is na te gaan welke functionaliteit we naar de buitenwereld willen aanbieden. Idealiter zou dit via een eenvoudige declaratie moeten kunnen gebeuren in de betreende klassen, zoals gebruikelijk in Rails. De wereld buiten Ruby is echter overheersend statisch getypeerd, waaronder ook de SOAP standaard, wat onvermijdelijk conicteert met de ongetypeerde methodes van Ruby. AWS lost dit probleem op door een
API denitie klasse te gebruiken. Een API
kan het beste vergeleken worden met een Java of C# interface.
Een API bevat de methode waarvoor
parameter- en returntypes expliciet opgegeven worden, waardoor AWS de type conversie kan toepassen. Een stuk van de API van onze applicatie ziet er als volgt uit:
1
class
WsApi < A c t i o n W e b S e r v i c e : : API : : B a s e
2
wsdl_service_name
3
api_method
: find_all_categories ,
' diet_service '
4
api_method
: find_category ,
: r e t u r n s =>
: e x p e c t s =>
[ [ Category ] ]
[ : int ] ,
: r e t u r n s =>
[
Category ]
5
end
Codevoorbeeld 59: API voor de webservice
Op basis van deze API denitie kan AWS automatisch een WSDL genereren. De bekomen WSDL voor deze eenvoudige code is al 80 regels lang en zodoende hebben we hem hier niet afgeprint. De aanzienlijke overhead van het gebruik van XML is een nadeel van de huidige webservices standaard, waarover later meer (sectie 5.5.5). Het punt is dat ontwikkelaars in feite nooit rechtstreeks in contact komen met WSDL of SOAP. Alles wordt geregeld op methode- en objectniveau.
Of het nu gaat om het ontwikkelen van
webservices of een normale applicatie, veel verschil is er dankzij AWS niet. 86
Uiteraard is een API denitie niet genoeg om een werkende service te verkijgen. De logica achter de API wordt op dezelfde manier geïmplementeerd als voorheen. Men denieert een controller die overerft van de AplicationController, maar AWS komt tussen wanneer er normaal een view zou gerendered moeten worden. Aangezien de business logica verweven is in de ActiveRecord klassen, is de taak van een AWS controller dus ook niets meer dan data ophalen en teruggeven. Zodoende zijn onze controllers zeer eenvoudig, zoals te zien in onderstaande code. Wat we schrijven is gewone Rails code zoals voorheen, waarbij AWS een enorme hoeveelheid achter de schermen uitvoert om normale code om te zetten naar de SOAP of WSDL standaard.
1
class
WsController <
ApplicationController
2
web_service_api
3
web_service_scaffold
4
def
5
end
7
def
8 10
Category . f i n d ( : a l l ,
: o r d e r =>
' name ' )
find_category ( id )
return
9
: invoke
find_all_categories
return
6
WsApi
Category . f i n d ( i d )
end end
Codevoorbeeld 60: Controller voor de webservice
Op de derde lijn is te zien dat ook voor webservices gebruik kan gemaakt worden van scaolds. Hier gaat het om een dynamische scaold die at runtime een heuse collectie controllers en views genereert om de webservices via een browser te kunnen uittesten.
Tevens kunnen voor webservices dezelfde functionele
testen geschreven worden als voor normale controllers. In onze applicatie hebben we de webservices functionaliteit beperkt gehouden tot een tiental functies, gezien het om een tussentijds onderzoek ging en niet de bedoeling was om webservices diep uit te werken. In de implementatie hebben we gebruik gemaakt van één API die rechtstreeks gehecht is aan aan één controller.
In de praktijk zijn complexere mechanismen zoals layered en delegated dispatching beter
geschikt om verschillende functionaliteiten te scheiden naar analogie met de verschillende controllers. Dit wordt zeer goed door Leon Breedt beschreven in [17].
Het mag echter uit bovenstaande tekst en code
blijken dat het opzetten van een webservice met Rails ongelofelijk eenvoudig is en zeer weinig tijd vraagt. Zodra de logica beschreven is in een ActiveRecord, is het maar enkele minuten werk om de functionaliteit ook aan te bieden via een webservice.
5.5.4
Webservices gebruiken in Rails
Uiteraard is het nodig dat niet enkel het aanbieden van webservices eenvoudig gaat, ook het gebruik van webservices in eigen code moet ondersteund worden. Onderstaande code maakt duidelijk dat het slechts een kwestie van één declaratie in de betreende controller is om dit te kunnen doen. Verder dienen amper aanpassingen gedaan te worden aan de controllercode, alles wordt wederom achter de schermen geparsed en geconverteerd door AWS.
87
1
class
2
SomeController <
web_client_api
3
: soap ,
ApplicationController
: Category , h t t p : / / my . u r l . com/ ws / a p i
4 5
def
6 7
8
list
@foods = Category . f i n d _ a l l _ c a t e g o r i e s end end
Codevoorbeeld 61: Het gebruik van webservices in Rails
5.5.5
REST
Er zijn al een tijd geruchten dat de Rails ontwikkelaars plannen om ActionWebservice uit het framework te halen en verder door het leven te laten gaan als een plugin. Sinds de komst van Rails 1.2 heeft men het idee van
Representational State Transfer (REST) naar voor geschoven als dé manier om SOA's te
ontwerpen. REST is gebaseerd op een idee van Roy Fielding, één van de grondleggers van het HTTP, in zijn doctoraatsartikel [15]. We hebben bewust geen gebruik gemaakt van REST in onze applicatie, omdat op dit moment de techniek nog te jong is om te zien in welke richting het zal evolueren. REST is opgebouwd rondom het concept van een resource, die uniek geïdenticeerd is door een URL. Op deze URL kunnen de vier standaard HTTP opdrachten (GET, POST, PUT en DELETE) toegepast worden.
Aangezien er geen andere mogelijkheden zijn dan de basisoperaties maakt Rails het mogelijk
om de REST CRUD functionaliteit in de controller voor een ActiveRecord klasse te schrijven met één regel code. Gesteld dat we zulke declaratie zouden hebben geschreven voor het model Food, zijn volgende operaties op bijhorende URL mogelijk:
• GET /foods:
geeft een collectie van alle Foods terug.
• POST /foods:
maakt een nieuwe Food aan met de informatie in het HTTP bericht.
• GET /foods/123:
geeft de Food met id gelijk aan 123 terug.
• PUT /foods/123:
update de Food met id gelijk aan 123 met de informatie in het HTTP bericht.
• DELETE /foods/123:
verwijdert de Food met id gelijk aan 123.
De onmiddellijke voordelen van de REST aanpak zijn:
•
Past perfect in de Rails architectuur. Waar AWS soms nog geforceerd overkomt (het gebruik van de API), is het alsof REST geboren is voor Rails. Zowel Rails als REST zijn URL-gebaseerd voor hun werking.
Bovendien zorgt de automatische CRUD-generatie voor nog minder werk dan voorheen
zonder één enkele aanpassing aan de applicatie.
•
REST is vele malen lichter dan SOAP, omdat gewerkt wordt op URL basis en de complexe XMLparsing stap kan overgeslagen worden. Er kan gebruik gemaakt worden van een WSDL, maar dit is niet noodzakelijk. Vaak worden rechtstreekse URL's gebruikt in applicaties die de services gebruiken.
•
REST is perfect cachebaar, omwille van de toestandloosheid van HTTP. 88
De keuze van Rails om voluit voor REST te gaan, lijkt ons riskant. Rails, en laat staan REST, zijn op dit moment onvoldoende doorgebroken om er reeds zulke ruchtbaarheid aan te geven. Er zijn reeds enkele frameworks in andere talen zoals Java en C# die REST ondersteunen, maar de industriestandaard is nu eenmaal webservices. Ons lijkt het beter te focussen op een bewezen en veelvuldig gebruikte standaard, om doorbraak van Rails te forceren in de bedrijfswereld.
89
5.6 Iteratie 4: Dokter functionaliteit 5.6.1
Doelstelling
Op dit moment rest ons nog één functionaliteit vooraleer we alle functionaliteit van de originele applicatie bereikt hebben. Meer bepaald moet het mogelijk zijn dat een dokter de applicatie kan gebruiken om de eetgewoonten van zijn patiënten op te volgen. Na een gesprek met de klant werd bovendien duidelijk dat enkel de correcte dokter de gegevens van een patiënt mag bekijken. Het is dus niet zo dat een dokter de gegevens van elke patiënt kan bekijken. Een dokter moet ook de nutriënten kunnen instellen die getoond worden aan de patiënt.
5.6.2
Implementatie
De implementatie van de iteratiefunctionaliteit verliep volgens het klassieke stramien: eerst de modellen, daarna de controllers en de views. We bespreken hieronder de gevolgde stappen summier, omdat ze vrij standaard zijn en de gebruikte technieken reeds vroeger werden besproken. Het eerste probleem dat we dienden op te lossen is gebruikers op één of de andere wijze associëren met elkaar in een dokter-patiënt relatie.
De eerste optie die dan onmiddelijk in de gedachten springt, is
het gebruik van een jointabel met twee vreemde sleutels naar dezelfde tabel. Hoewel we alle mogelijke conventies volgden, leek Rails niet kunnen om te gaan met deze vorm van zelfreferentie, wat we ook probeerden. Ten slotte namen we genoegen met een minder exibele oplossing door Rails automatisch een boomstructuur van gebruikers te laten onderhouden, zoals reeds gebruikt voor de categorieën in iteratie één (zie sectie 5.2.2). Via een migratie wijzigden we dan op eenvoudige wijze de tabel voor gebruikers om nu ook een vreemde sleutel parent_id te bevatten. Dit betekent dat we per gebruiker één (andere) gebruiker kunnen instellen die de dokter van deze gebruiker is. Zo zullen we uiteindelijk een vrij brede boomstructuur bekomen.
We pasten vervolgens de AdminController en bijhorende views aan om voor
een gebruiker een dokter te kunnen deniëren en omgekeerd (met Ajax). Op modelniveau moet er verder niets gewijzigd worden. De informatie over de eetgewoonten zit reeds in de databank onder de vorm van gepersisteerde maaltijden en kunnen ook aangewend worden om statistieken hiervan te tonen aan een dokter. Voor de controllers en de views is het werk ook vrij beperkt.
We deniëren een nieuwe controller, de
DoctorController, om de functionaliteit te scheiden van de rest.
We gebruiken eenzelfde opbouw van
visuele componten als voor de maaltijden om de uniformiteit te bewaren. We passen de Ajax-techniek op analoge wijze toe zoals in de vorige iteratie (zie sectie 5.4.4). Uiteraard zit hier wat denk- en codeerwerk achter, maar niets dat nog niet is aangehaald in voorgaande besprekingen. Wederom schreven we voor alle nieuwe logica voldoende tests om ons van een correcte werking te vergewissen. Het uiteindelijke resultaat is in screenshotvorm te zien op guur 5.14.
9
De graek die te zien is op guur 5.14 wordt at runtime aangemaakt met de Gru Graphic Library van Georey Grosenbach. Vermits Rails uiteindelijk gewoon Ruby code is, kunnen Ruby bibliotheken gewoon gebruikt worden door Rails klassen.
Om de portabiliteit van onze applicatie te vergroten, hebben we
deze bibliotheek ondergebracht in de map /lib die bij conventie tijdens het starten van Rails automatisch geladen wordt.
9 http://nubyonrails.com/pages/gru 90
Figuur 5.14: Screenshot van doktermodule (graek met calciumwaarden van een patiënt)
Op geheel analoge wijze kan het gehele Rails framework ondergebracht worden in een map die bij conventie is vastgelegd. Op die manier zijn we zeker dat er geen incompatibele versie van het framework gebruikt wordt op het systeem waar we onze applicatie zullen installeren. Jammergenoeg is gebleken dat we onze applicatie niet 100% porteerbaar kunnen maken.
Zo is steeds een werkende installatie van Ruby en
ImageMagick (intern gebruikt door Gru ) vereist.
5.6.3
Evaluatie
Het resultaat van de iteratie werd positief ontvangen. De door Gru geproduceerde graeken werden mooi bevonden, maar de klant merkte terecht op dat de schaal van de tijdsas niet echt correct is. Helaas bleek na inspectie van de Gru API hier geen afdoend middel tegen te bestaan. Aangezien het zoeken naar een andere bibliotheek hoogstwaarschijnlijk veel werk zou betekenen werd onderling besloten om het resultaat zo te laten. Uiteindelijk is de graek perfect interpreteerbaar. De applicatie is op dit moment op functioneel vlak gelijk of zelfs meer functioneel dan de originele applicatie. Het doel van de case study is bereikt en in principe kan de ontwikkeling nu gestaakt worden. We wensen echter Rails werkelijk bloot te stellen aan de buitenwereld. Op het internet circuleren namelijk artikels (voornamelijk via blogs) waarin Rails qua performantie niet zo goed uit de verf komt. Na overleg met de klant werd dan ook besloten dat de resterende ontwikkelingstijd zou geschonken worden aan een optimalisatie iteratie.
91
5.7 Iteratie 5: optimalisatie & deployment "I calculated the total time that humans have waited for web pages to load. It cancels out all the productivity gains of the information age." -
5.7.1
Scott Adams
Doelstelling
Op een bepaald moment in de ontwikkeling van een applicatie komt men onvermijdelijk op een punt dat alle functionele vereisten van de klant voldaan zijn (hoewel dit vaak een utopie is). Aangezien dit punt werd bereikt op het einde van vorige iteratie, kan nu de laatste iteratie gebruikt worden om ook de niet-functionele vereisten onder de loep te nemen. De belangrijkste niet-functionele vereiste van de klant is meestal performantie. Het verschil tussen het behandelen van 100 of 500 gelijktijdige gebruikers, kan een extra machine en bijkomende kosten betekenen. Vanuit het standpunt van de klant, die uiteraard de kosten het liefst beperkt ziet, is het dus niet meer dan logisch dat de applicatie ook performant is. Performantie kan op twee vlakken behaald worden.
Allereerst kan op het niveau van software slecht
presterende code herschreven worden zodanig dat deze sneller uitgevoerd wordt. Op dit niveau heeft men bovendien de beschikking over bepaalde technieken zoals caching die de performantie de hoogte in sturen. Op hardware niveau kan men dankzij de share-nothing architectuur van Rails (zie sectie 1.4.9) extra Rails processen en fysieke servers bijplaatsen om mee te schalen met een verhoogde load. Het hoeft geen betoog dat men eerst moet pogen op software niveau de nodige optimalisaties uit te voeren alvorens men de toevlucht neemt tot fysieke hulpmiddelen, daar de kosten ervan op lange termijn wezenlijk lager ligt.
5.7.2
Testmethode
Rails is voorzien van automatische logging, waarvan de granulariteit kan gaan van zeer grof (oproepen op controllers) tot zeer jn (afzonderlijke SQL queries) en ingesteld kan worden per environment. Beschouwen we bijvoorbeeld een logregel uit de productie environment:
Processing FoodController#show_food (for 192.168.0.1 at 2007-04-04 13:34:08) [GET] (pid:3140) Session ID: 07f99f07cdf5c9b9e28d1d4800f6634a (pid:3140) Parameters: {"action"=>"show_food", "id"=>"764", "controller"=>"food"} (pid:3140) Rendering within layouts/general_layout (pid:3140) Rendering food/show_food (pid:3140)
Completed in 0.19100 (5 reqs/sec) | Rendering: 0.01000 (5%) | DB: 0.17100 (89%)
| 200 OK
Het blijkt dat deze logging reeds een schat aan informatie bevat (zie vette aanduiding) die kan gebruikt worden om de performantie van de applicatie te meten. Het log geeft afzonderlijk de tijd gespendeerd aan databankoperaties, het renderen van een RHTML le met ERb en de overhead van het framework aan (de overschot na de tijd voor databankoperaties en rendering). Om de performantie te testen, volstaat het om de tijd van afzonderlijke controllermethoden in acht te nemen, daar deze het enige aanspreekpunt zijn van gebruikers. Uiteraard kan een model of een view ook geoptimaliseerd worden, maar zoals hierboven te zien is, geeft het log dit ook aan. Het Rails framework bevat geen standaardcomponent om performantievergelijkingen uit te voeren tussen verschillende conguraties. Het bevat wel een standaard proler, maar deze bleek ontoereikend voor onze 92
doeleinden omdat de proler enkel werkt op één methode tegelijk. Het is dus niet mogelijk om in te loggen (via de login methode) vóór het testen van een aander methode, waardoor steeds een authenticatiefout gegenereerd wordt.
We hebben dus een eigen oplossing moeten uitwerken.
We hebben nood aan een
script dat vele malen en zonder manuele tussenkomst kan uitgevoerd worden om een zo accuraat mogelijk resultaat te bekomen. Bovendien is het van groot belang dat de performantietesten de handelingen van een werkelijke gebruiker zo goed mogelijk simuleren, om de metingen zo correct mogelijk te maken. Volgende mogelijkheden werden daarom onderzocht:
1. Een eigen script, gebaseerd op de techniek van DSL's zoals beschreven in 5.3.10. 2. Railsbench
10 : een verzameling van scripts geschreven door Stefan Kaes die uitgebreide statistieken
genereren over de performantie van een bepaalde verzameling van opgegeven controllermethoden. Railsbench wordt beschouwd als de standaard voor Rails benchmarking. 3. Selenium on Rails
11 : een tool die gebruikt maakt van scripts geschreven in JavaScript om gebrui-
kershandelingen perfect na te bootsen in een browser. Het gebruik ervan is werkelijk eenvoudig en komt in feite neer op het opnemen van een macro terwijl men de applicatie gebruikt.
12 (Watir): een open-source tool die vrijwel geheel gelijkaardig is
4. Web Application Testing In Ruby
aan Selenium, maar waarbij de scripts geschreven worden in Ruby. Beide tools zijn ontworpen om aan client-side (GUI) testing te kunnen doen. Indien we dus deze GUI handelingen enkele tientallen malen kunnen herhalen, zijn deze tools bruikbaar voor onze performantietesten.
Na een reeks eerste tests vielen keuze 1 en 2 snel af.
Wanneer de metingen vergeleken werden met
resultaten in het log, bleek al gauw dat deze testmoden enkele ordes naast de realistische waarden zaten. Voor keuze één blijkt de overhead van het testing framework te hoog te zijn om een realistische meting te kunnen uitvoeren. Voor keuze twee blijken de meetwaarden steeds enkele factoren lager te zijn dan in werkelijkheid. De resterende twee mogelijkheden in acht genomen was de keuze snel gemaakt. Selenium is vele malen gebruiksvriendelijker, maar mist de functionaliteit om lussen in de scripts te steken, wat uiteraard onontbeerlijk is om een accuraat gemiddelde te kunnen bepalen. Gezien Watir scripts gewone Ruby klassen zijn, zijn lussen uiteraard mogelijk. Watir scripts zijn uitermate eenvoudig en blijken wederom een mooi voorbeeld van DSL's in Ruby. Onderstaand Watir script toont bijvoorbeeld het inloggen in de applicatie (@ie slaat op de Internet Explorer browser in de code). Door een lus te zetten rondom deze declaraties kunnen we een goed beeld van de performantie van dit onderdeel krijgen.
1
@ i e . g o t o ( h t t p : / / l o c a l h o s t : 3 0 0 0 / n e p h r o l o g y )# @ i e
2
@ i e . t e x t _ f i e l d ( : name ,
" username " ) . s e t ( " joram " )
3
@ i e . t e x t _ f i e l d ( : name ,
" password " ) . s e t ( " joram123 " )
4
@ie . button ( : v a l u e ,
" Login " ) . c l i c k
is
de
browser
Codevoorbeeld 62: Gedeelte van het Watir script dat gebruikt werd om gebruikers te simuleren
10 http://railsbench.rubyforge.org/ 11 http://www.openqa.org/selenium-on-rails/ 12 http://wtr.rubyforge.org/
93
Tenslotte wordt het log bekomen na uitvoering van het Watirscript automatisch geanalyseerd door het Rawk
13 script, geschreven door Chris Hobbs, dat statistieken zoals het gemiddelde, minima en de stan-
daard deviatie van de meettijden berekent.
5.7.3
Testopstelling
De fysieke testopstelling bestaat uit een client- en servermachine. De servermachine draait een webserver met Rails en is een Intel centrino 1.5 Ghz laptop met 512 MB DDR RAM en een 4200 rpm harde schijf. Dit is uiteraard geen realistische serverconguratie, maar het zijn niet de absolute tijdswaarden maar de relatieve tijdswinsten die behaald kunnen worden die van belang zijn.
De clientmachine is een andere
(desktop) pc die het Watirscript uitvoert om de overhead van de browser niet te laten doorwegen op de tijdsmetingen. Om een realistisch beeld te krijgen van de performantie van de applicatie, werden negen controlleracties uitgekozen die representatief zijn voor de hele applicatie. De onderlijnde woorden geven aan waarom de actie gekozen werd.
• login/login:
toont het login venster. Deze actie is zo goed als geheel statisch (er zijn geen elementen
die kunnen wijzigen).
• food/list_categories:
ophalen en afbeelden van alle categorieën in de databank. Deze actie is een
standaard Rails methode waarin data wordt opgehaald, klaargezet en getoond
• food/show_food/764:
ophalen en afbeelden van de nutriënteninformatie omtrent het voedings-
middel met id 764. Deze actie werd gekozen omwille van de onderliggende join met de grote food-
contents tabel.
• food/show_meal:
rekenintensieve methode die alle berekingen omtrent nutriëntenconsumptie
doet en de huidige maaltijd afbeeldt. De huidige maaltijd bestaat uit 10 verschillende consumpties en reeds 10 consumpties zijn die dag al opgeslagen, om toch een aardige hoeveelheid rekenwerk te forceren.
• food/show_saved_meal:
gebruikt Ajax om informatie om 10 consumpties van een maaltijd van
een bepaalde dag op te halen.
• food/update_nutrients:
gebruikt Ajax om informatie van 8 nutrienten up te daten. Deze actie
werd gekozen om het aanpassen van data te testen.
• food/quick_search (op 'a'):
rendert een lijst van +700 elementen van gevonden voedingsmiddelen.
Deze actie werd er uitgepikt om na te gaan wat de overhead van het redeneren is.
• admin/index:
haalt het aantal elementen in iedere tabel op en toont deze.
• admin/create_user:
voegt een gebruiker toe via een gewone HTTP-post.
13 http://rubyforge.org/projects/rawk-the-logs/
94
5.7.4
Invloed van de development environment
Een eerste optimalisatie valt zeker en vast te halen door gebruik te maken van een optimaal gecongureerde omgeving (zie 1.4.7). Standaard gebruikt Rails de ontwikkelingsomgeving. In deze omgeving worden bij elke request alle klassen herladen en staat de loggingfaciliteit op hoogste niveau. Deze omgeving is gericht op een zo hoog mogelijke productiviteit bij het ontwikkelen, maar dit heeft uiteraard een impact op de performantie. Tabel 5.1 toont de metingen aan voor de uitvoer van het Watir-script, 100 maal herhaald. Aantal
Med
Gem
Max
Min
login
104
0.0150
0.0256
0.1800
0.0100
0.3658
list_categories
101
0.2500
0.3292
0.5000
0.2400
3.3733
show_food
101
0.4800
0.4599
0.7210
0.3400
4.6955
show_meal
203
3.4050
3.5574
4.5970
3.1740
50.6138
show_saved_meal
100
0.3350
0.3319
0.4610
0.2300
3.4049
update_nutrients
100
0.3500
0.3930
0.5010
0.2700
3.9938
quick_search
200
0.4555
0.4970
0.9420
0.1200
8.2145
index
101
0.4610
0.3869
0.6610
0.2700
3.9729
create_user
100
0.2305
0.2947
0.5510
0.1900
3.0533
Request
Std. Dev
Tabel 5.1: Meting Win XP Pro - Webrick 1.3.1 - Development env. (seconden)
Wanneer we deze resultaten interpreteren, worden onze vermoedens bevestigd. De ontwikkelingsomgeving is duidelijk ongeschikt voor productiedoeleinden. Ook zoals verwacht is de login pagina met voorsprong de snelste pagina, wat niet verwonderlijk is gezien er geen dynamische content gegenereerd dient te worden en Rails dus weinig te maken heeft met de afhandeling van de request. Het verschil tussen minima en maxima is telkens vrij groot.
De standaard variatie is echter meestal
beperkt, wat ons leert dat de metingen goed geclusterd zijn en er dus slechts sprake is van enkele pieken. Na enig opzoekingswerk blijkt dat deze pieken verklaard kunnen worden door de ineciënte garbage collector van Ruby. Een andere mogelijke verklaring is dat de databank uiteraard ook cached. Bij een eerste uitvoering van een query zal een cache-misser optreden en zal er dus een piek zijn. De volgende uitvoeringen zal het resultaat rechtstreeks uit de cache kunnen geleverd worden. De methode show_meal is duidelijk de minst performante methode van de beschouwde methoden: een gemiddelde tijd van 3.5 seconden voor één gebruiker is onaanvaardbaar voor een realistische webapplicatie. De standaard variatie bij show_meal is tevens zeer groot, wat betekent dat de metingen ver uit elkaar liggen. Een logregel voor show_meal toont ons dit: Completed in 3.76500 (0 reqs/sec) | Rendering: 0.13000 (3%) |
DB: 1.83300 (48%)
Bovenstaande logregel toont aan dat ongeveer de helft van de tijd dan ook naar de databank gaat. De andere helft (49% hier) zijn de berekeningen in de methode zelf. Bovendien toont het log ons dat voor één uitvoer van show_meal er 720 afzonderlijke SQL queries naar de databank gaan (hier niet getoond). Wanneer we de andere methoden inspecteren, blijkt dat er steeds veel afzonderlijke queries voorkomen, weliswaar niet in die mate als voor show_meal. Er is dus zeker en vast nog ruimte voor verbetering.
5.7.5
Overgang naar de standaard Rails productie omgeving
Standaard raadt de documentatie van het framework aan om de omgeving in te stellen op de standaard productie omgeving wanneer een applicatie in productie wordt gebracht. Hierin wordt logging beperkt 95
tot het absolute minimum en worden klassen slechts eenmaal bij het booten ingeladen. Het zou dus niet meer dan logisch zijn dat de meetresultaten in tabel 5.2 positiever uitvallen dan de vorige meting. Request
Aantal
Med
Gem
Max
Min
Std. Dev
login
104
0.0100
0.0171
0.2710
0.0001
0.3653
list_categories
101
0.1900
0.1934
0.3600
0.0900
1.9870
show_food
101
0.4610
0.2830
0.5110
0.1400
2.9172
show_meal
203
3.4050
2.9543
4.4460
2.5740
42.1996
show_saved_meal
100
0.1450
0.1513
0.4000
0.0600
1.6028
update_nutrients
100
0.2455
0.2260
0.4110
0.1000
2.3403
quick_search
200
0.3855
0.3962
0.8610
0.0200
7.4123
index
101
0.1500
0.1564
0.5700
0.0600
1.6783
create_user
100
0.1750
0.1965
0.4310
0.1500
2.0648
Tabel 5.2: Meting Win XP Pro - Webrick 1.3.1 - Production env. (seconden)
Een eerste vaststelling is dat alle gemeten waarden zoals verwacht onder die van de ontwikkelingsomgeving liggen. Het is dus zonder meer duidelijk dat een correct ingestelde omgeving een positieve invloed heeft op de performantie. Tevens valt op dat de standaard afwijking in elk van de gevallen kleiner is, wat betekent dat deze omgeving stabieler is dan de ontwikkelingsomgeving. Dat is ook logisch, aangezien men minder afhankelijk is van schijfoperaties (om klassen te laden en om logging uit te schrijven), wat geregeld wordt door het besturingssyteem en waarop Rails geen invloed heeft. Request login list_categories
Dev (rps)
Prod (rps)
Winst (%)
39.06
58.48
+50%
3.04
5.17
+70%
show_food
2.17
3.53
+63%
show_meal
0.28
0.34
+21% +120%
show_saved_meal
3.01
6.61
update_nutrients
2.54
4.42
+74%
quick_search
2.01
2.50
+24%
index
2.58
6.39
+148%
create_user
3.39
5.09
+50%
Tabel 5.3: Development vs Production (gemiddeld aantal requests per seconde)
Tabel 5.3 toont aan dat de productie omgeving gemiddeld een winst geeft van 69%, indien we de mogelijkheid van optreden uniform veronderstellen. In de praktijk zal dit echter zeker niet uniform zijn. Zo zal de login pagina slechts één maal gezien worden, terwijl de maaltijd (show_meal) vele malen zal herladen worden. De werkelijke winst zal dus hoogstwaarschijnlijk lager liggen. Dit indachtig, vallen de resultaten eerder tegen. Deze opstelling zou allerminst bruikbaar zijn in een werkelijke omgeving, zelfs indien we een factor bijrekenen voor een snellere machine. Tabel 5.3 leert ons bovendien dat bepaalde methoden meer baat hebben bij een geoptimaliseerde environment dan andere. Voor show_meal kan de rekenintensiviteit aangehaald worden, waarop de omgeving amper invloed heeft. Quick_search is dan weer vooral zwaar om te renderen, wat at runtime plaatsvindt en waarop de omgeving ook weinig invloed heeft. Enkel het minder loggen kan hier wat verschil in meettijd betekenen.
96
5.7.6
Invloed van het platform
Meer dan de helft van de websites en webapplicaties op het WWW worden op dit moment gehost door Linux/unix servers (cijfers netcraft.com).
De kans dat een Rails applicatie uiteindelijk zal gedeployed
worden op een *nix server is dus vrij groot. We besloten daarom om de invloed van het platform op de performantie na te gaan. De tests werden gedaan op dezelfde machine, met dezelfde databankinstellingen en dezelfde omgevingsconguratie. Tabel 5.4 toont de bekomen meetresultaten aan. Request
Aantal
Med
Gem
Max
Min
Std. Dev
login
104
0.0075
0.0094
0.0975
0.0031
0.1582
list_categories
101
0.0440
0.0746
0.1830
0.0388
0.8101
show_food
101
0.0615
0.0808
0.1916
0.421
show_meal
203
0.7755
0.7515
1.6511
0.6293
10.8067
show_saved_meal
100
0.0381
0.0456
0.1532
0.0175
0.5518
update_nutrients
100
0.0700
0.0895
0.2657
0.0503
0.9884
quick_search
200
0.2407
0.2256
0.5027
0.0077
4.3826
index
101
0.0397
0.0499
0.1565
0.0239
0.6090
create_user
100
0.0374
0.0558
0.1613
0.0272
0.6393
0.8922
Tabel 5.4: Meting Ubuntu 7.04 - Webrick 1.3.1 - Production env.
De meetresultaten in tabel 5.4 zijn op zijn minst verrassend te noemen. In elke test scoort de Linux server enkele beduidende factoren beter dan de windows server. De pieken zijn nog steeds aanwezig, maar de standaard afwijking is vele malen kleiner dan op de windows server, wat betekent dat de Linux server een veel stabieler platform biedt voor Rails. Het enige wezenlijk verschil is dat we op het Linux platform een custom Mysql binding hebben gecompileerd in C, waar dit standaard op Windows een binding in Ruby is. Volgens verscheidene internetfora zou de winst echter niet meer dan 10-20% mogen zijn . Feit is dat de resultaten té goed zijn om te negeren, waardoor verdere tests dus zullen gedaan worden op het Linux platform. Tabel 5.5 toont de winst aan die gemaakt wordt door de applicatie te deployen op het Linux platform, met exact dezelfde conguratie als voor Windows. Gemiddeld gezien presteert deze conguratie 176 % beter. Request
Win. XP (rps)
Ub. 7.04 (rps)
58.48
106.38
list_categories
5.17
13.40
+159%
show_food
3.53
12.38
+251%
show_meal
0.34
1.33
+291%
show_saved_meal
6.61
21.93
+232%
update_nutrients
4.42
11.17
+153%
quick_search
2.50
4.43
index
6.39
20.04
+214%
create_user
7.94
17.92
+126%
login
Winst (%) +82%
+77%
Tabel 5.5: Ubuntu 7.04 vs Windows XP - Webrick 1.3.1 - Production env.
97
5.7.7
Eager Loading
De ActiveRecord ORM maakt standaard gebruik van lazy loading.
Dit betekent dat data pas uit de
databank zal gehaald worden wanneer dit nodig is. Beschouw bijvoorbeeld onderstaande code: unit_name =
Nutrient
.nd(:rst, :conditions => [name = Energie]).
unit
.name
Voor deze regel gebeuren er twee queries naar de databank, eenmaal naar de nutrient tabel en eenmaal naar de unit tabel. Indien men echter in toekomstige code gebruikt maakt van geassocieerde objecten, kan het voordeliger zijn om de data in één keer op te halen. Concreet betekent dit dat er een JOIN operatie zal moeten gebeuren tussen de geassocieerde tabellen. ActiveRecord bezit de mogelijkheid om zulke joins te forceren: unit_name = Nutrient.nd(:rst, :conditions => [name = Energie],
:include => [:unit]
).unit.name
In plaats van twee queries, zal nu maar één query gebeuren. Het ophalen van data die misschien later nodig zal zijn, noemt men eager loading. Er is echter geen vaste regel die kan gebruikt worden om te bepalen wanneer eager loading een positieve invloed zal hebben. Soms is het voordeliger om veel kleine queries te doen dan één grote query met enkele joins omdat een join een vrij zware databankoperatie is.
In de praktijk komt het dus neer een op een
trial-&-error proces om te bepalen welke tabellen moeten opgehaald worden en welke niet. Wanneer we het log bestuderen, is het duidelijk dat show_food en show_meal zeer veel kleine queries doen naar de databank. Zoals beschreven in sectie 5.7.4 bestaat één oproep van show_meal uit ongeveer 720 afzonderlijke queries, waaronder meermaals het ophalen van één nutrient, één unit of één food. De verwachting is dus dat een beter resultaat kan behaald worden met eager loading. Om de invloed van eager loading te testen, hebben we wederom gebruikt gemaakt van het Watirscript dat 100 maal herhaald werd.
Voor show_food was de invloed van eager loading nihil of zelfs nadelig.
Voor show_meal kon het resultaat verbeterd worden door het voedsel op te halen bij het inladen van de consumpties en het ophalen van nutriënten en voedsel bij het inladen van de foodcontents. Combinaties waarbij foodcontents of nutriënten reeds van bij het inladen van de consumpties werden opgehaald, bleken merkwaardig genoeg de meettijd te verhogen tot meer dan één seconde. Dit toont nog maar eens aan dat het gebruikt van eager loading meer trial-&-error is dan exacte wetenschap. De tijd voor show_meal kon verlaagd worden tot
gemiddeld 0.6058 seconden, wat een winst van 24% betekent, enkel door 2 regels
code toe te voegen. De meeste tijd blijkt na proling (zonder inloggen) van show_meal in het bereken van de nutriëntenopnames te zitten, wat niet veel ruimte meer biedt voor optimalisatie.
5.7.8
Transacties
Een minimale snelheidswinst kan behaald worden door gebruik te maken van databanktransacties. Standaard zal ActiveRecord voor elke afzonderlijke update of insert één transactie gebruiken. In het geval van
update_nutrients, waarbij steeds acht nutriënten worden geupdated, zouden deze acht update operaties in feite in één transactie kunnen gebeuren. In Rails is dit zeer eenvoudig: men dient rond de code die men in de transactie wil uitvoeren gewoon het transaction do ... end statement te zetten. De performantiewinst bij update_nutrients is echter vrij beperkt. Met één transactie komt de meettijd op
0.0868 seconden, een winst van slechts 3%. Wanneer er meer operaties gebeuren, zal waarschijnlijk
het resultaat beter zijn, maar zulke methoden komen niet voor in deze applicatie. 98
5.7.9
Trage View-helpers
In de huidige resultaten vertoont het geval quick_search een verdacht hoge tijd. Een resultaat van
0.2256
seconden lijkt vrij hoog voor het ophalen en tonen van 748 voedingsmiddelen, zeker omdat het ophalen gebeurt met Ajax. We dachten eerst dat de databank de bottleneck zou zijn, maar er is slechts één query op een geïndiceerd attribuut. Het log maakt ons alvast wijzer:
Completed in 0.46564 (2 reqs/sec) |
Rendering: 0.43245 (92%)
De rendertijd is zeer hoog in vergelijking met de andere gevallen, waar dit meestal rond of onder de 10% ligt.
Nochtans bestaat de RHTML le die gerendered moet worden uit weinig code: een loop over de
gevonden voedingsmiddelen waarin een remote link (Ajax) voor dat voedingsmiddel via een view-helper wordt uitgeschreven:
<%= link_to_remote "#{food.name}", :url => {:action => "get_quantity_eld", :id => food} %>
Het is niet meer dan logisch dat het gebruik van ERb een zekere runtime overhead heeft. We proberen dan ook de (JavaScript) output van de helper letterlijk te vervangen:
', {asynchronous:true, evalScripts:true}); return false;" href="#"> <%= food.name %>
Indien we de testen opnieuw runnen bekomen we nu een verbijsterend gemiddelde van wat een nettowinst van
gebruik ervan komt wel met een groot voordeel. het tweede.
0.0417 seconden,
441% betekent. De overhead van ERb is dus niet te onderschatten, maar het Zo is het eerste codestuk veel onderhoudbaarder dan
Als vuistregel zou dus kunnen gesteld worden dat onderhoudbaarheid primeert, tenzij de
rendertijd een problematisch groot aandeel van de totale tijd inneemt. In de overige testen is de rendertijd slechts een minimale fractie van de totale tijd, waardoor we dus geopteerd hebben voor onderhoudbaarheid.
5.7.10
Invloed van de hardware
De meeste acties hebben we nu al vrij goed kunnen optimaliseren, maar de rekenintensieve show_meal actie hinkt nog zwaar achterop. Aangezien de software optimalisaties bijna uitgeput zijn, kunnen we nu even kijken naar hardware optimalisaties. We kregen hiervoor de mogelijkheid om onze applicatie te testen op een servermachine van de vakgroep GH-SEL. Het betreft hier een Intel Xeon 3.40 Ghz met 1 Gb Ram en een SATA 7200 rpm harde schijf met als besturingssysteem Debian Linux. We hebben de applicatie eenvoudigweg gekopieerd naar de nieuwe machine en deze voorzien van Ruby. Onze verwachting dat de snellere machine betere prestaties zou neerzetten werd ook bevestigd door de meetresultaten, opgenomen in tabel 5.6.
99
Request
Aantal
Med
Gem
Max
Min
Std. Dev
login
104
0.0033
0.0052
0.0662
0.0013
0.1180
list_categories
101
0.0387
0.0458
0.1103
0.0359
0.5046
show_food
101
0.0431
0.0561
0.1255
0.0426
0.6148
show_meal
203
0.5298
0.5508
0.7062
0.5117
7.8499
show_saved_meal
100
0.0184
0.0260
0.1102
0.0168
0.3423
update_nutrients
100
0.0455
0.0564
0.1341
0.0427
0.6231
quick_search
200
0.0270
0.0231
0.1637
0.0066
0.4766
index
101
0.0177
0.0289
0.1550
0.0173
0.3982
create_user
100
0.0269
0.0387
0.0971
0.0212
0.4535
Tabel 5.6: Meting met correcte Mysql binding
Wanneer we nu de vorige machine vergelijken met de nieuwe machine zoals te zien is in de laatste kolom van tabel 5.7, kunnen we enkel concluderen dat snellere hardware zeker en vast een invloed heeft op de performantie van de applicatie. Dit is niet meer dan logisch uiteraard. De resultaten in de laatste kolom maken echter ook duidelijk dat vooral de snellere harde schijf een invloed heeft en niet zozeer de verhoogde rekenkracht (slechts 10% beter voor het rekenintensieve show_meal ). Voor deze machine hebben we ook de meetresultaten verzameld voor een installatie zonder de MySql binding in C. Het is duidelijk uit de vierde kolom dat de MySql binding in C een veel grotere invloed heeft dan de 10-20% die we vernoemd hebben in sectie 5.7.6 (gemiddeld 55% sneller dan de conguratie zonder binding in C). Request login
OM (rps)
NM1 (rps)
Verschil OM - NM1 (%)
NM2
Verschil OM - NM2
106.38
222.22
+109%
312,50
+194%
list_categories
13.40
15.87
+18%
21.83
+63%
show_food
12.38
11.03
-11%
17.83
+44%
show_meal
1.65
0.90
-56%
1.82
+10%
show_saved_meal
21.93
24.50
+12%
38.46
+75%
update_nutrients
11.52
13.72
+19%
17.73
+54%
quick_search
23.98
17.09
-29%
43.24
+80%
index
20.04
30.12
+50%
34.60
+73%
create_user
17.92
22.57
+26%
25.84
+44%
Tabel 5.7: Originele machine (OM) vs nieuwe machine zonder correcte binding (NM1) vs nieuwe machine met correcte binding (NM2)
5.7.11
Caching
In zowel de soft- als hardwarewereld wordt vaak gebruik gemaakt van
caching. Caching komt neer op
het bijhouden van data op zo een manier dat de volgende aanvraag voor de data sneller kan behandeld worden dan de eerste.
Caching gebeurt op allerhande niveau's, van CPU caches tot databank caches.
Caching wordt ook vaak toegepast voor webapplicaties om steeds een statische webpagina uit de cache te kunnen verzenden naar de eindgebruiker. Rails biedt dan ook logischerwijs ondersteuning voor caching. Aangezien een actie van een controller uiteindelijk overeenkomt met een webpagina, gebeurt het cachen op het niveau van de acties. Het aangeven van hoe er moet gecached worden, gebeurt via een declaratie van één lijn in de desbetreerende controller. Rails biedt ondersteuning voor drie vormen van caching:
100
• Page caching:
acties waarvoor page caching gedeclareerd is, transformeren in feite naar volledig
statische webpagina's. Na de eerste uitvoer van de desbetreende actie, wordt de resulterende HTML pagina op harde schijf opgeslagen. Volgende requests krijgen meteen deze pagina teruggestuurd. Het is zelfs zo dat enkel de webserver te pas komt bij het afhandelen van de requests, en Rails niet eens wordt aangeraakt. Het is duidelijk dat dit de meest performante optie is. Wanneer we de door ons gekozen acties beschouwen, komt slechts één actie hiervoor in aanmerking: de login methode. Voor al de andere acties is authenticatie vereist, wat niet kan gebeuren als we Rails omzeilen.
• Action caching:
bij deze vorm van caching komt het Rails framework wel nog te pas aan de
afhandeling van de request, maar de actie zelf wordt niet uitgevoerd.
Concreet betekent dit dat
onze authenticatie- en authorizatielters zullen worden uitgevoerd, maar de logica in de acties niet. Op analoge wijze als voor page caching wordt het resultaat opgeslagen op harde schijf.
In onze
applicatie kunnen vele zaken op deze manier gecached worden, maar in de door ons gekozen subset van acties hebben we geopteerd voor de acties index, list_categories en show_food.
De andere
methoden zijn in principe ook allemaal cachebaar, maar ze zijn naar onze mening niet zo geschikt voor caching omdat ze frequent wijzigen (zoals bijvoorbeeld de huidige maaltijd).
• Fragment caching:
caching wordt toegepast in de viewlaag. Dit is de minst performante vorm
van caching omdat in tegenstelling tot voorgaande mogelijkheden, de controlleracties wel uitgevoerd worden. Fragment caching past men toe door stukken viewlogica te declareren als cachebaar. De grote winst die hierbij gemaakt wordt, is dat de view-helpers niet meer zullen worden uitgevoerd maar direct worden vervangen door een opgeslagen versie. Dit is de manier waarop we het probleem van de trage view-helpers uit sectie 5.7.9 eleganter hadden kunnen oplossen. Het meetresultaat is ongeveer gelijk en de onderhoudbaarheid blijft behouden.
Indien we de caching zoals hierboven toepassen op de vermelde acties, bekomen we de meetresultaten in tabel 5.8. Merk op dat we voor login geen resultaat meer hebben, aangezien het Rails framework er niet meer aan te pas komt en we dus ook geen logresultaat hebben. Praktisch zal dit echter de tijd nodig om het bestand van harde schijf te halen en te verzenden zijn. Merk ook op dat de metingen van de resterende drie acties zeer dicht bij elkaar liggen. Dit is ook logisch, aangezien enkel de ters worden uitgevoerd en de rest rechtstreeks van harde schijf geleverd wordt. Request
Aantal
Med
Gem
Max
Min
Std. Dev
login
104
?
?
?
?
?
list_categories
101
0.0058
0.0073
0.0692
0.0054
0.1192
show_food
101
0.0056
0.0076
0.0704
0.0054
0.1249
index
101
0.0053
0.0075
0.0706
0.0054
0.1303
Tabel 5.8: Meting na toepassen van caching
Het is duidelijk dat caching dé manier is om de performantie naar omhoog te krijgen (naar ongeveer 133 rps). Caching is helaas niet steeds toepasbaar omdat de resulterende webpagina's te volatiel zijn. Zo heeft het cachen van een webpagina die beursgegevens weergeeft weinig zin. De overhead om de opgeslagen versie te verwijderen en een nieuwe aan te maken zou dan overwegen. In Rails kan men gecachete exemplaren verwijderen (expiring) door middel van zogenaamde Sweepers. Sweepers zijn Observers die bepaalde model-callbacks implementeren (zie sectie 3.3.6) en zodoende na wijzigingen in het model bepaalde opgeslagen webpagina's verwijderen. In de praktijk zal men steeds een 101
afweging moeten maken tussen de kost van het sweepen en de winst van het cachen. In onze applicatie zijn sommige delen (gebruikersbeheer, voedselcatalogus, patiënteninformatie,etc) weinig volatiel en dus perfect geschikt voor caching.
5.7.12
Mongrel
De documentatie van Rails raadt aan om de Webrick webserver niet te gebruiken voor productie-doeleinden. Hoewel deze webserver vrij snel is, staat hij bekend als allesbehalve stabiel is. Aangeraden wordt om de Mongrel
14 webserver te gebruiken. Bovendien is Rails single-threaded, wat betekent dat slechts één re-
quest per keer afgehandeld wordt. Webrick volgt dit model, maar dit is uiteraard niet realistisch naar praktisch gebruik toe.
Mongrel is echter ontworpen om makkelijk concurrente gebruikers toe te laten,
zoals we later zullen zien. Mongrel is zeer eenvoudig te installeren en gedraagt zich precies zo als Webrick, om de transitie zo aangenaam mogelijk te laten verlopen.
Het verschil in performantie tussen de twee webservers wisselt
naargelang de actie, zoals blijkt uit tabel 5.9. Het is echter niet zo dat de verschillen dramatisch zijn. Op dit moment lijkt er geen reden om Webrick niet te gebruiken, maar het is pas in sectie 5.7.15 dat Mongrel zijn werkelijke kracht zal tonen. Aantal
Med
Gem
Max
Min
Std. Dev
login
Request
104
?
?
?
?
?
vs. Webrick (Gem) ?
list_categories
101
0.0059
0.0070
0.0588
0.0056
0.0907
+4%
show_food
101
0.0057
0.0092
0.0603
0.0056
0.1568
-21%
show_meal
203
0.6317
0.5897
0.7301
0.5188
8.4121
-7%
show_saved_meal
100
0.0176
0.0251
0.0887
0.0169
0.3232
+6%
update_nutrients
100
0.0455
0.0603
0.2135
0.045
0.6772
+9%
quick_search
200
0.0252
0.0176
0.1137
0.0067
0.4943
+4%
index
101
0.0055
0.0078
0.706
0.0051
0.1283
-10%
create_user
100
0.0576
0.0477
0.1016
0.0205
0.5573
-23%
Tabel 5.9: Meting Mongrel + vergelijking met Webrick
5.7.13
Invloed van de hardware (2)
Op het laatste moment kregen we de mogelijkheid om onze applicatie te testen op een machine die een echte krachtpatser is. Het betreft een AMD64 3800+ met 2 GB Ram en een SATA 7200 rmp harde schijf met een Debian installatie. Uit de test bleek nogmaals dat, zoals te verwachten is, hardware zeker een grote invloed heeft op de uitvoeringstijd. Tabel 5.10 geeft een vergelijking van de vorige servermachine met de nieuwe machine, gemeten in gemiddeld aantal requests per seconde (rps). Het is duidelijk dat deze machine voor Rails veel beter geschikt is dan de vorige (gemiddeld 36% beter).
14 http://mongrel.rubyforge.org/
102
Request
Intel Xeon 3.40 Ghz, 1GB ram (rps)
login
AMD64 3800+, 2GB ram (rps)
winst (%)
?
?
?
list_categories
142.86
172.41
+21%
show_food
108.70
166.67
+53%
show_meal
1.70
2.43
+43%
show_saved_meal
39.84
54.64
+36%
update_nutrients
16.58
22.78
+37%
quick_search index create_user
56.82
56.82
~
128.21
135.14
+5%
20.96
40.65
+94%
Tabel 5.10: Meting op een AMD64 3800+ met 2GB Ram (Mongrel)
Merk op dat show_meal nog steeds de minst performante actie is. Met slechts 2.43 rps is dit ronduit een slecht resultaat te noemen. De reden hiervoor moet volledig bij Ruby gelegd worden, waarvan algemeen geweten is dat de taal weinig performant is voor aritmetiek en zeker voor oating-point berekeningen zoals hier. Een mogelijke oplossing zou eruit bestaan om gebruik te maken van de eenvoudige interfacing tussen Ruby en C (zoals beschreven in [16]). De slecht presterende code zou dan in C kunnen geschreven worden, wat hoogstwaarschijnlijk vele malen performanter zou zijn dan de equivalente Ruby code. Het gebruik van C gaat echter volledig in tegen de gedachtengang van Rails. Aangezien onze studie zich beperkt tot Ruby en Rails, hebben we deze mogelijkheid dan ook niet toegepast. In een bedrijfscontext zou deze situatie echter anders zijn en zou men voldoende pragmatisch moeten zijn om in te zien dat deze actie beter zou herschreven worden in C. Principes zijn goed, maar ze mogen nooit exibiliteit van de oplossing in de weg staan. Merk trouwens op dat de show_meal actie die wij testen acht nutriëntenwaarden berekent op basis van tien consumpties in het verleden en tien huidige consumpties. In realiteit zullen deze cijferwaarden meestal lager liggen, waardoor de mindere performantie van de actie in de meeste gevallen niet storend zal zijn.
5.7.14
Rails & performantie: beginsituatie vs. huidige conguratie
Het mag duidelijk zijn uit de voorgaande secties dat een standaard Rails applicatie in grote mate kan geoptimaliseerd worden. Het is dus zeker de moeite geweest om de laatste iteratie te wijden aan optimalisatie. Het zou zelfs onverstandig zijn om geen tijd te investeren in optimalisatie, wanneer de performantiewinsten in tabel 5.11 in acht worden genomen (de beginsituatie is beschreven in sectie 5.7.5, het beste resultaat in voorgaande sectie). Merk op dat de tabel zowel software- als hardwareoptimalisaties bevat. Gemiddeld genomen is de applicatie
32.75 keer sneller dan in het begin. Dit durven we best een goed resultaat te
noemen, zeker in vergelijking met de hoeveelheid werk die er maar voor nodig is. Naar onze mening kan een Rails applicatie dan ook pas af genoemd worden na een soortgelijke optimalisatiestap. Merk op dat sommige resultaten aantonen dat Rails geen snelheidsmonster is, maar dat gemiddeld gezien (81.44 rps) het resultaat naar onze mening meevalt.
Er zullen zeker en vast snellere frameworks
bestaan, maar men moet onthouden dat Rails in de eerste plaats mikt op een hogere productiviteit dan performantie. Bovendien kan Rails eenvoudig schalen, zoals we verder zullen zien.
103
Request
Beginsituatie (rps)
login
Beste resultaat (rps)
Winstfactor (aantal keer sneller)
39.06
?
?
list_categories
3.04
172.41
56.71
show_food
2.17
166.67
76.81
show_meal
0.28
2.43
8.68
show_saved_meal
3.01
54.64
18.15
update_nutrients
2.54
22.78
8.97
quick_search
2.01
56.82
28.27
index
2.58
135.14
52.38
create_user
3.39
40.65
11.99
Tabel 5.11: Beginsituatie vs. beste resultaat
5.7.15
Concurrente gebruikers
De applicatie is op dit moment zo ver geoptimaliseerd als naar onze kennis mogelijk is. Merk echter op dat er nog een addertje onder het gras zit. Rails is namelijk standaard single-threaded. Dit betekent dat de requests na elkaar afgehandeld worden en geen parallele verwerking mogelijk is. Stel dat een bepaalde requestafhandeling één seconde duurt en er tien requests staan te wachten die een zeer korte behandeling vereisen. Dan zullen de tien korte requests niet uitgevoerd worden voor de eerste gedaan is. Mongrel kan eenvoudig aangewend worden om Rails concurrent te maken door middel van de Mon-
grel_cluster uitbreiding.
Dankzij deze uitbreiding kan men een willekeurig aantal Mongrel processen
opstarten op willekeurige poorten waarachter dezelfde applicatie zit.
Het praktisch gebruik van Mon-
grel_cluster is werkelijk ongecompliceerd en vraagt slechts drie lijnen conguratie. In principe kan Webrick ook concurrent gemaakt worden, maar standaard zijn er geen tools of manieren beschikbaar. De reden hiervoor zal zo dadelijk snel duidelijk worden. Om concurrente gebruikers te simuleren, maken we gebruik van de open-source tool Siege
15 . Met Siege
is het mogelijk om een willekeurig aantal gebruikers (intern is elke gebruiker een aparte thread) een bepaalde URL voor een gekozen tijd constant te laten opvragen. Siege genereert een heleboel statistieken, waarvan de gemiddelde antwoordtijd (de tijd tussen het verzenden van een request en het ontvangen van het antwoord) ons het meest kan vertellen over de eigenschappen van de applicatie op het vlak van concurrentie. We hebben Siege ingeschakeld om de actie list_categories telkens gedurende één minuut te stresstesten met achtereenvolgens 10, 20, 50, 100, 200 en 300 concurrente gebruikers. We hebben deze actie gekozen omdat ze zowel statisch (door action caching) als dynamisch (de lters) is en ons dus een globaal beeld kan verschaen inzake Rails en concurrentie, terwijl we ook het cachen kunnen testen. We hebben Siege laten lopen op een aparte machine om de metingen niet te beïnvloeden. Deze machine is verbonden via een 100 Mbit verbinding met de Intel Xeon 3.4 Ghz machine die we reeds in voorgaande secties hebben gebruikt.
Het netwerk is geen bottleneck in deze opstelling, aangezien de pagina's die
worden getransfereerd klein zijn in omvang. Hoewel we geopteerd hebben voor Mongrel als webserver, wilden we Webrick toch nog niet helemaal afschrijven.
De eerste concurrentietest bestond dan ook in een vergelijking tussen de twee webservers
waarbij geen caching werd toegepast. Zoals guur 5.15 aantoont, kent Webrick duidelijk zijn meerdere in Mongrel wanneer de webserver bestookt wordt met vele gebruikers tegelijk.
Merk op dat voor 200
gelijktijdige gebruikers ook Mongrel in moeilijkheden begint te komen met een onaanvaardbare gemid-
15 http://www.joedog.org/JoeDog/Siege
104
delde antwoordtijd van 9.15 seconden.
Bij 300 gelijktijdige gebruikers crashten zowel de Webrick- als
Mongrelserver.
Figuur 5.15: Concurrente gebruikers: Mongrel vs. Webrick (één proces, geen caching)
Voor productiedoeleinden zou deze conguratie dus enkel bruikbaar zijn voor een 20 tot 50 gebruikers. Daarna zal de wachttijd ervaren worden als storend.
Uiteraard zal een productieconguratie gebruik
maken van caching. De resultaten van dezelfde test maar nu met caching, worden getoond in guur 5.16. Zoals te verwachten, zijn de resultaten qua gemiddelde antwoordtijd beter dan zonder caching, maar een zelfde (meer dan lineaire) trend is ook te zien in deze resultaten. Opmerkelijk is dat deze guur ons leert dat Mongrel ook beter kan omgaan met caching, want het verschil tussen de server is deze keer veel groter. In deze test crashte de Webrickserver wederom bij 300 gelijktijdige gebruikers, maar Mongrel overleefde de test met een gemiddelde antwoordtijd van 3.50 seconden. De keuze tussen Webrick of Mongrel is vrij eenvoudig nu, met Mongrel als nadrukkelijke winnaar. Dit is een beetje onverwacht, omdat Webrick op individuele requests vaak beter presteert dan Mongrel. Meteen is ook duidelijk waarom er geen moeite gestoken wordt in de ontwikkeling van clustering tools voor Webrick. De volgende logische stap bestaat erin na te gaan hoe de applicatie presteert met meerdere Mongrel instanties op dezelfde machine. Dit kan het best vergeleken worden met een webserver die threads gebruikt voor verschillende requests af te handelen. Het nadeel van Rails en Mongrel bestaat er echter uit dat het aantal threads niet tijdens uitvoering geregeld kan worden, maar op voorhand dient gecongureerd te worden en dus een statisch gegeven is. Het instantiëren van de verschillende Mongrel processen gebeurt eenvoudig via Mongrel_cluster. Om de requests te verdelen over de verschillende processen, maken we gebruik van de eenvoudige software load balancer Pen
16 , die tevens als front-end dient voor gebruikers van
buitenaf. De Mongrel instanties kunnen dus niet van buitenaf aangeroepen worden, maar alle aanvragen passeren eerst de load balancer. We hebben achtereenvolgens één, drie, zeven en tien Mongrels instanties opgestart en dezelfde test als hierboven herhaald.
De resultaten zonder caching in guur 5.17 en met caching in guur 5.18 hebben
16 http://siag.nu/pen/ 105
Figuur 5.16: Concurrente gebruikers: Mongrel vs Webrick (één proces, caching)
echter wat extra uitleg nodig.
In eerste instantie is te zien dat de gemiddelde antwoordtijd bij drie
en zeven Mongrel instanties slechts licht daalt. Dit is ook logisch te verklaren, aangezien processen op een niet-parallelle computer ook niet gelijktijdig kunnen uitgevoerd worden en er dus slechts winst kan gemaakt worden wanneer andere processen blokkeren of lang duren.
In die zin geeft deze test in feite
een vertekend beeld met de korte list_categories actie en zou het beter zijn dat verschillende acties door elkaar en tegelijk aangeroepen worden om een realistisch beeld te krijgen.
Zo hebben de kort durende
acties zeker baat bij meerdere procesinstanties die hun request kunnen afhandelen wanneer bijvoorbeeld één instantie een show_meal een aanvraag aan het afhandelen is. Helaas hebben we geen tool of manier ontdekt om zo een test op te stellen, maar de bekomen resultaten geven alvast een goede indicatie omtrent het realistisch gedrag.
Figuur 5.17: Verschillend aantal Mongrel instanties (geen caching)
106
Figuur 5.18: Verschillend aantal Mongrel instanties (met caching)
Uit de guren 5.17 en 5.18 blijkt dat een conguratie met zeven Mongrel processen de beste optie is voor deze applicatie. Om het beste aantal Mongrel instanties te bepalen, bestaat er geen vaste formule. We zijn dus jammergenoeg aangewezen op trial-and-error werk om dit cijfer te bepalen. Indien we guur 5.18 en de gemiddelde uitvoeringstijd van de verschillende acties in achting nemen (zie sectie 5.7.13), kunnen we verwachten dat de applicatie in staat is een antwoord te leveren binnen een niet-storende gemiddelde antwoordtijd wanneer het aantal gelijktijdige gebruikers rond de 100 ligt. Merk op dat het hier wel gaat om 100 gebruikers die constant de applicatie gebruiken, wat in de praktijk vrij onrealistisch zou zijn waardoor dit cijfer dus waarschijnlijk positiever zal uitdraaien. We wensen echter nogmaals aan te stippen dat de situatie helemaal anders zou zijn wanneer de gebruikers voortdurend
show_meal zouden aanroepen. Maar zoals opgetekend in sectie 5.7.13, zal dit in de praktijk meevallen. Algemeen gezien zijn we dus best tevreden omtrent de performantie van de applicatie.
5.7.16
Fysieke distributie en concurrente gebruikers
Tot op dit moment hebben we de Rails applicatie beperkt tot slechts één fysieke machine.
In sectie
1.4.9 werd reeds aangehaald dat Rails ontworpen is volgens het idee van een share-nothing architectuur. Concreet wil dit zeggen dat verschillende fysieke servers met dezelfde Rails applicatie kunnen aangewend worden om dezelfde requests te beantwoorden. Het is met andere woorden onbelangrijk welke server het antwoord geeft, daar het antwoord onafhankelijk is van de specieke server. We hebben dit idee reeds toegepast in vorige sectie waar de verschillende Mongrel instanties de requests beantwoorden. Als laatste test wensen we nu te achterhalen of een Rails applicatie makkelijk kan geschaald worden door middel van fysieke distributie. Het doel van deze test is niet nagaan wat de beste hoeveelheid fysieke servers voor onze applicatie is, maar controleren of de applicatie kan schalen wanneer performantie werkelijk een probleem zou vormen. Onze testopstelling wordt schematisch weergegeven in guur 5.19 en bestaat uit drie verschillende machines.
De twee server machines zijn de machines die we eerder gebruikt hebben
voor de andere tests doorheen de voorgaande secties.
De derde machine draait enkel de software load
balancer (Pen), om de server machines niet te belasten met het verdelen van de requests. Noteer dat op de guur geen databank is weergegeven, maar dat in de praktijk uiteraard een gemeenschappelijke databank 107
nodig is.
Merk ook op dat de hardware van de servers verschillend is.
Dit is niet eens zo een slechte
situatie, aangezien in vele bedrijven een nieuwe server pas wordt bijgeplaatst wanneer de performantie een probleem is of dreigt te worden. De kans dat dan fysiek dezelfde machine aangekocht wordt, is eerder klein.
Figuur 5.19: Testopstelling voor de fysieke gedistribueerde test
Het conguren van de testopstelling ging zeer vlot en is zelfs eenvoudiger dan een lokale distributie met verschillende Mongrel processen.Wanneer we dezelfde test als in vorige sectie toepassen op de conguratie, zijn de resultaten zeer gunstig.
De metingen in guur 5.20 tonen overtuigend aan dat de beste lokale
conguratie (zeven Mongrel instanties) absoluut geen partij is voor een conguratie waarbij de applicatie fysiek gedistribueerd is, zelfs als het maar om twee gedistribueerde processen gaat. Zelfs bij 300 concurrente gebruikers blijft de gemiddelde antwoordtijd binnen aanvaardbare grenzen.
Merk op dat op de servers
afzonderlijk ook nog eens verscheidene Mongrel instanties kunnen draaien.
Figuur 5.20: Twee fysiek gedistribueerde Mongrel processen vs zeven lokale Mongrel processen
Het is dan ook zonder meer duidelijk dat het schalen van een Rails applicatie zeker en vast nuttig is. Het feit dat het ontwerp van Rails hiermee rekening houdt waardoor een gedistribueerde opstelling zo eenvoudig kan bekomen worden, kan dan ook enkel bejubeld worden. Helaas ontbrak ons de nodige hardware om meer tests omtrent fysieke distributie uit te voeren. We zijn er echter van overtuigd, gesteund door de
108
metingen in guur 5.20, dat de applicatie nog heel wat performanter kan gemaakt worden door gewoon wat servers toe te voegen aan de opstelling.
5.8 Samenvatting: de applicatie in vogelvlucht Het doel bij de start van de ontwikkeling van de applicatie was de functionaliteit van de originele J2EE applicatie te evenaren.
Dit doel is zeker en vast gehaald en zelfs voorbij gestreefd naar het einde van
iteratie vier. Doorheen de bespreking van de iteraties hebben we doelbewust veel minder code aangehaald dan we in werkelijk geïmplementeerd hebben. De code is integraal te inspecteren op de CD-ROM bij dit werk. De applicatie bevat in totaal ongeveer 2700 werkelijke regels code, waarvan ongeveer 1100 regels logica (zonder HTML) en de rest testcode. De code coverage (berekend met RCov, zoals vermeld in sectie 5.4.7) is algemeen gezien zeer hoog (in het merendeel van de klassen meer dan 90%) . De statistieken hiervan staan in HTML formaat op de CD-ROM. Wanneer we de omvang van de applicatie, gemeten in aantal klassen en bestanden, beschouwen kunnen we concluderen dat dit aantal vrij goed meevalt in vergelijking met de functionaliteit van de applicatie. Er werden 61 templates en één layout geïmplementeerd. De applicatie telt zes controllers, de controller voor de webservice inclusief. Er zijn 14 modellen en 16 databanktabellen aangemaakt doorheen de verschillende iteraties. Tot slot, de functionaliteit van de applicatie in een notendop:
•
Op het vlak van beveiliging is de applicatie voorzien van
authenticatie en authorizatie (zie
sectie 5.3.6). Dit garandeert dat ongepriviligeerde gebruikers de applicatie niet kunnen gebruiken en bepaalde delen van de applicatie voor sommige gebruikers meer restricties hebben dan andere.
•
Op het vlak van administratie is voorzien in
gebruikersbeheer en het beheer van rollen en rechten
voor het authorizatiesysteem. Onder administratie verstaan we het bekijken, toevoegen, verwijderen, editeren en zoeken.
•
Gebruikers kunnen door alle
categorieën van voedsel bladeren, informatie over de nutriënten-
inhoud inwinnen en voedingsmiddelen naar keuze toevoegen aan de huidige maaltijd. Gebruikers kunnen daarnaast ook
•
speciek zoeken naar bepaalde voedingsmiddelen.
Gebruikers kunnen de huidige maaltijd opslaan en de inhoud aanpassen. Dit omhelst het wijzigen van de hoeveelheid van de consumptie of het volledig verwijderen van de consumptie.
• Opgeslagen maaltijden
kunnen bekeken worden via een kalender en eventueel ook terug verwij-
derd worden.
• •
Gebruikers kunnen instellen welke nutriëntenwaarden door de applicatie moeten berekend worden. Gebruikers kunnen toegewezen worden aan een bepaalde opvragen omtrent de eetgewoonten van hun patiënten. de applicatie.
dokter. Die dokter kan statistieken
Dokters kunnen ook nota's opslaan via
Ten slotte kunnen dokters ook de getoonde nutriëntenwaarden van hun patiënten
wijzigen.
109
5.9 Mogelijke uitbreidingen Een applicatie kan nooit helemaal af genoemd worden. Er is altijd wel iets dat ontbreekt of beter kan. Onze applicatie vormt dan ook geen uitzondering op deze regel. We sommen hieronder enkele mogelijke uitbreidingen op die naar boven zijn gekomen tijdens de besprekingen met de klant. Deze uitbreidingen hebben echter om diverse redenen (tijd, andere prioriteiten, ...) de nale versie niet gehaald.
• Internationaliseren van de applicatie
is zoals beschreven in sectie 1.6.3 een serieuze tekortko-
ming van Rails. We hebben de mogelijkheden voor internationalisering bestudeerd bij aanvang van de ontwikkeling, maar momenteel is er geen enkele adequate manier om volwaardige internationalisatie te bekomen. Het probleem ligt niet zozeer bij Rails, maar eerder bij Ruby dat ondersteuning voor UTF-8 ontbreekt. We verwachten echter dat het niet lang meer zal duren alvorens een oplossing gevonden wordt, aangezien veel ontwikkelaars er naar vragen.
•
Introductie van een
baliemedewerker. Op dit moment dient een administrator een patiënt te asso-
ciëren met een dokter. In realiteit zal dit gebeuren door administratief personeel van het ziekenhuis. Deze functionaliteit is in principe niet moeilijk toe te voegen aan de applicatie aangezien met het authorizatiesysteem de basis hiervoor al gelegd is. Enkel een aparte controller en views dienen dus geïmplementeerd te worden.
•
Op dit moment kunnen maaltijden niet meer heropend worden na persistentie. Dit was moeilijk te implementeren omdat de huidige maaltijd wordt opgezocht op basis van de meest recente datum. Maar als we de huidige maaltijd willen vervangen naar een oudere maaltijd moeten we ook de datum van deze oude maaltijd aanpassen, wat dan weer incorrect is bij de berekeningen voor de nutriëntenwaarden. Het zou dus een interessante toevoeging zijn om
maaltijden te kunnen heropenen
omwille van foutieve ingaves.
• Statistieken
kunnen in de huidige implementatie enkel gegenereerd worden door dokters. Het zou
handig zijn om dit ook te voorzien voor gebruikers.
•
Waarschuwingen voor het overschrijden van bepaalde nutriëntenwaarden houden momenteel enkel rekening met bovengrenzen.
Dit is ook logisch, aangezien het voor dieetpatiënten om het over-
schrijden van maximale waarden gaat. De applicatie zou echter ook kunnen gebruikt worden voor
ondergrenzen van nutriëntenwaarden, waarmee bijvoorbeeld diëten kunnen opgevolgd worden. De huidige implementatie bevat reeds modellen waarvoor deze ondergrenzen zijn gedenieerd, zodat weinig werk vereist zou zijn voor deze functie.
•
...
5.10 Evaluatie van toekomstbestendigheid Typisch is de ontwikkelingsfase slechts een minimale fractie van de totale levensduur van een applicatie. Het onderhouden van de applicatie vraagt op termijn een veel grotere kost dan het implementeren van de logica. Dit betekent dan ook dat een applicatie die beter onderhoudbaar is meer waardevol is dan een minder onderhoudbare. Maar welke factoren maken een applicatie beter onderhoudbaar? Volgens het Karlsson model (zoals gedenieerd in [7]) voor onderhoudbaarheid zijn er vijf attributen die een invloed hebben op de onderhoudbaarheid van software: consistentie, eenvoud, zelf-beschrijving, 110
modulariteit en testbaarheid.
We zullen deze vijf attributen opsommen en toepassen op de applicatie.
Deze studie werd voor het Rails framework reeds gedaan in een paper van Arjen Van Schie [12] en bekomt soortgelijke resultaten. 1. Op het vlak van
consistentie scoort onze applicatie zeer goed. In grote mate is dit te danken aan
het Rails framework en dan vooral het Convention over Conguration principe dat naar voren is gekomen in zo goed als elke iteratie. Zo weten we door naamconventies automatisch welke modellen mappen op welke tabellen en omgekeerd. We weten dankzij de directoryconventies perfect waar elk model, controller, view, helper, testklasse, plugin of bibliotheek zich bevindt. De vaste MVC architectuur bepaalt welke klassen er mogelijk zijn. We weten dankzij de oproepconventies welke views zullen opgeroepen worden na het oproepen van bepaalde controlleracties, etc. De consistentie wordt automatisch afgedwongen mits we de conventies volgen, waarbij we bovendien minder coderegels als winst bovenop krijgen. Dit wil zeggen dat wanneer een andere ontwikkelaar onze applicatie dient te onderhouden deze meteen zijn weg zal vinden doorheen de code, net omdat we overal de Rails conventies hebben gevolgd. 2.
Eenvoud omvat de complexiteit van code en het aantal regels code.
Op dat vlak slaagt onze
applicatie met glans. Er zijn slechts ongeveer 1100 regels code (plus ongeveer 1700 regels testcode) wat vrij weinig is voor een applicatie van deze omvang. Wederom is dit toe te schrijven aan het Convention over Conguration principe, dat zorgt voor een vermindering in zowel aantal lijnen code als een vermindering van de complexiteit van de code. Bovendien zorgt Rails door toepassing van het DRY principe dat er amper code herhaald dient te worden. Onze applicatie is niet een toevallige gelukstreer op het vlak van eenvoud. De studie van Arjen Van Schie toont aan dat de meeste Rails applicaties vrij weinig regels code tellen en gemiddeld niet boven de vijf regels per methode stijgen. 3. Met
zelf-beschrijving bedoelt men het feit of op eenvoudige wijze aan de code kan gezien worden
wat de intentie er van is. Hoewel dit moeilijk na te gaan is als ontwikkelaar van de applicatie, zijn we van mening dat de code vrij zelf-beschrijvend is. Op hoog niveau is de vaste Rails MVC architectuur in combinatie met de vaste directorystructuur hiervoor verantwoordelijk. Indien we namelijk code terugvinden in een bepaalde map, weten we meteen de functie van die code. De werkelijke applicatielogica is moeilijker te beoordelen en we zijn ons er van bewust dat bepaalde zaken (bijvoorbeeld de berekening van de nutriëntenwaarden bij een maaltijd) niet zo eenvoudig te begrijpen zijn voor een buitenstaander.
Voor deze logica kan men echter zeer eenvoudig documentatie schrijven die
17 makkelijk kan worden omgezet naar overzichtelijke HTML pagina's. Voorts zijn ook door RDoc de Unit en functionele tests een handig hulpmiddel om de interne werking van de applicatie duidelijk te maken. 4. De applicatielogica is in grote mate
modulair te noemen. Dit is niet zo moeilijk om in te zien,
aangezien Rails van bij de start de opsplitsing in deelcomponenten vastlegt door de verplichte MVC architectuur. 5. Op het punt van
testing kunnen we ook een vrij duidelijk standpunt innemen. Het feit dat het
testframework een inherent deel is van het Rails framework kan alleen maar toegejuicht worden. Op die manier wordt de drempel om te testen verlaagd, omdat testklassen naadloos kunnen gebruik maken van de andere componenten van Rails. Er is amper een leercurve of conguratie vereist om tests te kunnen schrijven. Het framework zelf dwingt het schrijven van testen niet af, maar geef wel de eerste aanzet door de juiste (stub) les aan te maken bij de creatie van modellen of controllers.
17 http://rdoc.sourceforge.net/ 111
Wanneer we bovenstaande attributen als maatstaf gebruiken voor onderhoudbaarheid, kunnen we concluderen dat onze applicatie vrij goed scoort. Het is ook opvallend dat we amper moeite hebben moeten doen om de eigenschappen van bovenstaande punten te verhogen, en dat het framework hierop een grote invloed heeft.
We kunnen algemeen stellen dat, vooral mits het volgen van de conventies, men op
eenvoudige wijze een onderhoudbare applicatie bereikt door gebruik te maken van het Rails framework. Uiteraard dient men als ontwikkelaar nog steeds het gezond verstand te gebruiken om de intenties van Rails niet ongedaan te maken.
112
Hoofdstuk 6
Eindbeschouwingen & conclusies In deze scriptie hebben we onderzoek verricht naar zowel de theoretische als de praktische kant van het Ruby on Rails framework. Tijdens het uitwerken van de nefrologie case studie hebben we de Agile Development methodologie aangewend om het ontwikkelingsproces te sturen, om zodoende zowel de methodologie als de ondersteuning van Rails ervoor te kunnen evalueren. Hieronder formuleren we hieromtrent enkele conclusies, die duidelijk naar voren zijn gekomen tijdens het onderzoek.
6.1 Conclusies Ruby on Rails kan het best omschreven worden als een totaalpakket voor het ontwikkelen van webapplicaties. Het is een opionated en een vooral praktisch ingesteld framework. Dit betekent dat het framework een bepaalde manier van ontwikkelen vooropstelt, bepaalde beslissingen neemt die de meeste ontwikkelingsprocessen ten goede komen en vooral poogt om productiviteitswinst te boeken.
Rails maakt alle
facetten van het ontwikkelingsproces zoals implementatie, testen, optimalisatie en onderhoud eenvoudig door voor elk van deze fasen voorzieningen aan boord te hebben en deze perfect op elkaar af te stemmen. Rails is opgebouwd rondom een handvol concepten die rigoreus worden toegepast doorheen het framework. Frappant is dat deze concepten vaak meer lososch dan technisch van aard zijn en hierdoor Rails vaak radicaal verschilt met andere, veelal traditionele frameworks. Op het hoogste niveau is de onveranderlijke MVC architectuur opmerkenswaardig. Rails tilt bovendien het gebruik van MVC naar een hoger niveau. Wanneer men namelijk ontwikkelt met het Rails framework, is men steeds aan het werk in één van de drie MVC componenten. Er is een vaste plaats voor elk mogelijk stuk code en alle componenten van de applicatie interageren op een standaard manier. Het is alsof Rails voor ons de schetsen heeft gemaakt, en wij enkel nog moeten inkleuren. Het DRY en Convention over Conguration principe zijn in Rails nooit veraf.
Zoals doorheen de case
studie zeer duidelijk gebleken is, zijn deze twee mantra's de sleutel voor veel van de magie achter het framework.
Conventies zijn cruciaal bij het ontwikkelingsproces, maar deze worden nooit als verplicht
doorgevoerd door het framework. Mits de conventies echter gevolgd worden, is het uitdrukkelijk gebleken dat het framework een enorme hoeveelheid werk in eigen handen neemt. Het uitgangspunt bij het ontwerp van Rails is om een zo hoog mogelijke productiviteit toe te laten. De case studie heeft ook meer dan eens aangetoond dat dit geen loze woorden zijn. 113
De reden van de
hoge productiviteit is het specieke karakter van Rails.
Door zich te focussen op de niche markt van
de webapplicaties, kan men bepaalde zaken als vanzelfsprekend aannemen. In combinatie met conventies wordt de kracht van deze speciciteit nog verhoogd, maar dit betekent echter ook dat Rails soms niet alle wensen of strategieën ondersteunt, en de voordelen van Rails zelfs kunnen verdwijnen of een hinder vormen in bepaalde gevallen. Wanneer men echter blijft binnen de niche van Rails, namelijk webapplicaties met een database back-end, kunnen we gesteund door de nefrologie caste studie besluiten dat Rails uitermate geschikt is voor deze steeds belangrijker wordende markt. Ten slotte kunnen we nog het facet van Agile Development belichten. Het werken in iteraties gecombineerd met veel communicatie is positief gebleken voor zowel ontwikkelaar als klant. Enerzijds is de klant meer tevreden omdat hij werkelijk invloed heeft op de ontwikkeling en na iedere iteratie de investeringen meer resultaat ziet hebben.
Anderzijds is Agile Development ook gunstig voor de ontwikkelaar.
Fouten of
misinterpretaties hebben namelijk een grotere kans om sneller opgemerkt te worden in vergelijking met tradionele methoden. In wezen is Agile Development niet zo verschillend van tradionele ontwikkelingsmethoden. Men beschikt over dezelfde fasen en technieken, maar men werkt op een veel kleinere schaal. Hoewel strikt gezien Agile Development kan aangewend worden in elk project, is het onbetwistbaar dat het gehanteerde framework en tools een grote rol spelen in het succes van de methodologie. Zoals gebleken, heeft Rails vele eigenschappen (migraties, een korte feedbackloop, etc.) die inspelen op de vereisten van Agile Development. Rails kan zeer makkelijk omgaan met verandering, wat essentieel is wanneer men Agile wil ontwikkelen. Een ander enorm voordeel van Rails is dat op elk moment in de ontwikkeling een werkend product kan getoond worden, wat inspeelt op de overvloedige interactie met de klant. We kunnen dan ook stellen dat Agile Development met Rails een perfect huwelijk is.
Agile Development op zich is zeker een valabele
ontwikkelingsmethodologie te noemen, maar in de combinatie met Rails zijn we er van overtuigd dat het een industriewaardige manier van ontwikkelen is.
6.2 Eindbeschouwing Sinds de start van deze scriptie is de belangstelling voor Rails van het grote publiek enorm toegenomen. Toen we in augustus 2006 startten met dit werk, waren er slechts twee Engelstalige boeken op de markt, waar dit nu na minder dan een jaar tijd al meer dan dertig zijn. Rails evolueert heden nog steeds aan zeer hoog tempo. Het is in de eerste plaats af te wachten of het prille enthousiasme met de tijd niet zal temperen. We hopen echter dat dit werk heeft laten inzien dat de reden tot het enthousiasme gegrond is. Maar als ons onderzoek in dit werk kan of mag dienen als enige indicatie, hebben we er alle vertrouwen in.
114
Bibliograe [1] David Black. Ruby for Rails: Ruby Techniques for Rails Developers. Manning Publications, 2006. [2] Jamis Buck. Integration Testing in Rails 1.1 [online]. 2006. [3] Prof. Dr. Ir. Frank Gielen. Cursus Software Architectuur. Universiteit Gent, 2006. [4] Chad Fowler. Rails Recipes. The Pragmatic Programmers, 2006. [5] Martin Fowler. Domain Specic Languages [online]. 2004. [6] Jesse James Garrett. Ajax: A New Approach to Web Applications [online]. 2005. [7] Even-André Karlsson. The reuse of software: a holistic approach. Wiley, 1995. [8] Yukihiro Matsumoto. The Ruby Programming Language [online]. 2000. [9] Tim O'Reilly. What Is Web 2.0 - Design Patterns and Business Models for the Next Generation of Software [online]. 2005. [10] John Ousterhout.
Scripting:
Higher level programming for the 21st century.
IEEE Computer,
31(3):2330, 1998. [11] Trygve M. H. Reenskaug. Thing-Model-View-Editor - an example from a planningsystem. 1979. [12] Arjen Van Schie. The ease of Ruby on Rails maintenance (thesis summary). University of Amsterdam, 2007. [13] Bruce Tate. Crossing borders: What's the secret sauce in Ruby on Rails? [online]. 2006. [14] Bruce Tate. From Java to Ruby: Things Every Manager Should Know. Pragmatic Bookshelf, 2006. [15] Roy T. Fielding & Richard N. Taylor.
Principled design of the modern web architecture.
ACM
Transactions on Internet Technology, 2(2), 2002. Programming Ruby: The Pragmatic Programmers'
[16] Dave Thomas, Chad Fowler, and Andy Hunt.
Guide. Pragmatic Bookshelf, 2004 (2nd ed.). [17] David Heinemeier Hansson & Dave Thomas.
Agile Web Development with Rails.
The Pragmatic
Programmers, 2nd edition, 2006.
Referenties waar [online] bij staat betreen online artikels van invloedrijke auteurs. terug te vinden op de bijhorende CD-ROM in pdf formaat.
Op de CD-ROM zijn tevens ook artikels
waarnaar verwezen wordt in voetnoten (blogs, etc.) opgenomen.
i
Deze artikels zijn