wk voetbalpool webapp: dag 16

 
23 april 2010

Omdat we aan de backend-kant zo’n beetje klaar zijn vroeg Jan-Willem me gisteren of er nog dingen gedaan moesten worden. Het was me opgevallen dat er in sommige klassen een aardige berg Ctrl-C Ctrl-V code voorkwam (aka ‘copy-paste code reuse’). Ik stelde voor dat hij dat zou refactoren tot een enkele geparametriseerde methode. Maar dat lijkt niet te gaan lukken. Een ‘nice to have’ natuurlijk, maar uit esthetisch en onderhoudsoogpunt (voor zover we onderhoud denken te willen gaan doen) toch wel lastig.

De situatie was als volgt: in de GroupFactory werd een Map[String, Map[GroupView, boolean]] bijgehouden waarin per groep (het eerste String-argument) een map werd bijgehouden die registreert welke gerelateerde GroupViews er wel en niet gestart zijn. (Een GroupView is een trait; in Java zou je het als een abstracte superklasse kunnen zien waarvan de specifieke groupviews, zoals bijv de GroupMembersView, GroupAvgScoreView enz., overerven.) Per view-type hadden we vervolgens een methode die voor een groep de gerelateerde view ophaalde:

def getGroupScoresView(groupname: String)
                  : Option[GroupScoresView] = {
    if (!groupViewsPerGroupname.keySet.contains(groupname)) {
      None
    }
    else
      groupViewsPerGroupname(groupname)
      .find(_ match { case (view, b) =>
           view.isInstanceOf[GroupScoresView] && b
      })
      match {
        case Some((view, b)) =>
                  Some(view.asInstanceOf[GroupScoresView])
        case None => None
      }
  }
 
  def getGroupAvgScoreView(groupname: String)
                    : Option[GroupAvgScoreViewActor] = {
    if (!groupViewsPerGroupname.keySet.contains(groupname)) {
      None
    }
    else
       groupViewsPerGroupname(groupname)
       .find(_ match { case (view, b) =>
            view.isInstanceOf[GroupAvgScoreViewActor] && b
       })
       match {
         case Some((view, b)) =>
                   Some(view.asInstanceOf[GroupAvgScoreViewActor])
         case None => None
       }
  }
 
  // ... en nog zo meer...

Zoals je ziet is het enige verschil de type-parameter die aangeeft welk type actor we willen hebben. En dat willen we dus generiek oplossen door een methode getGroupView[A <: GroupView] te bouwen.

Dus Jan-Willem ging enthousiast aan de slag, en na korte tijd was de codebase een stuk kleiner, de zaak compileert nog steeds (ook altijd fijn), dus we kunnen gaan testen... en dan krijgen we ineens een hele berg ClassCastExceptions. Welk type view we ook opvragen, we krijgen altijd de melding dat het object van type GroupScoreView niet van het gevraagde type GroupXYZView is - alsof we altijd de eerste uit de lijst krijgen. Hoe kan dat? We controleren eerst of de view van het gevraagde type GroupXYZView is, en zo te zien komt daar een true uit, waarna de aanroep view.asInstanceOf[GroupXYZView] toch goed zou moeten gaan. Maar kennelijk...?

Uiteindelijk na een ochtendje speurwerk zijn we tot de overtuiging gekomen dat het probleem op een zodanig diep nivo ligt dat we er niet erg veel aan kunnen doen: namelijk dat het eraan ligt dat de JVM zelf deze constructie niet aankan. Als je je iets herinnert over de introductie van generieke types in Java 5, weet je waarschijnlijk nog wel dat toen de beslissing is genomen om de JVM compatibel te houden met voorgaande versies, en dat daarom de JVM zelf geen generieke type-informatie bevat. Generieke types zijn puur compile-time dingen die door de compiler wel gecheckt worden, maar niet in de JVM terecht komen; dit noemen ze type erasure en in Java krijg je voor onveilige constructies een waarschuwing of (bijvoorbeeld ingeval van de instanceof-operator) een compile-error. Zo niet in Scala. De generieke types worden zo te merken vervangen door Any (oftewel Object voor Javanen en .Netters); de isInstanceOf slaagt vervolgens altijd, en de eerste de beste GroupView wordt teruggegeven als resultaat van de find(...). Ook de methode asInstanceOf[Any] lukt natuurlijk wel, maar als je dan vervolgens het object als specifieke GroupXYZView gaat behandelen... dan is de ClassCastException een feit. (Behalve natuurlijk als je toevallig naar het juiste type cast.)

Wat dat betreft zit de .Net CLR wel handiger in elkaar dan de JVM, omdat die zelf wel nog kennis heeft van de generieke type-informatie. Daar zou zo'n 'generieke' isInstanceOf dus wel goed kunnen gaan. Scala gaat ook geport worden naar het .Net-platform - goed nieuws zou je zeggen, maar ze hebben besloten om Scala/.Net hetzelfde moet gaan werken als Scala/JVM, en dus ...gaat die ook type erasure toepassen. Bummer.

Maar dit alles is maar cosmetica. Je kan je ook afvragen of selectie op basis van het type van je object zo'n mooie keuze is; maar zo leek het tot nog toe het beste te werken omdat als je een nieuwe view bouwt, je die alleen aan die lijst hoeft toe te voegen, en we niet verwachten dat er twee views van hetzelfde type bij een enkele groep hoeven te horen. En hoe weinig elegant het resultaat dan ook is: het werkt in elk geval wel.

Het backend is nu zo'n beetje klaar, gisteren zijn de laatste views gereedgekomen: de geaggregeerde status updates view en de afhandeling van gele kaarten. Dat laatste had uiteindelijk nog een aardig sneeuwbal-effect in de afhandeling van de JSON. Omdat nu niet alle data meer als string binnenkomt hebben we nu niet meer een Map[String, String] maar een Map[String, Any] waar de geparste JSON inzit, wat een aantal extra typecasts nodig maakt. Maar we kunnen nu in elk geval ook redelijk makkelijk vereisen en vervolgens afvangen dat de binnenkomende data van het juiste type is. Ook het front-end komt steeds verder af; Anatoly en Christa kunnen nu ook gele kaarten afhandelen, er zijn aangescherpte validaties, uitloggen lukt, datums worden nu netjes als datum gepresenteerd ipv. als '230420101730' voor bijv. vanavond half zes. (Jaja, 'datums' -> 'data', ik weet het, maar dat is dubbelzinnig in een ICT-context)

Rikkert heeft gisteren uiteindelijk de CouchDB inderdaad nog aan de praat gekregen op een VM onder Windows, dus die VM fungeert nu in feite als aparte database-server. Hij wil nog proberen of hij de logging van CouchDB kan omleiden, omdat het echt hele stapels en stapels aan debug-berichten in log4j plempt, waardoor wijzelf niet meer onze eigen logs kunnen volgen - er staat domweg teveel rommel tussen. Achteraf denkt hij dat het misschien toch verstandig was geweest om een andere database te pakken. CouchDB heeft relatief veel problemen veroorzaakt, terwijl de functionaliteit die we nu gebruiken ook simpeler te realiseren was geweest door een conventionelere database, zonder alle heisa eromheen. Dat krijg je als je experimenteel wil zijn. Helaas is het voor hem vandaag de laatste dag intern, dus hopelijk werkt alles nu naar behoren...?

Omdat de backend nu zo goed als klaar is, modulo binnenkomende bugmeldingen natuurlijk, begin ik met het opzetten van een Android-applicatie. Bij het downloaden is er de keuze welke versie van de Android SDK je wilt hebben. In mijn naïviteit download ik ze maar allemaal, waarna de computer toch even wat langer moet downloaden dan ik had verwacht... tijd voor een ommetje over ons landgoed. Als ik terugkom volg ik de installatie-instructies en het eerste voorbeeldje. En inderdaad, aan het eind van de dag ben ik even ver als op dag 5 kan ik op de meegeleverde emulator ...het programmaatje Hello World runnen. Een overwinning!

En daarmee wordt het half zes, en alles is wel.


Werken met ?
Kijk dan bij onze mogelijkheden voor zowel starters als ervaren engineers.


Categorieën: Development