Een Commandline Aanpak voor JavaScript Gebaseerde Interfaces

In deze blogpost bespreek ik een alternatieve aanpak voor het opzetten van JavaScript [1] gebaseerde interfaces. Dit heeft voornamelijk betrekking op webapplicaties en niet zozeer op websites in het algemeen. Juist in het geval van webapplicaties is er een grote mate van interactie met de gebruiker, aangezien in dat geval applicatie en gebruiker samen moeten werken in het uitvoeren van een taak, terwijl het bezoeken van een webpagina grotendeels eenrichtingsverkeer is. Er vindt dus communicatie plaats tussen het client side gedeelte van de applicatie (de interface) en de gebruiker. Uiteraard vindt er ook communicatie plaats tussen de interface en de server waarop de applicatie draait. In het eenvoudigste geval bestaat deze communicatie uit het ophalen van (HTML) pagina’s en het versturen van formulieren.

De klassieke gebruiker interactie cyclus

De klassieke vorm van interactie van de gebruiker met de webapplicatie, de gebruiker interactie cyclus of user interaction cycle is weergegeven in onderstaand figuur:

De klassieke gebruiker interactie cyclus

Met deze vorm van interactie is iedereen wel bekend, omdat het de manier beschrijft waarop zo’n beetje elke website en webapplicatie werkt. De gebruiker vraagt een pagina op (request). Op de een of andere niet zo relevante manier, zorgt de server ervoor dat de gevraagde informatie opgehaald wordt uit een datastore (file of database) en als een HTML pagina teruggestuurd wordt naar de client (response). Dit is het hele eieren eten.

Als er data verstuurd moet worden naar de server, bijvoorbeeld omdat er een formulier verzonden wordt, wordt dit verpakt in de request door middel van parameters. De server doet vervolgens iets met deze data en stuurt ook weer een response terug. Heel simpel gezegd is dit te beschouwen als een Model-View-Controller systeem waar de View de HTML pagina is die de gebruiker voor z’n neus krijgt en het Model en de Controller het geheel van data opslag en server logica voorstellen.

Dit werkt al vijftien jaar prima, maar het heeft wat nadelen. Het komt vaak voor dat de gebruiker na het verzenden van data weer teruggestuurd wordt naar de oorspronkelijke pagina. Bijvoorbeeld in het geval dat een formulier niet helemaal juist is ingevuld, of als een reactie wordt geplaatst onder een blog of forum post. In zo’n geval is het helemaal niet nodig om de hele pagina te verversen, een gedeelte ervan verversen is meer dan genoeg, het overgrote deel van de data is toch al aanwezig. Bovendien, en dat heeft vooral te maken met gebruikersbeleving, is het vervelend als een pagina weer terugspringt naar boven.

Om deze gebruikersbeleving te verbeteren is door Microsoft het xmlHttpRequest object in JScript ingebakken (sinds Internet Explorer 5.0 - het gaat dus al bijna 9 jaar mee). Dit zorgde ervoor dat men vanuit JavaScript een request kon genereren en een response kon verwerken. Voornamelijk door onbekendheid en gebrek aan ondersteuning binnen andere browsers is deze techniek weinig toegepast. Een veelgebruikte alternatieve techniek was gewoon “echt” een pagina opvragen in een verborgen frame (of iframe) en de inhoud ervan vervolgens verwerken (meestal kopieren) met behulp van JavaScript. Tegenwoordig is het erg hip geworden gebruik te maken van het xmlHttpRequest object. Vaak echter is het in de gebruikte situatie overbodig en verergert de gebruikerservaring alleen maar (doordat bijvoorbeeld niet meer gebruik kan worden gemaakt van de navigatiemogelijkheden die de browser biedt, zoals een “terug” knop).

Beide technieken veranderen helemaal niets aan de gebruiker interactie cyclus. Nog steeds wordt data verzonden en opgehaald via het (relatief langzame) internet. De gebruikerservaring verbetert wel, er wordt in het algemeen minder data opgehaald, en ook wordt niet meer heel de pagina ververst als dat niet nodig is [2].

De JavaScript gebaseerde gebruiker interactie cyclus

Willen we de gebruikerservaring daadwerkelijk verbeteren, dan moeten we er voor zorgen dat niet voor elk wissewasje met de server gecommuniceerd hoeft te worden. Dat kost namelijk tijd en gebruikers hebben een hekel aan wachten. Om de communicatie met de server minder nodig te maken, heb je de data nodig aan de client kant, een soort buffer dus. Soms is dit triviaal, voor het sorteren van een tabel bijvoorbeeld is geen nieuwe request nodig (en ook geen nieuwe query richting database met een gewijzigde SORT parameter). Alle benodigde data is namelijk al bekend bij de client, dus sorteren kan veel eenvoudiger en sneller met JavaScript. Ook het filteren van of het zoeken binnen een lijst die toch al binnen is gehaald, kan je het beste met JavaScript doen. Met deze gedachte in het achterhoofd beschouwen we een JavaScript gebaseerde interactie cyclus:

De JavaScript gebaseerde interactie cyclus

Wat in bovenstaande figuur direct duidelijk is, is de toegenomen hoeveelheid blokjes. Het ziet er meteen een stuk ingewikkelder uit. Belangrijk hierin is het toevoegen van een Controller en een Model aan de client kant. De Controller is verantwoordelijk voor het controleren van de interactie met de gebruiker. Deze Controller communiceert met de View en met het Model. Het Model is een afspiegeling van het Model op de server. Het hoeft vaak niet een complete kopie te zijn, maar functioneert als een soort buffer. Communicatie met de server is natuurlijk nog steeds belangrijk, maar is nu verschoven naar een tweede niveau en vindt alleen plaats als er data moet worden weggeschreven of opgehaald. Deze communicatie met de server kan dan ook voor een groot deel plaats vinden zonder dat de gebruiker daar iets van merkt.

Communicatie door middel van deze command line aanpak

In bovenstaande (JavaScript gebaseerde) interactie cyclus vindt op een drietal plaatsen communicatie plaats.

  • Communicatie van de gebruiker naar de Client Controller
  • Communicatie van de Client Controller naar de Server Controller
  • Communicatie van de Server Controller naar de Client Controller

Al deze drie vormen van communicatie gaan in het algemeen op een andere manier. Het eerste betreft vaak interactie door middel van muis en toetsenbord, het tweede betreft het verzenden van requests naar de server, al dan niet met parameters. Het derde betreft het ontvangen van een response van de server, waarvan de inhoud kan bestaan uit HTML of ruwe data (op wat voor manier dan ook). Of de communicatie in de laatste twee vormen plaatsvindt via een klassieke request of door middel van het xmlHttpRequest object, doet hier niet terzake.

Als deze drie vormen van communicatie op dezelfde manier plaats zouden vinden, zou dat wat voordelen kunnen bieden. Meer specifiek: ik zal hier een methode beschrijven om deze communicatie op een commandline achtige manier te laten plaatsvinden.

De belangrijkste reden om voor een commandline aanpak te kiezen is transparantie. Het is tekst gebaseerd en makkelijk te lezen en te schrijven door mensen, iets waar JSON al een stuk minder duidelijk is. Een voorbeeld van een command is het volgende:

Add user Rikkert Koppes

In het algemeen bestaat het command uit een werkwoord, eventueel een onderwerp en een serie parameters. Het is duidelijk wat met bovenstaand command bedoeld wordt. Door een Command interpreter zou bovenstaand command vertaald kunnen worden in de volgende functie aanroepen:

Add('user','Rikkert','Koppes');   //of
Add_user('Rikkert','Koppes');     //of
user.Add('Rikkert','Koppes');

Welke van deze methoden gebruikt wordt laat ik hier buiten beschouwing, dat is een kwestie van afspraken en smaak. Het gaat erom dat een command vertaald kan worden naar iets wat een machine begrijpt, namelijk een functie aanroep.

Uitvoeren van Commands

De opdeling in communicatievormen kan iets worden gewijzigd, namelijk in communicatie die plaatsvindt vanuit de client (door de gebruiker of door de controller geïnitieerd, bijvoorbeeld periodiek) en communicatie die plaatsvindt vanuit de server (na een request van de client, of autonoom, bijvoorbeeld periodiek). Houden we deze indeling aan, dan komen we op de volgende view scenario’s:

  1. Client geïnitieerd
  2. Client geïnitieerd met ruggespraak (synchroon)
  3. Client geïnitieerd met ruggespraak (asynchroon)
  4. Server geïnitieerd

Elk van deze scenario’s komt hierna aan bod.

1. Client geïnitieerd

Een door de client geïnitieerde command afhandeling.

Als door de client een command wordt geïnitieerd, bijvoorbeeld doordat een command in een commandline interface wordt ingetypt, doordat de gebruiker een actie uitvoert in een grafische interface, of automatisch periodiek, wordt het command geïnterpreteerd en uitgevoerd. In het algemeen houdt dit in dat het model (let wel: hier het client side model) wordt gewijzigd. Daarna wordt eventueel een extra actie uitgevoerd, te denken valt bijvoorbeeld aan het updaten van de view.

2. Client geïnitieerd met ruggespraak (synchroon)

Een door de client geinitieerde command afhandeling met ruggespraak (synchroon).

Soms is het nodig dat het command doorgestuurd (gedelegeerd) wordt naar de server. In dat geval wordt het command niet op de client geïnterpreteerd, maar via een request doorgestuurd naar de server en daar geïnterpreteerd en uitgevoerd, wat in het algemeen inhoudt dat het model op de server wordt gewijzigd. Vervolgens kan de server één of meerdere commands terugsturen in de response (eventueel alleen het initieel ingevoerde command), waarna deze ook op de client wordt geïnterpreteerd en uitgevoerd. Dit is bijvoorbeeld weer het updaten van het model, zodat modellen op de server en op de client synschroon lopen. Hierna kan eventueel weer een extra actie worden uitgevoerd. Mocht er op de server iets zijn fout gegaan, dan kan ook dat in een command worden teruggecommuniceerd, waarna typisch een melding wordt gedaan naar de gebruiker.

3. Client geïnitieerd met ruggespraak (asynchroon)

Een door de client geïnitieerde command afhandeling met ruggespraak (asynchroon).

Een alternatief op bovenstaand scenario is de asynchrone aanpak, waarin het command zowel op de client direct wordt uitgevoerd als naar de server gestuurd. Dit gaat prima, als het command zowel op de client als op de server hetzelfde resultaat heeft. De gebruiker heeft dan sneller feedback van zijn actie en de interactie als geheel voelt sneller aan. Mocht er echter iets fout gaan op de server, bijvoorbeeld doordat een gebruiker geen rechten heeft om een bepaalde actie uit te voeren (dit kan op de client nooit waterdicht worden gecontroleerd), dan lopen de modellen uit de pas en moet de op de client uitgevoerde actie teruggedraaid worden. Het is belangrijk hier rekening mee te houden als je kiest voor een asynchrone aanpak.

4. Server geïnitieerd

Een door de server geïnitieerde command afhandeling.

Ook de server kan commands initieren. Dit kan bijvoorbeeld plaatsvinden op het moment dat de pagina laadt, of in de vorm van een zogenaamde snail http communicatie [3]. Dit houdt in dat de server (expres) erg lang doet om een pagina te serveren. Door op deze manier de verbinding “open” te houden kan periodiek (zeg elke seconde), data worden verstuurd naar de client zonder dat daar een request aan vooraf is gegaan. Google Docs & Spreadsheets maakt bijvoorbeeld gebruik van deze techniek. De afhandeling van het command aan de client kant verloopt verder net als bij het client geïnitieerde scenario zonder ruggespraak.

Code!

Hoe gaat een en ander dan in z’n werk? We willen nu natuurlijk wel eens wat voorbeeldcode zien. Komt ie! Allereerst moeten we een command interpreter hebben, oftewel iets wat er voor zorgt dat we commands (dat zijn gewoon strings) kunnen vertalen naar een method aanroep:

com.finalist.Command = function() {
  var register = function(name,funcRef) { /*snip*/ };
  var doCommand = function(commandString) { /*snip*/ };
  var log = function(message) { /*snip*/ };
  var bindCli = function(HTMLElement) { /*snip*/ };
  var bindLog = function(HTMLElement) { /*snip*/ };
  return {
    register: register,
    doCommand: doCommand,
    log: log,
    bindCli: bindCli,
    bindLog: bindLog
  }
} ();

De invulling van de methods heb ik hier even niet meegenomen. De vijf bovenstaande methods zorgen ervoor dat je een command kan registeren onder een bepaalde naam (een string), een command kan uitvoeren door een commandstring in te voeren, een bericht kan loggen (en weergeven als daar een element voor beschikbaar is) en een HTML element koppelen als input en output. De laatste twee zijn erg handig in de ontwikkelingsfase.

Vervolgens definiëren we het model van de applicatie. Zoals eerder gezegd hoeft dit niet een exacte kopie te zijn van het model op de server. Ter illustratie heb ik een simpel model opgenomen met één klasse en wat methods.

monsterApplication.model = function() {
  var Monster = function(name,gender) { /*snip*/ };
  Monster.prototype.die = function() { /*snip*/ };
  Monster.prototype.wedTo = function(otherMonster) { /*snip*/ };
  Monster.prototype.giveBirth = function(childName) { /*snip*/ };
  var eve = new Monster('eve','female');
  return {
    Monster: Monster,
    eve: eve
  }
} ();

Vervolgens definieren we een controller:

monsterApplication.controller = function() {
  var Command = com.finalist.Command;
  var model = monsterApplication.model;
  var mate = function(cmd,father,mother,childName) { /*snip*/ };
  var population = function() { /*snip*/ };
 
  Command.register('mate',mate);
  Command.register('population',population);
  Command.register('pop',population);
} ();

In de controller wordt de Command en de model “package” geimporteerd en worden twee functies gedefinieerd. Deze functies willen we aan de gebruiker aanbieden en dus registeren we ze als command. Merk op dat de population functie onder twee namen wordt geregistreerd. Dit command is dus onder twee synoniemen aan te roepen.

Als laatste definieren we een view. Aangezien deze applicatie nog in ontwikkeling is, maken we even snel een commandline interface:

monsterApplication.view = function() {
  var Command = com.finalist.Command;
  Command.bindCli(document.getElementById('cli'));
  Command.bindLog(document.getElementById('log'));
} ();

En de bijbehorende HTML:

<!doctype html>
  <title>Monsterapplication example</title>
  <script type="text/javascript" src="Command.js"></script>
  <script type="text/javascript" src="monsterApplication.model.js"></script>
  <script type="text/javascript" src="monsterApplication.view.js"></script>
  <script type="text/javascript" src="monsterApplication.controller.js"></script>
  <input id="cli">
  <pre id="log"></pre>

We kunnen nu commands intypen in het input veld en het geheel output laten genereren in het pre element.

Voordelen en nadelen

De voordelen van deze aanpak zijn onder andere:

Use cases vertalen rechtstreeks naar commands
Vaak wordt in de ontwerpfase van een applicatie gebruik gemaakt van use cases, kort gezegd dingen die je met je applicatie wilt kunnen doen. Het is logisch dat alles wat je wilt kunnen doen, te doen moet zijn op jouw commando. Vaak blijken use cases zich dus direct te vertalen in commands.
Helpt je naar een Model-View-Controller aanpak
Een MVC aanpak is geen voorwaarde of consequentie van een commandline aanpak, maar ik heb gemerkt dat het er eigenlijk redelijk automatisch uit voortvloeit. Alles wat commands doen is namelijk dat wat de controller moet doen. Waar de controller in de praktijk uit gaat bestaan zijn precies de command handlers, plus eventueel wat ondersteunende functies.
De mogelijkheid te ontwikkelen zonder een grafische interface.
Tijdens de ontwikkeling van een applicatie komt het soms voor dat een bepaalde functionaliteit technisch al af is en getest kan worden, maar dat er nog geen grafische interface (GUI) voor is. In dat geval is het redelijk eenvoudig een command line interface beschikbaar te stellen via welke de gebruiker (tester) direct met het systeem kan communiceren.
Powerusers kunnen sneller werken
Powerusers zijn gebruikers die veelvuldig met het systeem werken en de behoefte hebben terugkerende taken op een snelle manier uit te voeren. Door de commandline interface ook in het eindproduct beschikbaar te stellen voor deze gebruikers, zouden ze een stuk sneller kunnen werken met de applicatie.
Er is een alternatieve interface beschikbaar voor visueel gehandicapten
Webapplicaties zijn voor een groot deel gericht op muis en visuele interactie. Visueel gehandicapten kunnen profijt hebben van een tekst gebaseerde interface, waar zij eenvoudig commando’s kunnen intypen. Als de resultaten van hun handelingen ook via een tekstuele uitvoer beschikbaar wordt gemaakt, kunnen zij op deze manier de grafische interface volledig omzeilen. Via bijvoorbeeld voicexml (wat gedeeltelijk geïmplementeerd is in Opera), zou zelfs een aanvullende spraakgestuurde interface ontworpen kunnen worden.
Eén helder communicatieprotocol
Aangezien alle communicatie plaatsvindt met behulp van commands, is het eenvoudig om bijvoorbeeld het serverside gedeelte van de command afhandeling te testen door commands direct als request parameter mee te sturen. Ook kunnen commands door het systeem (de Client Controller of Server Controller), eenvoudig worden gedelegeerd naar een ander deel van het systeem.
Je kan het weggooien als je klaar bent met ontwikkelen
Als je de commandline aanpak gebruikt voor testen, kan je het weer weggooien als je daarmee klaar bent. Als je grafische schil af is, en je besluit geen alternatieve tekst gebaseerde interface aan te bieden voor je gebruikers, en je hebt het ook niet nodig voor communicatie tussen server en client, dan kan je het zo weer weggooien.

Natuurlijk zijn er ook nadelen. Om maar wat te noemen:

Overkill
Voor kleine applicaties is een dergelijke aanpak wellicht overkill. Zeker als niet met de server wordt gecommuniceerd, vallen al twee vormen van communicatie weg. Wat dan overblijft is slechts een JavaScript aangelegenheid en het lijkt wat overdreven communicatie binnen JavaScript via een string based protocol te doen.
Wéér een protocol
Tja, het is inderdaad weer iets nieuws. We hadden al JSON, SOAP etc en dit is wéér een nieuw dingetje. Dat maakt het combineren van technieken er natuurlijk niet makkelijker op.

Samenvattend

Weinig concrete code, ik probeer hier dan ook voornamelijk een aanpak over te brengen. Het gebruiken van een command gebaseerde interface houdt de boel voornamelijk overzichtelijk, zeker als je met grotere JavaScript projecten aan de gang gaat. Het concreet invullen van de command interpreter is redelijk eenvoudig: split een string op spaties, bouw wat trucjes in om spaties in parameters toe te staan (iets met quotes en een regular expression ofzo) en roep de apply method van je geregistreerde functie aan. Communiceren met de server kunnen we tegenwoordig allemaal wel.

Voetnoten

  1. Waar ik over JavaScript spreek, bedoel ik ook JScript.
  2. Het gebruik van het xmlHttpRequest object is zeer populair geworden onder de naam AJAX (Asynchronous JavaScript And XML). Dit is echter een vrij misleidende en nietszeggende term, aangezien in het dagelijks gebruik vrijwel nooit sprake is van XML en het ook zelden echt asynchroon wordt toegepast. Ik vermijd de term dan liever ook.
  3. Snail HTTP lijkt heel erg op een HTTP push techniek, maar dat is het strict genomen niet. HTTP push bestaat wel, maar is nooit door Internet Explorer ondersteund. Wat hier gebeurt is een bijzonder trage response op een request (dus geïnitieerd door een client pull), waarbij periodiek verse informatie wordt gestuurd. De typische duur van zo’n response is zo’n 1 - 2 minuten.

—————————————————————————————
Meer weten over Java-specialist Finalist IT Group?


4 reacties »

  1. Interessant aanpak maar ik heb toch het gevoel dat die extra command processing laag meer problemen op gaat leveren dan ze op zal lossen. Het parsen van deze commandline is namelijk niet zo simpel als je aangeeft. Zo is het voorbeeld dat je geeft, “Add user Rikkert Koppes”, al niet triviaal. Ik de meeste gevallen worden de voor- en achternaam in aparte kolommen opgeslagen en met een naive parser haal ze niet uit elkaar. Okee dan passen ons taaltje aan; “Add user ‘Rikkert’ ‘Koppes’” waar het eerste argument de voor- en het laatste argument de achternaam is. O nee, dat werkt natuurlijk ook niet omdat je altijd uitslovers hebt met een naam als Remco van ‘t Veer.

    Als het dan gelukt is om een mooi commandline taaltje te definieren moet deze zowel aan de in de browser als op de server geparsed kunnen worden. In de meeste gevallen betekent dit dat er twee parsers onderhouden moeten worden, hoewel een enkele parser in JavaScript (gratis in Java6) natuurlijk ook een optie is. Zijn er eigenlijk parser generators voor JavaScript?

    Waarom zou je geen interne DSL maken met JavaScript? Dus een JavaScript API welke je met een commandline aanbiedt. Dit geeft je het voordeel dat geen parser hoeft te maken en JavaScript is flexibel genoeg om iets gebruikersvriendelijks te produceren. En laten we nou eerlijk zijn “User.add(’Rikkert’, ‘Koppes’)” is toch niet zoveel slechter dan “Add user Rikkert Koppes”? Daarbij kan je dan naast de powerusers ook de technische gebruikers faciliteren; “User.each(function(u){u.firstname==’Rikkert’&&u.delete()})”.

    Remco van 't Veer - november 21, 2007 11:43

  2. Je denkt wat te diep na ;)
    Het belangrijkste is dat de communicatie op verschillende vlakken gelijk getrokken wordt, dat het redelijk leesbaar is en een afspiegeling is van je use cases. De exacte syntax doet niet zo terzake. Wat ik als voorbeeld heb opgeschreven is een optie, maar als dat niet toereikend is voor de applicatie, gebruik dan wat anders. Wat je bijvoorbeeld zou kunnen doen is:

    Add user “Remco” “van ‘t” “Veer”
    Add user Remco van_’t Veer
    Add user {”firstname”:”Remco”,”infix”:”van ‘t”,”surname”:”Veer”}

    Het maakt allemaal niet zoveel uit, als het maar overal hetzelfde is. Als het allemaal te ingewikkeld wordt (uitgebreide datastructuren), moet je eerder gaan nadenken over je datamodel dan over je parser. Wat ik zelf heb is een interpreter die splitst op spaties, waarbij spaties in een parameter kunnen worden meegenomen door ‘m of te vervangen door een underscore, of te omvatten met dubbele quotes. De command handler kijkt vervolgens naar het aantal meegegeven parameters en trekt daar conclusies uit.

    Rikkert Koppes - november 23, 2007 11:40

  3. Rikkert, als ik je laatste voorbeeld zie:

    Add user {”firstname”:”Remco”,”infix”:”van ‘t”,”surname”:”Veer”}

    Vraag ik me af waarom je niet gewoon JSON gebruikt….

    DWR (http://getahead.org/dwr), een van de bekendere AJAX bibliotheken vertaald op die manier een set parameters naar een Pojo. Dan krijg je dus:

    userService.addUser({”firstname”:”Remco”,”infix”:”van ‘t”,”surname”:”Veer”});

    Net zo leesbaar toch, en je hoeft zelf geen parser te schrijven!

    Peter Maas - november 26, 2007 12:13

  4. Kan ook, laatste voorbeeld was ook inderdaad bedoeld richting JSON

    Punt is: maakt me niet uit, ik heb de inhoud van de doCommand method in m’n artikel niet voor niets weggelaten. Implementatie doet m.i. niet zo ter zake, het gaat me meer om de gedachte: doe de communicatie op alle niveau’s hetzelfde en zorg ervoor dat het leesbaar is en met de hand eenvoudig in te typen.

    Rikkert Koppes - november 26, 2007 13:45

Reageer

RSS feed for comments on this post · TrackBack URI