Bob Swart (aka Dr.Bob)
StemBox - Kennistechnologisch stemadvies met Delphi

Tot eind juni 1999 was ik werkzaam als kennistechnoloog (en Delphi specialist) bij het bedrijf Bolesian in Helmond, gespecialiseerd in kennistechnologie. Eén van onze meest recente kennissystemen, beschikbaar gesteld op onze website betrof de StemBox: een hulpmiddel voor de (zwevende) kiezer bij het verkrijgen van een objectief persoonlijk stemadvies voor de tweede kamer verkiezingen (mei 1998) en de Europese verkiezingen (op 10 juni 1999).

De StemBox is in Borland Delphi ontwikkeld (anders zou ik er nu niet zoveel aandacht aan besteden), en in dit artikel wil ik een aantal van de technische achtergronden, ontwerpbeslissingen en consequenties van het gebruik van Delphi voor de implementatie van de StemBox bespreken. Maar allereerst een stukje achtergrond, zodat duidelijk wordt hoe StemBox achter de schermen werkt.

De Kennis
Het primaire doel van StemBox is te laten zien wat er met KennisTechnologie mogelijk is. Voor veel mensen is KT de "ver van mijn bed show", maar met StemBox willen we laten zien dat we er ook een heel praktische invulling aan kunnen geven. StemBox gratis beschikbaar stellen op onze website leek hierbij een geschikt middel. De StemBox is een programma waarbij een aantal (politieke) stellingen aan de gebruiker wordt getoond. Bij iedere stelling staat tevens een aantal (tussen de 2 en 5) standpunten. De gebruiker kan aangeven in welke van deze standpunten hij/zij zich het meest kan vinden. Soms kunnen meerdere antwoorden gekozen worden, maar meestal is slechts één antwoord toegestaan (het is echter niet verplicht om overal iets in te vullen). De stellingen zijn gegroepeerd aan de hand van een twaalftal politieke onderwerpen, zoals "criminaliteit", "vervoer en milieu", "onderwijs", "immigratie", "ouderenbeleid" en "belastingen". De standpunten zijn op hun beurt geformuleerd op basis van de inhoud van de verkiezingsprogramma's van de vijf grootste politieke partijen van Nederland, te weten de PvdA, CDA, VVD, D'66 en GroenLinks. Er is bewust gekozen voor slechts de vijf grootste partijen, omdat de analyse van méér partijprogramma's ons gewoon teveel tijd zou hebben gekost - StemBox is namelijk een marketingsproject van Bolesian, zonder daadwerkelijke operationele klant.

De Analyse
Maar alleen met een aantal stellingen en standpunten rondom een twaalftal onderwerpen komen we er natuurlijk niet. De crux van de kennisanalyse zit hem in het bepalen van de mate waarin het verkiezingsprogramma van ieder van de vijf partijen overeenkomt met ieder van de standpunten. En daarbij geldt de regel dat er per partij per stelling precies 100 punten verdeeld mogen worden over de verschillende standpunten. En daarbij kan het voorkomen dat volgens het verkiezingsprogramma een partij het slechts eens is met één standpunt, wat dan meteen de volledige 100 punten krijgt. Maar het kan ook zijn dat er twee of meer standpunten als alternatieven (al dan niet elkaar uitsluitend) worden genoemd in het verkiezingsprogramma, waarbij de 100 punten van de partij dus verdeeld moeten worden over deze standpunten. Het is met name hier dat de twee Bolesian kennisanalisten - Joyce de Gier en Suzanne Franken - het zware werk hebben moeten doen.

De KennisEditor
Terwijl de twee kennisanalisten bezig waren met het formuleren van de stellingen en standpunten, en het uitdelen van de punten over de standpunten, kon ik vast beginnen met het bouwen van een ondersteunende tool om de kennis op te slaan; de zogenaamde kenniseditor. Als eerste begon ik het kennismodel om te zetten in een (relationeel) datamodel, en kwam zo uit op drie tabellen: één voor de 12 onderwerpen (topics), één voor de subtopics en stellingen (per topic maximaal 8 en in totaal 44), en tenslotte één tabel voor alle standpunten (een paar honderd) en de punten van de vijf partijen verdeeld over deze standpunten. In het kort ziet de layout van de drie tabellen er als volgt uit:

TopicsSubtopicsStandpunten
Topic (sleutelveld) - Integer
Topic naam (string)
Subtopic (sleutelveld) - Integer
Topic (link naar Topic tabel)
SubTopic string
Stelling string
Meervoudige Antwoorden?
Standpunt (sleutelveld) - Integer
Subtopic (link naar Subtopics)
Standpunt tekst
Extra tekst (uitleg indien nodig)
PvdA punten
CDA punten
VVD punten
D66 punten
GroenLinks punten

Met behulp van de Borland Database Desktop zijn deze drie tabellen - in Paradox formaat - in luttele minuten gemaakt. Vervolgens heb ik in Delphi de kenniseditor voor het grootste deel laten bestaan uit een drietal database-grids met daarin de records uit de drie tabellen. De enige bijzonderheid was het definiëren van de SUBTOPIC tabel als "MasterSource" van de STANDPUNT tabel (waarbij de Subtopic velden verbonden zijn), en het definiëren van de TOPIC tabel op zijn beurt als de "MasterSource" van de SUBTOPIC tabel (met de Topic velden verbonden).

Merk op dat we niet eens een DBNavigator component nodig hebben, aangezien een DBGrid automatisch een nieuw record aanmaakt (insert) op het moment dat we naar een nieuwe, lege, regel in het grid gaan, en een Post doet op het moment dat we weer naar een ander record gaan. Met Control-Delete kunnen we een record verwijderen, en meer functionaliteit was eigenlijk niet nodig voor het eenvoudig kunnen invoeren van de analyse resultaten. Overigens laat het plaatje meteen een situatie zien waarbij de standpunten uniek te verdelen zijn over de partijen; iedere partij is het met precies één standpunt eens. Daarnaast kwamen onze analisten uiteraard ook meer complexe standpunten tegen, zoals bijvoorbeeld het subtopic "Preventie" bij het onderwerp "Criminaliteit", waarbij de punten van de verschillende partijen steeds over meer dan één standpunt verdeeld moesten worden, op basis van de inhoud van de vijf verschillende verkiezingsprogramma's.

De Code Generator
Toen de drie tabellen gevuld waren met de resultaten van de analyse, was dat in principe voldoende fundament om de StemBox "bovenop" te bouwen. Er is echter gekozen voor een iets andere benadering. De StemBox zou een internet toepassing moeten worden, draaiend op onze website (op de Windows NT Web Server van onze internet service provider). Om de drie Paradox tabellen te kunnen gebruiken is het hierbij noodzakelijk om de Borland Database Engine (BDE) op deze Web Server te installeren. Op zich niet zo'n probleem, maar het was tevens de bedoeling om de StemBox als zgn. CGI (Common Gateway Interface) Console toepassing te ontwikkelen. Dat wil zeggen dat voor iedere nieuwe gebruiker of pagina de toepassing opnieuw wordt opgestart, zijn werk doet, en weer afgesloten wordt. Normaal gesproken gebeurt dat binnen één seconde, maar als de BDE ook elke keer in het geheugen geladen moet worden en weer moet worden afgesloten betekent dit een extra wachttijd die kan oplopen van één tot enkele seconden per keer, en dat is natuurlijk niet acceptabel. Als alternatief hadden we de CGI toepassing als ISAPI DLL kunnen ontwikkelen, waarbij de toepassing - inclusief de BDE - slechts éénmaal wordt geladen, en vanaf dat moment in het geheugen van de Web Server geladen blijft - in principe tot de Web Server wordt uitgezet, of crasht. En ook al levert dat snelheidswinst op, het nadeel van deze benadering is dat het moeilijk is om wijzigingen in de toepassing aan te brengen (bijvoorbeeld als een verkiezingsprogramma wijzigt - wat daadwerkelijk gebeurt, aangezien de eerste versie van StemBox nog maar op de voorlopige verkiezingsprogramma's gebaseerd was). Daarnaast brengt het ook stabiliteitsproblemen met zich mee, omdat een ISAPI DLL de minder fijne eigenschap heeft dat het de gehele Web Server "down" brengt op het moment dat er zich een run-time error voordoet (of een exception ontsnapt) binnen de DLL. En ook al maken programmeurs natuurlijk nooit echte fouten, het is vervelend als zelfs een klein foutje toch optreedt en als gevolg daarvan de gehele website uit de lucht is. Nee, om deze problemen te voorkomen hebben we gekozen om de database problematiek geheel te omzeilen door de data uit de tabellen tot Pascal source code te transformeren. Geïnitialiseerde Pascal records, om precies te zijn. Volgens vrijwel dezelfde datastructuur als de layout van de drie tabellen zoals we die eerder zagen. Een bijkomend voordeel is het feit dat de kennis nu in de toepassing "gebakken" zit, en niet meer in een externe database, waardoor "reverse engineering" door anderen bemoeilijkt wordt, en we toch elke keer weer snel een nieuwe versie uit de database kan genereren (bijvoorbeeld als de verkiezingsprogramma's tussentijds mochten wijzigen).

  unit StemBoxKennis;
  interface
  const
    MaxTopic = 12;

  type
    TTopic = record
      Naam: PChar;
      SubTopics: Integer;
    end {TTopic};

  var
    Topics: Array[1..MaxTopic] of TTopic = (
      (Naam: 'Criminaliteit'; SubTopics: 5),
      (Naam: 'Gezondheidszorg'; SubTopics: 3),
      (Naam: 'Economie en werkgelegenheid'; SubTopics: 5),
      (Naam: 'Milieu en vervoer'; SubTopics: 8),
      (Naam: 'Democratie/Bestuurlijk'; SubTopics: 2),
      (Naam: 'Belasting'; SubTopics: 2),
      (Naam: 'Ouderenbeleid'; SubTopics: 4),
      (Naam: 'Onderwijs'; SubTopics: 3),
      (Naam: 'Immigratie'; SubTopics: 4),
      (Naam: 'Sociale Zekerheid'; SubTopics: 4),
      (Naam: 'Landbouw'; SubTopics: 2),
      (Naam: 'Huisvesting'; SubTopics: 2));

  const
    MaxStellingPerTopic = 8;

  type
    TStelling = record
      Naam: PChar;
      Stelling: PChar;
      Multi: Boolean; { kunnen er meerdere standpunten geselecteerd worden? }
      Standpunten: Integer;
    end {TStelling};

  type
    TStellingen = Array[1..MaxStellingPerTopic] of TStelling;

  var
    Stelling: Array[1..MaxTopic] of TStellingen = (....);

  const
    MaxStandpuntPerStelling = 5;

  type
    TStandpunt = record
      Standpunt,Extra: PChar;
      PVDA, CDA, VVD, D66, GL: Integer;
    end {TStandpunt};

  type
    TStandpunten = Array[1..MaxStandpuntPerStelling] of TStandpunt;

  var
    Standpunten: Array[1..MaxTopic,1..MaxStellingPerTopic] of TStandpunten = (....);
Bovenstaande source code is natuurlijk niet met de hand ingetikt, maar gegenereerd door het navigeren door de inhoud van de drie Paradox tabellen en de inhoud van de records naar een bestand weg te schrijven. De twee stukken code die met "(....)" zijn aangegeven bevatten in het echt de vele geïnitialiseerde "kennis"-records die op deze manier zijn gegeneerd uit de drie tabellen. In pseudo-code ging het navigeren overigens als volgt:
  with Table do
  begin
    First;
    while not eof do
    begin
      { schrijf de record-data weg naar de unit StemBoxKennis }
      Next
    end
  end;
Door gebruik te maken van deze techniek - waarbij we dus de gegevens uit de database in feite inbedden in het programma zelf - is de StemBox een stand-alone programma geworden. Niet langer afhankelijk van de BDE, een database of welk ander extern bestand of library dan ook. En aangezien er ook maar weinig extra "units" nodig zijn, en al helemaal geen "VCL" (Visual Class Library) componenten gebruikt zijn, is de StemBox toepassing ook relatief klein in omvang (en dus snel opgestart): slechts iets meer dan 100 Kbytes voor de uiteindelijke executable!

Kennisvalidatie
Het kunnen genereren van Pascal records is wel fijn, maar we zijn eigenlijk nog een belangrijk stap vergeten: de kennisvalidatie. En dan bedoel ik niet de twee analisten die elkaars werk controleren en discusseren over de verdeling van de punten over de standpunten, maar met name de controle van de invoer van deze punten in de kenniseditor. Er moeten per partij per stelling namelijk precies 100 punten verdeeld zijn. Niet meer, en ook niet minder. Deze controle zou trouwens ook in de kenniseditor zelf kunnen gebeuren, alhoewel het in praktijk lastig blijkt om elke keer een foutmelding te krijgen als je nog bezig bent met het invoeren van de punten van de verschillende partijen en de - onvolledige - som dus (nog) niet op 100 uitkomt. Een betere gelegenheid om dit te doen is op het moment dat de Pascal code gegenereerd wordt, of als extra source code in de gegenereerde unit zelf. Voor de laatste oplossing heb ik gekozen, en het testen of ik voor iedere partij wel precies 100 punten uitdeel wordt dan ook gedaan in het initialization deel van de unit StemBoxKennis. De compiler directive "VALIDATE" zorgt ervoor dat ik deze controle kan aanzetten (vlak na het genereren van de source code) en weer kan uitzetten als deze correct blijkt te zijn, zodat deze code niet in de operationele versie van StemBox wordt uitgevoerd:

  implementation
  {$IFDEF VALIDATE}
  {$C+}
  uses
    SysUtils;

  var
    PvdA,CDA,VVD,D66,GL,i,j,k: Integer;
  initialization
    for i:=1 to MaxTopic do
    begin
      for j:=1 to MaxStellingPerTopic do
      begin
        PvdA := 0;
        CDA := 0;
        VVD := 0;
        D66 := 0;
        GL := 0;
        for k:=1 to MaxStandpuntPerStelling do
        begin
          Inc(PvdA,Standpunten[i,j,k].PvdA);
          Inc(CDA,Standpunten[i,j,k].CDA);
          Inc(VVD,Standpunten[i,j,k].VVD);
          Inc(D66,Standpunten[i,j,k].D66);
          Inc(GL,Standpunten[i,j,k].GL)
        end;
        Assert(PvdA in [0,100],Format('PvdA (%d - %d) = %d',[i,j,PvdA]));
        Assert(CDA in [0,100],Format('CDA (%d - %d) = %d',[i,j,CDA]));
        Assert(VVD in [0,100],Format('VVD (%d - %d) = %d',[i,j,VVD]));
        Assert(D66 in [0,100],Format('D66 (%d - %d) = %d',[i,j,D66]));
        Assert(GL in [0,100],Format('GL (%d - %d) = %d',[i,j,GL]));
      end
    end
  {$ENDIF}
  end.
Merk op dat ik de test of het aantal uitgedeelde punten correct is doe met behulp van Assert. Wie meer wil weten over het gebruik van Assertions verwijs ik graag naar nummer 46 van SDGN Magazine, waarin ik vanaf pagina 38 dieper op dit onderwerp in ga.

De Internet Toepassing
Goed, we hebben dus inmiddels de - gevalideerde - kennis beschikbaar in de vorm van ingevulde Pascal records. Nu nog de zgn. inference engine" om hiermee een persoonlijk stemadvies te kunnen samenstellen, en de internet raamwerk toepassing er omheen. Om te beginnen met het laatste; we hebben bewust nagedacht over de juiste implementatie methode voor de StemBox toepassing. We hebben zowel Java applets, ActiveX controls en ActiveForms (zie SDGN Magazine nummer 44, pagina 17 t/m 21 voor een uitgebreid artikel van mijn hand over Delphi 3 en ActiveForms), als server-side toepassingen zoals ISAPI en CGI toepassingen overwogen. Een van de belangrijkste uitgangspunten van StemBox is echter dat iedere internet gebruiker in staat moet worden gesteld om een persoonlijk stemadvies te ontvangen. Dit betekent in praktijk dat we alle mogelijke browsers moeten ondersteunen, dus ook bijvoorbeeld de 16-bits Netscape Navigator 2.01 die geen Java ondersteunt, of Internet Explorer 3.0 die net als Netscape Navigator 3.0 een inmiddels wat verouderde versie van Java ondersteunt. Java viel mede hierdoor af als bruikbaar alternatief. Hetzelfde gold voor ActiveX en ActiveForms oplossingen, aangezien die alleen direkt door de 32-bits Microsoft Internet Explorer wordt ondersteund (voor Netscape Navigator is een plug-in nodig). Bovendien draaien de ActiveX componenten zèlf alleen op een 32-bits Windows platform, wat betekent dat we ook hiermee dus niet iedere internet gebruiker zullen kunnen bedienen. En dan bleven de server-side toepassingen over, zoals ISAPI en CGI. Omdat we alleen via FTP toegang hadden tot onze web server (machine), en deze niet van afstand konden rebooten of herstarten, zou een ISAPI oplossing mogelijk tot onderhoudsproblemen hebben geleid (bij het neerzetten van een nieuwe versie bijvoorbeeld). Mede hierdoor was de keuze voor mij duidelijk: met behulp van Borland Delphi hebben we de StemBox als een CGI "console" toepassing ontwikkeld. Een niet-visuele toepassing die gegevens (gegeven antwoorden) via standaard input binnenkrijgt, en een dynamische HTML webpagina teruggeeft door deze op de standaard output weg te schrijven. Speciaal voor het verwerken van de binnenkomende data heb ik de unit DrBobCGI ontwikkeld die de verzonden data opvangt en door middel van de funktie ValueAsInteger beschikbaar stelt aan de rest van de CGI toepassing. Als volgend voorbeeld volgt hier de source code om het startscherm van de StemBox te genereren, inclusief het overzicht van het twaalftal beschikbare onderwerpen (hierbij komt nog geen data binnen, want er hoeft slechts een eerste HTML webpage gegenereerd te worden):

  {$APPTYPE CONSOLE}
  program StemBox;
  uses
    DrBobCGI, { ValueAsInteger }
    StemBoxKennis; { Topics, SubTopics, Standpunten }

  const
    Images = 'http://www.eBob42.com/StemBox/';
    EXE = 'http://www.eBob42.com/cgi-bin/StemBox.exe';
    URL = 'http://www.eBob42.com';

  var
    User,Topic,i: Integer;
  begin
    writeln('content-type: text/html');
    writeln;
    writeln('<HTML>');
    writeln('<HEAD>');
    writeln('<TITLE>Bolesian BV - StemBox</TITLE>');
    writeln('</HEAD>');
    writeln('<BODY BACKGROUND="',Images,'yellow.gif">')
    writeln('<CENTER>');
    writeln('<TABLE><TR><td>');
    writeln('<IMG SRC="',Images,'bologo.gif"></td><td><CENTER>');
    writeln('Dit Programma werd U aangeboden door <b>Bolesian BV</b>,');
    writeln('specialisten in kennistechnologie!')
    writeln('</CENTER></td><td><IMG SRC="',Images,'bologo.gif"></td></TR></TABLE>');
    writeln('<FORM ACTION="',EXE,'"METHOD=POST>');
    User := ValueAsInteger('User');
    if User = 0 then User := NewUser; { nog geen "user" ID - dus nieuwe start }
    writeln('<INPUT TYPE=HIDDEN NAME=User VALUE=',User,'>');
    Topic := ValueAsInteger('Topic');
    writeln('<TABLE BORDER><TR><td>',User,'</td></TR></TABLE><BR>');
    if Topic = 0 then
    begin
      writeln('<IMG SRC="',Images,'stembox.gif"WIDTH="448"HEIGHT=152><BR>');
      writeln('<FONT SIZE=5>Laat Uw stem spreken en bepaal Uw partij !</FONT>');
      writeln('<P>');
      writeln('<IMG SRC="',Images,'lijn.gif">');
      writeln('<P>');
      writeln('<TABLE>');
      writeln('<TR><TD WIDTH=480>');
      writeln('<b>Belangrijk:</b> U hoeft niet alle vragen in te vullen');
      writeln('voor het krijgen van een persoonlijk stemadvies.');
      writeln('<P>');
      writeln('<I>Klik in de onderstaande lijst de voor U meest relevante');
      writeln('onderwerpen aan:</I>');
      writeln('<BR>');
      for i:=1 to MaxTopic do
      begin
        writeln('<INPUT TYPE=CHECKBOX NAME=Topic',i,' VALUE=',i,'> ',Topics[i].Naam);
        writeln(' (',Topics[i].SubTopics,' vragen) <BR>')
      end;
      writeln('<P>');
      writeln('<CENTER>');
      writeln('<INPUT TYPE=SUBMIT VALUE="Ga naar het eerste onderwerpscherm">');
      writeln('</CENTER>');
      writeln('</td></TR></TABLE>')
    end
    else
      ....
  { vertoond de stellingen/standpunten, of geef advies en/of uitleg }
    writeln('</FORM>');
    writeln('<IMG SRC="',Images,'lijn.gif">');
    writeln('<FONT SIZE=1>');
    writeln('<CENTER>');
    writeln('StemBox created by <a href="',URL,'">Bob Swart</a> for Bolesian BV, ');
    writeln('Helmond, The Netherlands - last updated: 1998/02/14');
    writeln('</BODY>');
    writeln('</HTML>')
  end.
Als bovenstaande code voor het eerst wordt uitgevoerd zal zowel de waarde van "USER" als "TOPIC" nog 0 zijn, zodat het startscherm wordt vertoond. Dit ziet er in Netscape Navigator overigens als volgt uit:

Zodra de gebruiker één of meer onderwerpen heeft aangeklikt en op de knop "Ga naar het eerste onderwerpscherm" heeft gedrukt, komt de StemBox pas echt in aktie. Vanaf nu wordt voor elk onderwerp een lijst met de beschikbare stellingen en standpunten voor dat onderwerp vertoond, waarbij de gebruiker aan kan geven in welke standpunten hij/zij zich het meest kan vinden. De source code voor het presenteren van de stellingen en standpunten voor een bepaald onderwerp is redelijk eenvoudig, en bestaat in feite uit niets meer dan het uitschrijven van de bij het onderwerp (topic) behorende subtopics, stellingen en standpunten zoals die in de datastructuur terug te vinden zijn. Voor de stellingen waarbij meerdere standpunten geselecteerd mogen worden genereren we HTML code met checkboxen, terwijl de stellingen waarbij we maar één standpunt mogen selecteren resulteren in HTML code met radiobuttons.
  // Topic is het onderwerp waarvan we de Stellingen/Standpunten laten zien
  for i:=1 to Topics[Topic].SubTopics do
  begin
    writeln('<b>',Stelling[Topic,i].Naam,'</b><BR>');
    if Stelling[Topic,i].Multi then
      writeln('<I>(op deze vraag zijn meerdere antwoorden mogelijk)</I><BR>');
    writeln(Stelling[Topic,i].Stelling);
    writeln('<BR>');
    writeln('<TABLE WIDTH=400>');
    for j:=1 to Stelling[Topic,i].Standpunten do
    begin
      write('<TR><td>');
      if Stelling[Topic,i].Multi then
        writeln('<INPUT TYPE=CHECKBOX NAME=',Topic,'-',i,'-',j,' VALUE=',j,'> ')
      else writeln('<INPUT TYPE=RADIO NAME=',Topic,'-',i,' VALUE=',j,'> ');
      with Standpunten[Topic,i,j] do
        writeln('</td><td>',Standpunt,Extra,'</td></TR>')
    end {Standpunten};
    if not Stelling[Topic,i].Multi then { extra antwoord "geen keuze" }
      writeln('<TR><td><INPUT TYPE=RADIO NAME=',Topic,'-',i,' VALUE=',0,'></td>',
               <td><I>geen keuze</I></td></TR>');
    writeln('</TABLE>');
    writeln('<BR>')
  end;
Het resultaat van deze code is - voor het onderwerp Milieu en Vervoer bijvoorbeeld - te zien in de onderstaande screenshot. Merk op dat er "in het echt" ook plaatjes naast de tekst staan, ontworpen door onze grafisch ontwerpster Astrid van Wijngaarden, om de onderwerpen en stellingen te illustreren en het geheel een wat fleuriger aangezicht te geven:

Tevens wijs ik erop dat op bovenstaande screenshot ook te zien is dat voor elk van de twaalf onderwerpen kan worden aangegeven hoe belangrijk de invuller dit specifieke onderwerp vindt (zie de vier radiobuttons bovenaan de pagina). Default staat dit op gemiddeld, wat aan elke geselecteerd standpunt binnen dit onderwerp een "gewicht" van 100 toekent. Een keuze voor minder belangrijk betekent een gewicht van 50, terwijl zwaarwegend en zeer zwaarwegend tot een gewicht van resp. 150 en 200 leiden. Dit gewicht - bepaald per onderwerp - wordt uiteindelijk vermenigvuldigd met de partij-score voor ieder geselecteerd standpunt, en dat produkt telt mee bij het bepalen van de eindsom per partij en het uiteindelijke advies (en stelt ons tevens in staat een bijbehorende uitleg te geven).

Gebruikers en Sessies
De praktijk wijst uit dat de StemBox dagelijks door meerdere mensen tegelijk wordt gebruikt. En dat levert potentieel problemen op, aangezien de StemBox toepassing voor iedere nieuwe pagina wordt opgestart, de pagina genereert en verzendt, en weer wordt afgesloten. Er wordt op geen enkele manier door StemBox zelf bijgehouden welke gebruiker op dat moment zijn antwoorden opstuurt of de volgende pagina moet krijgen. En aangezien twee gebrukers dus zelfs min of meer tegelijkertijd hun antwoorden op dezelfde vragen kunnen insturen, zullen we dus iets slims moeten verzinnen om bij te houden wie wie is. Dat hebben we gedaan aan de hand van het volgnummer van de gebruiker. Zoals in bovenstaand plaatjes al te zien was, krijgt iedere gebruiker van StemBox bij het verkrijgen van de startpagina meteen een uniek volgnummer. In dit geval nummer 6.034 (te zien in het startscherm). Dit nummer begon bij 1, en zal rond de verkiezingen wellicht rond de 100.000 staan (er zijn in totaal meer dan 100.000 bezoekers langsgekomen in 1998). Vanaf dat moment "is" de gebruiker dit volgnummer, en worden alle antwoorden ook alleen maar gekoppeld aan dit volgnummer. Het systeem weet dan ook absoluut niet wie de gebruiker is, waar deze vandaan belt, etc. maar is slechts geïnteresseerd in het volgnummer, het nummer 6.034 in dit geval. Een fragment uit de eerder vertoonde source code laat dit nog eens duidelijk zien:

  User := ValueAsInteger('User');
  if User = 0 then User := NewUser; { nog geen "user" ID - dus nieuwe start }
  writeln('<INPUT TYPE=HIDDEN NAME=User VALUE=',User,'>');
Als de met de funktie ValueAsInteger opgehaalde waarde van "User" nog steeds nul is, dan wordt er een nieuwe waarde gegenereerd (het volgnummer, dat bij elke gebruiker eentje hoger wordt). En of er nu een nieuwe waarde aan "User" is toegekend of niet, de uiteindelijke waarde wordt weggeschreven als "hidden field" (dus onzichtbaar veld) in de gegenereerde HTML webpagina. En op deze manier zal de volgende keer bij het aanroepen van ValueAsInteger (dus als het tijd wordt om een eerste onderwerp op het scherm te zetten) de waarde van User niet langer gelijk zijn aan nul. Op deze manier wordt de waarde van "User" dus vanaf het allereerste scherm (waarbij de waarde voor het eerst wordt bepaald) tot en met het allerlaatste scherm een vast - onzichtbaar - onderdeel van de gegenereerde HTML pagina. Dat we vervolgens dezelfde waarde van "User" toch ook binnen de pagina zichtbaar maken als soort van "teller" heeft meer een cosmetische dan een functionele achtergrond, en gebeurt als volgt:
  writeln('<TABLE BORDER><TR><td>',User,'</td></TR></TABLE><BR>');

Gegevensopslag
Het bijhouden van het huidige onderwerp gaat in feite hetzelfde, met een "hidden field" genaamd Topic. Er is echter één groot verschil, namelijk dat het lijstje met de nog-te-vertonen onderwerpen net als de gewichten en geselecteerde standpunten natuurlijk beter niet van scherm naar scherm kunnen worden doorgegeven (dan zouden we uiteindelijk met een hele grote HTML webpagina eindigen, met mogelijkerwijs wel een honderdtal "hidden fields"). Beter lijkt het om deze informatie ergens anders op te slaan, gekoppeld aan de per gebruiker unieke waarde van "User" (het volgnummer van de gebruiker van StemBox). We hadden deze gegevens natuurlijk in een database kunnen opslaan, maar we hebben gekozen voor een oplossing die gebruik maakt van .ini bestanden, één per gebruiker, met de naam "sb999999.ini" (waarbij de 999999 de huidige waarde is van "User", en we dus in theorie niet meer dan 999.999 bezoekers aankunnen). Het opslaan van de antwoorden van een Topic gebeurt bijvoorbeeld als volgt:

  if (Topic > 0) then { sla de antwoorden op }
  begin
    with TIniFile.Create(Format('sb%.6d.ini',[User])) do
    try
      for i:=1 to Topics[Topic].SubTopics do
      begin
        if Stelling[Topic,i].Multi then { meerdere standpunten mogelijk }
        begin
          for j:=1 to Stelling[Topic,i].Standpunten do
            if ValueAsInteger(Format('%d-%d-%d',[Topic,i,j])) > 0 then
              WriteInteger(IntToStr(User),Format('%d-%d-%d',[Topic,i,j]),
                           ValueAsInteger(Format('%d-%d-%d',[Topic,i,j])))
            else { "clear" standpunt score }
              WriteInteger(IntToStr(User),Format('%d-%d-%d',[Topic,i,j]),0)
        end
        else { slechts één standpunt mogelijk }
          WriteInteger(IntToStr(User),Format('%d-%d',[Topic,i]),
                       ValueAsInteger(Format('%d-%d',[Topic,i])))
      end;
      WriteInteger(IntToStr(User),'Gewicht'+IntToStr(Topic),ValueAsInteger('Gewicht'))
    finally
      Free // IniFile
    end
  end;
Merk op dat deze code ook eerder gegeven antwoorden weer leeg maakt, zodat het mogelijk is om een pagina voor de tweede keer in te vullen (om bijvoorbeeld te zien of het advies dan anders uitvalt).

Advies en Uitleg
Het geven van Advies bestaat uit het per partij optellen van het produkt van de score van ieder geselecteerd standpunt met het gewicht van het bijbehorend onderwerp. Hier komt per partij een getal uit, en de hoogste heeft gewonnen. In theorie kan het voorkomen dat twee partijen uiteindelijk evenveel punten hebben gekregen, alhoewel de kans daarop erg klein is (de kans neemt af met het aantal geselecteerde standpunten). Overigens is voor het geven van een advies het selecteren van slechts één standpunt al voldoende, alhoewel het duidelijk zal zijn dat de waarde die aan een advies toegekend kan worden, toeneemt met het aantal geselecteerde standpunten. Mits deze doordacht zijn gekozen, want bij twijfel geldt dat niets selecteren beter is dan zomaar een standpunt selecteren (waarbij in het laatste geval de balans zelfs mogelijkerwijs in een ongewenste richting door kan slaan). Het kan natuurlijk voorkomen dat zelfs na het maken van doordachte keuzen in de standpunten, StemBox met een wat onverwacht advies komt. Onverwacht dan in die zin dat de gebruiker bij zichtzelf een bepaalde politieke richting had verwacht, en wellicht geeft StemBox dan dus een ander advies. In zulke gevallen is het belangrijk om een duidelijk en gedetailleerde uitleg bij het advies te kunnen krijgen, en ook daarin kan StemBox voorzien. Het uitlegscherm van StemBox bestaat uit een overzicht van de ingevulde onderwerpen, met daarbij het gewicht van het onderwerp, en per stelling het standpunt of de standpunten die door de gebruiker zijn geselecteerd, met daarbij de punten van de partijen, zodat meteen te zien is welke partij het meest overeenkomt met de geselecteerde standpunten.

In bovenstaand voorbeeld heb ik bijvoorbeeld gekozen voor het standpunt dat inhoudt dat het gedoogbeleid van softdrugs moet worden gestopt op termijn. Dit standpunt blijkt voor 70% overeen te komen met het standpunt van de CDA, en voor 25% met het standpunt van de VVD. De overige partijen blijken het niet eens te zijn met dit standpunt, en hebben dan ook geen punt voor dit standpunt. Zo wordt dit overzicht voor elk ingevuld standpunt gegenereerd, zodat ik - zelfs als het advies mij verrast doordat er een andere partij uitkomt dan ik zou hebben verwacht - zelf kan controleren hoe mijn gekozen standpunten zich verhouden ten opzichte van de verkiezingsprogramma's van de partijen. Uit persoonlijke ervaring kan ik melden dat dit erg leerzaam is en uitermate inzichtelijk voor je eigen politieke gevoel - met name voor de zogenaamde "zwevende kiezer" kan dit een ware eye-opener zijn. Merk op dat we hierbij niet zover zijn gegaan om ook de punten van de partijen over de niet geselecteerde standpunten te geven. Dat zou wel erg makkelijk kunnen uitnodigen tot het teruggaan en opnieuw selecteren van standpunten teneinde het uiteindelijk stemadvies - al dan niet bewust - te kunnen manipuleren. De source code voor het geven van uitleg ziet er in detail als volgt uit:
  with TIniFile.Create(Format('sb%.6d.ini',[User])) do
  try
    for Topic:=1 to MaxTopic do
    if ReadInteger(IntToStr(User),'Gewicht'+IntToStr(Topic),0) > 0 then
    begin
      writeln('<hr>');
      writeln('Het onderwerp <b>',Topics[Topic].Naam,'</b> gaf U een gewicht van ',
               ReadInteger(IntToStr(User),'Gewicht'+IntToStr(Topic),0),'<P>');
      for i:=1 to Topics[Topic].SubTopics do
      begin
        Antwoorden := [];
        if Stelling[Topic,i].Multi then
        begin
          for j:=1 to Stelling[Topic,i].Standpunten do
            if ReadInteger(IntToStr(User),Format('%d-%d-%d',[Topic,i,j]),0) = j then
              Antwoorden := Antwoorden + [j]
        end
        else
        begin
          j := ReadInteger(IntToStr(User),Format('%d-%d',[Topic,i]),0);
          if j > 0 then Antwoorden := [j]
        end;
        if Antwoorden <> [] then
        begin
           writeln('<b>',Stelling[Topic,i].Naam,'</b><BR>');
           if Stelling[Topic,i].Multi then
           begin
             writeln('Bij de stelling "<I>',Stelling[Topic,i].Stelling,'</I>"',
                     ' koos u voor antwoord(en):<BR>');
             for j:=1 to Stelling[Topic,i].Standpunten do
               if ReadInteger(IntToStr(User),Format('%d-%d-%d',[Topic,i,j]),0) = j then
                 writeln(Chr(Ord('A')+j-1),'.',
                         Standpunten[Topic,i,j].Standpunt,' ',
                         Standpunten[Topic,i,j].Extra,'<BR>')
          end
          else // not Multi
          begin
            writeln('Bij de vraag/stelling "<I>',Stelling[Topic,i].Stelling,'</I>"',
                    ' koos u voor antwoord:<BR>');
            j := ReadInteger(IntToStr(User),Format('%d-%d',[Topic,i]),0);
            Antwoorden := Antwoorden + [j];
            writeln(Chr(Ord('A')+j-1),'.',
                    Standpunten[Topic,i,j].Standpunt,' ',
                    Standpunten[Topic,i,j].Extra,'<BR>')
          end;
          writeln('<P>');
          writeln('De standpunten van de partijen komen hiermee als volgt overeen:');
          writeln('<MENU>');
          writeln('<TABLE BORDER>');
          write('<TR><td> Partij </td>');
          for j:=1 to Stelling[Topic,i].Standpunten do if j in Antwoorden then
            write('<td> Score ',Chr(Ord('A')+j-1),'</td>');
          writeln('</TR>');
          write('<TR><td><b>PvdA</b></td>');
          for j:=1 to Stelling[Topic,i].Standpunten do if j in Antwoorden then
            write('<td>',Standpunten[Topic,i,j].PvdA,'</td>');
          writeln('</TR>');
          write('<td><b>CDA</b></td>');
          for j:=1 to Stelling[Topic,i].Standpunten do if j in Antwoorden then
            write('<td>',Standpunten[Topic,i,j].CDA,'</td>');
          writeln('</TR>');
          write('<td><b>VVD</b></td>');
          for j:=1 to Stelling[Topic,i].Standpunten do if j in Antwoorden then
            write('<td>',Standpunten[Topic,i,j].VVD,'</td>');
          writeln('</TR>');
          write('<td><b>D66</b></td>');
          for j:=1 to Stelling[Topic,i].Standpunten do if j in Antwoorden then
            write('<td>',Standpunten[Topic,i,j].D66,'</td>');
          writeln('</TR>');
          write('<td><b>GL</b></td>');
          for j:=1 to Stelling[Topic,i].Standpunten do if j in Antwoorden then
            write('<td>',Standpunten[Topic,i,j].GL,'</td>');
          writeln('</TR>');
          writeln('</TABLE>');
          writeln('</MENU>');
          writeln('<P>')
        end
      end
    end
  finally
    Free // IniFile
  end;

StemBox
Ik hoop met dit artikel enig inzicht te hebben gegeven in de architectuur van de StemBox. Delphi blijkt een krachtig hulpmiddel te zijn voor het ontwikkelen van internet toepassingen die uit meerdere stappen (pagina's) en gebruikers (sessies) bestaan. Het enige operationele probleem dat dan ook naar voren kwam bij het in gebruik nemen van StemBox was de snelheid van onze website. Door de enorme stortvloed van honderden mensen die dagelijks op zoek waren naar een persoonlijk stemadvies, bleek de snelheid - met name op de internet piekuren (vanaf 5 uur 's middags tot 's avonds laat) - behoorlijk te wensen over te laten. Het probleem zat hem hierbij niet in de StemBox toepassing zelf (die gemiddeld elke 10 seconden een keer werd uitgevoerd), maar in de verbinding van onze website met de internet backbone, aangezien er bij ieder onderwerp ook de nodige plaatjes (animated GIF en JPG) meegestuurd worden. De oplossing ligt natuurlijk voor de hand: een snellere verbinding met de internet backbone, en op het moment dat u dit artikel leest zal dat laatste reeds gerealiseerd zijn, dus u kunt gerust (weer) eens een kijkje komen nemen voor een persoonlijk stemadvies.

Bob Swart was een kennistechnoloog en de Delphi specialist voor Bolesian in Helmond, en werkt nu voor Bob Swart Training & Consultancy (eBob42)
Mocht iemand nog vragen, opmerkingen of suggesties hebben, dan hoor ik die het liefst via .


This webpage © 1999-2006 by webmaster drs. Robert E. Swart (aka - www.drbob42.com). All Rights Reserved.