En god utvikler er opptatt av objektorientert design og ren kode. I denne kategorien skiver jeg om design patterns og relaterte ting. Forvent endel kode-eksempler..

FOP – Funksjonell objektprogrammering

Sunday, April 14th, 2013
Ingen kommentarer

For 17 år siden startet jeg med objektorientert programmering – jeg har det i blodet, og koder på den måten hver arbeidsdag (i alle fall ikke langt unna). Men for noen år siden begynte jeg også å se på funksjonell programmering, og denne bloggen har etterhvert fått mer og mer innhold om funksjonelle språk og teknikker.

Disse to paradigmene kan til en viss grad defineres ut fra forskjellen mellom dem; dvs. at de er i konflikt med hverandre, de har avvikende meninger om hvordan man skal designe løsninger. Etterhvert har jeg fått en viss forståelse for denne konflikten, eller mer presist jeg forstår mer og mer om hvilke tradeoffs de to paradigmene har i forhold til hvordan de vil jeg skal utføre jobben min.

Det er dette som får meg til å skrive denne teksten, hvor jeg vil kaste ut noen tanker om forholdet mellom paradigmene, og om hvordan de kan og bør sameksistere.

Kompleksitet

“Anyone can make the simple complicated. Creativity is making the complicated simple.” – Charles Mingus (innflytelsesrik jazzmusiker)

Programmering er å administrere kompleksitet. Vi skal løse et problem. Vi kan si at problemet uansett har en viss mengde kompleksitet, og beskrivelsen av løsningen tilfører ytterligere kompleksitet. Vår jobb er å strukturere denne kompleksiteten på en slik måte at den er enklest mulig å forstå, enklest mulig å verifisere (korrekthet), og enklest mulig å endre/bygge videre på. Alt dette samtidig som at løsningen må være god nok – da tenker jeg blant annet på krav til ytelse.

Og det er her de ulike programmeringsparadigmene og teknikkene kommer inn og tilbyr ulike måter å pakke inn kompleksiteten på.

OOP

Det objektorientert programmering tilbyr er å pakke inn kompleksitet i objekter. Internt i objektene kan det være mye kompleksitet, men de presenterer et tydelig grensesnitt til dem som skal forholde seg til objektene. Det som skjer på innsiden har ingen utenforstående noe med. Dette gjør programmer enklere å forstå, endre osv. Gitt at det er gjort på den “riktige” måten.

Men i tillegg til den skjulte kompleksiteten internt i objektene uttrykkes den viktige kompleksiteten for programmet i hvordan objektene samhandler med hverandre. Altså: Objekter er noe som er enkelt å forstå i seg selv – de er tydelige på hva de gjør – men når de brukes sammen med andre skapes det et komplekst nettverk av interaksjon.

Eller som foreleseren kanskje ville ha sagt det: En bil er en bil, og den er enkel å bruke. Du behøver ikke forstå hvordan den virker. Men når alle skal kjøre hjem klokken 16:00 oppstår det stor kompleksitet som kan være vanskelig å forstå.

I OOP har vi enkle objekter, men tillater kompleksitet i hvordan de fungerer sammen. For å forstå et objektorientert program må man kjenne samhandlingen mellom objektene, og hvilke bi-effekter disse samhandlingene har.

FP

Den sentrale enheten i funksjonell programmering er funksjonen. Og den ideelle funksjonen er enkel. Blant annet fordi den er en matematisk funksjon – gitt et bestemt input gir den alltid samme output. Funksjonen henger ikke på et objekt, og er ikke avhengig av tilstand.

Konvensjonen i funksjonell programmering er derimot å akseptere mer kompleksitet i dataene som funksjonene skal jobbe på. Og funksjonene har mer intim kunnskap om dataene de mottar. Når man skal forstå et typisk program fra den funksjonelle paradigmen må man vite mye mer om strukturen på dataene som flyter gjennom systemet. Men å forstå hvordan funksjonene samhandler er enklere.

OOPvsFP

Ingen av paradigmene kan altså fjerne kompleksitet, men den pakkes inn og plasseres på ulike steder.

OOP ∪ FP

Jeg har tidligere fortalt om hvordan min C#-kode de siste årene har utnyttet mer og mer av de funksjonelle egenskapene til språket. Og Rich Hickey har fortalt at han i flere år kun brukte den funksjonelle paradigmen når han kodet i C# (før han lagde Clojure). Han lagde da kun statiske klasser med statiske metoder, som i prinsippet blir som funksjonelle moduler av funksjoner uten tilstand. Det går altså an å bevege seg langt inn å bevege seg inn i FP fra OOP.

Det finnes også dem som benytter en funksjonelt språk som Scheme på en sånn måte at de programmerer objektorientert. Alt man tranger for å lage objekter er jo tross alt funksjoner med frie variabler.

Men det finnes en rekke språk som er designet for å forene og utnytte begge paradigmene. Til en viss grad gjør kanskje de fleste moderne programmeringsspråk dette. Men de mest åpenbare er språk som F#, Scala og Clojure.

Disse språkene inneholder altså elementer fra begge verdener. Samtidig som de typisk gjør det enklest å løse et problem på én måte, så tillater de den andre. Disse språkene blir som en union av OOP og FP.

Personlig synes jeg dette er utfordrende. Med mange muligheter får man også muligheten til å rote det til. Jeg vingler mellom OOP og FP når jeg programmerer, og den som skal lese og forstå koden må forholde seg til et større sett med teknikker og abstraksjoner.

OOP ∩ FP

Hva om man i stedet kombinerte de to paradigmene på en sånn måte at man håndhevet det beste fra dem begge?

I går kom jeg over en artikkel som het Functional programming in object oriented languages. Her forteller Simon Harris om en reise ikke helt ulik min egen – han programmerer i utgangspunktet objektorientert, men har fått mer og mer interesse for den funksjonelle paradigmen.

Simon har derimot gjort noen litt andre observasjoner og konklusjoner enn meg. Han er glad i OO, og bruker i større grad FP til å forbedre sin objektorientering. Gjennom å være disiplinert følger han flere av “reglene” fra funksjonell programmering i sine objektorienterte design.

Han bruker altså ikke OOP der hvor det er best og FP der det er best, men smelter det sammen til en kombinert paradigme. Han opererer i skjæringspunktet mellom OOP og FP.

OOPvsFP2

Dette fikk meg til å tenke tanker jeg ikke har tenkt før. Hvordan ville et språk som kombinerte OOP og FP på denne måten se ut? Hvordan ville det vært å programmere i det?

Immutable objects

Hva om språket ikke tillot at objektenes data endret seg? For en C#-utvikler betyr det at alle felt ville vært merket med readonly. Det er dette vi ofte kaller for Value Objects. Kunne du programmert kun med slike objekter?

De som kjenner funksjonell programmering vil si “ja”, dette er mulig.

I artikkelen snakker Simon mye om egenskapene til et slikt designvalg; at de f.eks. gir et tydeligere skille mellom commands og queries (CQS, et prinsipp fra objektorientert programmering). Og at slike objekter danner grunnlaget for såkalte persistente datastrukturer (som er sentrale for ytelsen i språk som Clojure).

Hva om språket også håndhevet at alle metodene på objektet måtte bruke objektets tilstand på en eller annen måte? Altså at det ikke er lov med metoder som kunne vært statiske. Vil dette kunne føre til god objektorientering?  Vil det føre til mer komplett test coverage? Vil det gi kode som er enklere å forstå og endre? Simons erfaringer kan tyde på dette.

Tankene er nokså ferske, men jeg kunne godt tenkt meg å forsøke å kode i et slikt språk. Som Simon kan jeg selvfølgelig være disiplinert og forsøke å følge disse prinsippene når jeg jobber i OO-språk. Men egentlig fikk jeg mest lyst til å lage et nytt språk.

Kanskje jeg kan gjøre en mellomting og bruke Lisp med et sett med makroer som gjør denne måten å kode på enkelt?

Konklusjon

De ulike programmeringsparadigmene handler om ulike måter å strukturere kompleksitet og gjøre den håndterbar. Paradigmene har ulike tradeoffs: OOP tilbyr en objektabstraksjon som skjuler kompleksitet, men man må forstå hva som skjer når objektene samhandler. FP tilbyr funksjoner som er enkle å forstå, men man må forstå mere komplekse datastrukturer og datatransformasjoner (eller funksjonskomposisjoner).

Språk som kombinerer OOP og FP i dag gjør det ofte (slik jeg ser det) mulig å skrive kode som kombinerer “det værste” fra begge verdener. Hva om vi hadde et språk som oppfordret til å kun kombinere “de beste” elementene? FOP – funksjonell objektprogrammering!  Det ville vært et strengere språk som gav mange føringer til hvordan man skal kode. Men jeg tenker det det hadde vært spenende å prøve.

Ackenhausen om BDD og arkitektur [Luke 7, 2012]

Friday, December 7th, 2012
2 kommentarer

Jeg ble kjent med Svein Arne Ackenhausen da jeg ble en del av programkommiteen for Norwegian Developers Conference. Under ser du ham i jam session med Carl Franklin fra .NET Rocks! 

Svein Arne har bidratt til årets kalender med en tankevekkende artikkel – den er lang, men bør leses. Den har potensiale til å gi mang en utvikler dårlig samvittighet.

acken

Hvem er du?
Trommis som byttet ut noter med kode og bandet med et konsulentfirma.

Hva er jobben din?
Driver et lite konsulentfirma ved navn Contango Consulting AS hvor jeg skriver kode for de som måtte ønske det.

Hva kan du?
Er blitt ganske flink til å se på programmering forbi programmeringsspråkene.

Hva liker du best med yrket ditt?
Følelsen av å ha skapt noe som faktisk er til nytte for andre.


Hva BDD skulle gjort med arkitekturen vår

Når jeg hører Dan North snakke om BDD er det som om alle brikkene faller på plass. Dette er hva software utvikling handler om. Strukturering av språk og kommunikasjonsform hjelper oss til å definere problemene vi prøver å løse. Når vi forstår problemet er det også en mulighet for at vi klarer å løse det. Spol frem 6-12 måneder og vi har akkurat det produktet kunden alltid har drømt om. Vel kanskje ikke men det er sånn vi ønsker å se på det.

Det BDD har lyktes med er å bidra med et sterkere fokus på å mer presist definere hva systemet gjør. Vi kommuniserer, vi skriver Given When Then (GWT) tekster (jeg sier bevisst ikke tester) sammen med beslutningstakerne. Vi lærer oss å forstå og utvikle konseptene i produktet. Dette er veldig bra! Vårt yrke har lenge hatt et for teknisk fokus. Vi «teknifiserer» problemet i et forsøk på å enklere kunne løse det teknisk. Arkitekturen, et pyramidisk sett med bokser (lag delt arkitektur) / et veldefinert sett med løkringer (onion architecture) osv. Vi får alle et bilde i hodet når vi tenker på arkitektur. For eksempel et ark bestående av piler og bokser. Faktum er at vi skal løse IKKE-TEKNISKE problemer ved hjelp av teknologi. Som tekniske kvinner og menn er vi blitt nødt til å innse at teknologien kommer i andre rekke i forhold til problemet vi prøver å løse.

La oss gjøre et lite sammendrag:

  • Vi bruker kommunikasjon og metoder som GWT som verktøy for å definere hva vi ønsker å produsere.
  • Den tekniske løsningen er et resultat av og kommer derfor i andre rekke i forhold til funksjonaliteten.
  • Arkitekturen definerer den totale tekniske løsningen.

Som jeg begynte med er det en følelse av noe nesten magisk når Dan North snakker om BDD. Når jeg ser hvordan BDD adopteres i praktisk bruk klarer jeg sjelden å beholde den følelsen. Det virker på meg som om vi mister noe på veien fra konsept til praksis. Hvis du kjenner deg igjen i og er enig i de tre punktene over så gjør følgende: Tenk tilbake på prosjektene du har vært med på å utvikle. Se for deg kun arkitekturen.

Ok, så softwareproduktet kan defineres som summen av behaviours. Implementasjonen av en spesifikk behaviour er en konsekvens av behaviouren. Det betyr at den utfører de nødvendige stegene for å tilfredsstille behaviouren. Arkitekturen er da summen av alle implementerte behaviours. Altså Behaviours -> Implementasjoner -> Arkitektur. HVORFOR KAN JEG DA IKKE FORSTÅ HVA PRODUKTET GJØR UT I FRA ARKITEKTUREN??

Avstanden mellom behaviours og det tekniske

Som oftest når jeg ser et arkitekturdokument ser jeg noe som beskriver en lagdeling som vist på bildet under. I tillegg til dette lagdelingsbildet kommer et massivt databaseskjema som er beskrevet ned til hver minste detalj. Vi sprinkler på litt tekst for å forklare noen deler av arkitekturen bedre og voila! her har vi arkitekturen vår.

Ok, dette var en sterk forenkling av det hele, men jeg håper du forstår tanken. Vanligvis ser jeg GWT-tekster implementert som kjørbare tester. Det er forsåvidt bra. Tester er en god ting!

Problemet er bare at arkitekturen som beskrevet over representeres ikke som et resultat av Behaviours -> Implementasjoner -> Arkitektur. Den er mer et koderammeverk hvor vi kan dele hver behaviour inn i en UI del, en tjeneste del, en domene del og en data del. Deretter tar vi hver av delene og implementerer de der de hører hjemme. Vi har altså sett på behaviouren, teknifisert den inn i UI, tjeneste, domene og data så den passer inn i en forhåndsdefinert lagdelingsstruktur.

Resultatet blir at vi distanserer implementasjonen fra hva produktet faktisk skal gjøre. Dette leder igjen til at vi tar en god del beslutninger på helt feil grunnlag.

Men la oss først gå tilbake til BDD i praksis. I og med at vi har distansert implementasjonen fra behaviours er vi nødt til å abstrahere implementasjonen fra GWT-tekstene når vi gjør de om til eksekverbare spesifikasjoner. Resultatet blir dette:

Grunnet at det ikke er noen klar relasjon mellom implementasjon og spesifikasjon ender vi opp med et stort sett med systemtester rundt en blackbox. Den eneste måten vi kan teste en blackbox på er igjennom systemtester. Systemtester er som vi vet veldig skjøre, veldig avhenginge av miljø, veldig tunge å vedlikeholde og generelt sett veldig utsatte for feil. En test man endrer oftere enn koden den tester har også høyere risiko for feil enn koden den prøver å verifisere. Altså faller den på sin egen urimelighet.

Vi var lenge enige om at systemtester var et nødvendig onde. Vi trengte noen av dem men vi holdt antallet så lavt som mulig fordi vi visste hva bakdelene med dem var. Jeg vil gå så langt som å si at et stort antall systemtester ofte er en code smell. Det tyder på at det underliggende systemet er en blackbox.

Hvordan ble systemet en blackbox? Hva gjorde at implementasjonen ikke ble til som en konsekvens av behavioren? Jeg tror mye av det ligger i hvordan vi tradisjonelt sett tenker på arkitektur. Tradisjonelle arkitekturer legger ofte føringer på hvordan kode skal implementeres i hvert lag. Dette betyr at vi tilpasser behaviouren til teknologien og ikke motsatt.

Et godt eksempel på akkurat dette er måten vi implementerer det vi kaller data-laget på. For det første ser jeg nesten aldri noe som kan kalles et datalag. Et lag er en absolutt separasjon. Et lag eksponerer ikke sin interne state. Det vil si at i et datalag kan ikke unit of work/transaksjonener eksponeres. Med det er vi inne på på noe veldig essensielt. Behovet vi har for transaksjoner og unit of works er grunnen til at ikke implementasjonen blir til som et resultat av behaviouren.

Forstå problemet og implementer det eksplisit

Så hvorfor trenger vi egentlig konseptet transaksjon utenfor persisteringslogikken? En generisk unit of work / transaksjon er noe vi trenger når vi ikke vet hva som er resultatet av et funksjonskall. Det vil si, vi starter en unit of work, kaller en overordnet metode som utfører en behaviour med alt det den måtte ville endre og til slutt committer unit of work. Realiteten er følgende: Vi vet ikke hva denne behaviouren gjorde. Hva den nå enn gjorde så ble alle endringer lagret et eller annet sted når vi committet.

Vi har med det distansert oss fra hva resultatet av behviouren er. Selve essensen i problemet vi prøver å løse forsvinner i “hva som nå enn ble committet”.

Hadde vi satt oss ned og funnet ut akkurat hva resultatet av behaviouren er kunne vi i stedet sagt at funksjonen vi kaller returnerer resultatet av behaviouren. Det gir oss en klar input og en definert output. Det kan trygt testes! Vi vil da separere mellom det å utføre behaviouren og det å persistere eventuell resulterende state. Dette er en veldig viktig distinksjon.

Toppnivå-funksjonen vi vanligvis kaller innenfor en unit of work kan da isteden returnere resultatet av behaviouren. Det returnerte resultatet kan sendes videre til noe som håndterer persistering. Innenfor funksjonalitet fokusert rundt ren persistering er det helt naturlig å håndtere eventuelle transaksjoner. Dette er relatert til det Greg Young og Udi Dahan snakker om i CQRS når de nevner «transaction per aggregate». Ved å gjøre implementasjonen eksplisitt har vi kunnet fjerne behovet for unit of works. I stedet for å bygge systemet rundt en generisk persisteringsplatform fokuserer vi heller på persistering som direkte relatert til behaviouren. Det vil si at vi kan oppfylle punkt to over: «Den tekniske løsningen er et resultat av og kommer derfor i andre rekke i forhold til funksjonaliteten».

Systemets koblingspunkter

Det vi definerer inn i det vi kaller lagdeling er en liten del av arkitekturen men det er ikke arkitekturen. Hva er det da?

Dette er min definisjon. Jeg velger å heller notere punkter istedenfor lag som vist over. Punktene definerer hovedabstraksjonene i systemet. Transport-punkter en behaviour kan bevege seg imellom. For å få noen nytte av transportkartet må det knyttes opp mot selve behaviourene. La oss si at en av behaviourene i dette systemet er live rapportering av varme i UI’et. Vi kan da definere opp stegene i behaviouren som i bildet under.

 

Ved å representere hovedabstraksjoner som punkter har vi ikke lagt føringer på hvordan en behaviour skal implementeres, bare hvilke stopp reiseruten involverer. Vi er derfor frie til å definere implementasjonen av behaviouren på den måten som er best egnet.

En behaviour kan ofte sammenlignes med en funksjonell kjede. Man starter kjeden med et sett med data (parametere) man sender med det første funksjonskallet. Deretter igjennom flere steg transporterer, transformerer og beriker vi den initielle dataen. Når vi kommer til det siste steget i behaviouren sitter man med et sett med data som representerer konklusjonen av hva behaviouren er ment å oppnå.

Normen pr i dag er i stedet å lage en generisk arkitektur som teknisk definerer hvordan alle behaviours skal se ut. Vi definerer for eksempel at alle kall skal komme igjennom service laget. Deretter skal domeneklasser implementeres ut i fra n tekniske guide lines. Datalaget har selvfølgelig også sterke føringer på hvordan det skal implementeres.

Ok, stop!

La oss i stedet si at en behaviour selv dikterer hvordan den skal implementeres. Vi vil da gå bort fra den vanlige “one size fits all”-tankegangen.

Tanken mange har er at et system er mer forståelig når alle behaviours er implementert på samme måte. Jeg forstår hvor vi vil med en slik tanke men vi klarer ikke å se hvor stort kompromisset er ved en slik løsning. Vi har lært oss til å tro at en slik løsning blir en mindre kompleks løsning fordi koden da vil se enhetlig ut. Problemet er: Det at koden ser enhetlig ut er irrelevant! Det er en missvisende trygghet!

Kode-evolusjon og stabilitet

Problemet enhetlig kode / kode som ser lik ut kan ha er best forklart ved å se på DRY prinsippet. DRY (don’t repeat yourself) er kanskje den største fallgruven vi som utviklere kan havne i. Missforstå meg rett – DRY er en god ting når det er brukt riktig, men når det er brukt feil er det et masseødeleggelsesvåpen!

Når man snakker om DRY glemmer man ofte å nevnte kontekst. Uten kontekst ødelegger DRY mer enn det hjelper. På plassen etter å faktisk løse problemet produktet er ment å løse kommer stabilitet. Vi må derfor være sikre på at når vi endrer et sted i koden så ødelegger vi ikke noe annet.

Kode skal IKKE være DRY fra et teknisk perspektiv men fra et funksjonelt perspektiv. Hele tanken bak DRY er at man ikke skal ha duplisert kode. Når man endrer kode skal man vite at man kun trenger å endre ett sted. Nøkkelen til dette ligger i å vite hva som endres sammen. Endringer kommer fra stakeholders. Endringer er nesten alltid rettet mot en behaviour. Vi må kunne garantere for at vi kan gjøre endringer i en behaviour uten å ødelegge andre behaviours. Produktets evolusjon kan ikke holdes tilbake som et resultat av at vi er redde for å ende opp med et ustabilt produkt. Vi må
iverata stabilitet samtidig som vi lar produktet evolvere.

Vi må derfor avgrense DRY til å gjelde innenfor behaviours, ikke alt som ser teknisk likt ut. Behaviours kan ofte inneholde de samme konseptene. Det vil ikke si at når et delt konsept endres i den ene behaviouren så skal det automatisk endres i den andre. Vi vet at hvis vi tar på noe, ødelegger vi det. Det er bare sånn softwareutvikling er. Det som endres sammen deler samme kode og det er da mer forståelig at det ødelegges sammen ;) (bruk av plumbing og rammeverk er unntak).

Vet vi hvor store kompromiss vi aksepterer?

La oss gå tilbake til problemet med “one size fits all”-arkitekturen. Denne typen implementasjon forutsetter også at alle behaviours er like. Hvis det er tilfellet er det verdiløs kode å skrive.

Se på Ruby on Rails. De har forstått at hvis applikasjonen din er ren CRUD så trenger du ikke skrive koden. Du kan i stedet generere den. Alle behaviours er bygget opp på samme måte: Hente data, endre data, lagre endringen. Med en gang det begynner å komme behaviours som ikke er CRUD ender vi isteden opp med et sammensurium hvor alt ser enhetlig ut. Det vil være det samme som om jeg forteller deg at den personen snakker italiensk men jeg kan ikke fortelle deg hva hun/han sier. Jeg forstår strukturen i språket men ikke innholdet. Dette er kompromisset vi tar ved å fokusere på at kode ser teknisk lik ut. Vi forstår at vi jobber med repositories, services, entiteter og units of work. Men vi har ingen anelse angående hva den gjør. Dette er et ENORMT kompromiss. Vi har allerede sagt oss enige i at behaviours er viktigere enn implementasjonen som er et resultat av behaviouren. Likevel har vi endt opp med å vurdere lesbarheten av de tekniske konseptene høyere enn forståelsen av behaviours.

Konklusjon

Software er ment å endre seg. Arkitekturen er ment å endre seg. Med mindre du har klart å lage et perfekt under av et produkt på første forsøk kommer systemet til å endre seg. Så lenge det endrer seg betyr det at vi forstår mer om problemet vi prøver å løse. Vi perfeksjonerer produktet mot å løse stakeholders problem på best mulig måte.

For kontinuerlig å kunne forbedre produktet må det være optimalisert for endringer. Det er derfor kritisk at behaviours er implementert i isolasjon. Som nevnt tidligere er endringer nesten alltid relatert til en behaviour. For å kunne ivareta det må vi sørge for at koden er nærmere knyttet til selve problemet.

BDD har satt oss på rett spor. Vi må bare ta det videre inn i implementasjonen og arkitekturen. Et for sterkt fokus på den teniske strukturen av softwarearkitektur vil selv med gode hensikter ofte lede til en stor andel “accidental complexity”. Kompleksitet som er der fordi distansen mellom den funksjonelle definisjonen av systemet og den tekniske definisjonen er for stor. Å krympe/eliminere denne distansen er hva BDD skulle gjort med arkitekturen vår!

Hvorfor map/filter/reduce?

Tuesday, November 6th, 2012
7 kommentarer

Map, filter og reduce er tre funksjoner jeg er veldig glad i, uansett hvilket språk jeg programmerer. Jeg har snakket om dette mange ganger. Første gang var i artikkelen jeg kalte filtrer, projiser, aggreger. Og sist var i artikkelen Den Lille Erlanger. Og nå gjør jeg det jammen meg igjen.

For hva er poenget?

Hvorfor skal man bruke disse funksjonene?

Den viktigste grunnen er å gjøre koden enklere å lese. “Hæ?” sier du kanskje, for du synes jo det er mye enklere å lese kode med for-løkker – det er jo det du er vandt til.

Løkker har et problem; de er veldig fleksible! En løkke kan brukes på mange forskjellige måter, og til mange forskjellige ting. For å forstå hva en løkke gjør må man ikke bare lese selve løkke-deklarasjonen, men også alt inneholdet i løkken. Parametre som brukes til å avgjøre når løkken skal avsluttes kan endres når som helst i løkke-kroppen. Kodeord som continue og break kan også radikalt endre hvordan løkken oppfører seg. Og hva er resultatet av løkken? For å svare på det må man lete etter alle mutasjoner som har skjedd underveis.

loop_hell

Et alternativ til løkker er rekursjon, men det kan være like vanskelig å forstå rekursiv kode.

Map, filter og reduce representerer derimot tre helt konkret forskjellige måter å iterere på, tre konkrete områder man kunne brukt løkker eller rekursjon. Ved å bruke disse tar man bort litt fleksibilitet. I stedet er man tydeligere på hva hensikten med koden er.

La oss se litt nærmere på hver av funksjonene. Hvilke typer løkker er det de erstatter? Eksempelkoden er i JavaScript, slik at flest mulig kan følge med.

Map

Map (kalt select i C#) er en funksjon som tar inn en liste, og returnerer en ny, like lang liste. Sett at vi har en funksjon “twice” som ganger et tall med 2. Om man skal “mappet” en liste med tall med denne funksjonen i en løkke vil det se slik ut:

var twice = function(x) {
  return x * 2;
};

var listDoubler = function(lst) {
  var result = [];
  for(var i = 0; i < lst.length; i++) {
    result[i] = twice(lst[i]);
  }
  return result;
};

Kode som dette har de fleste av oss skrevet mange ganger. Vi oppretter en liste, looper over en annen liste, og for hvert element gjør vi noe og putter resultatet i den første listen. Til slutt returnerer vi den ferdige listen.

Hver gang du ser kode som det så skal du vite at det kan gjøres mye enklere ved hjelp av map:

var listDoubler2 = function(lst) {
  return map(lst, twice);
};

Som du ser er det mye enklere å lese hva som skjer i “listDoubler2″. Men jeg har jukset litt, for JavaScript har i utgangspunktet ingen map-funksjon. Den må du lage selv. Men det er jo veldig enkelt, for den er nesten identisk med den første versjonen av “listDoubler”:

var map = function(lst, f) {
  var result = [];
  for(var i = 0; i < lst.length; i++) {
    result[i] = f(lst[i]);
  }
  return result;
};

Filter

Filter er en funksjon som tar inn en liste, og returnerer en ny som inneholder noen utvalgte element fra orginallisten. Sett at vi har funksjonen “even” som avgjør om et tall er partall. Da kan vi bruke en løkke til å filtrere ut partallene i en liste:

var even = function(x) {
  return x % 2 == 0;
};

var onlyEven = function(lst) {
  var result = [];
  for(var i = 0; i < lst.length; i++) {
    if (even(lst[i]))
      result.push(lst[i]);
  }
  return result;
};

Med en filter-funksjon blir det igjen mye enklere:

var onlyEven2 = function(lst) {
  return filter(lst, even);
};

Og som før er det enkelt å implementere en filter-funksjon:

var filter = function(lst, p) {
  var result = [];
  for(var i = 0; i < lst.length; i++) {
    if (p(lst[i]))
      result.push(lst[i]);
  }
  return result;
};

Reduce

Reduce (også kjent som fold, inject, aggregate m.m.) er litt vanskeligere å få taket på. Den tar på en måte og “slår sammen” en liste til én verdi. Denne verdien kan være en ny liste, men trenger ikke være det. Reduce er ikke så restriktiv som map og filter, og kan dekke veldig mange itereringsbehov. Funksjonen er likevel bedre enn iterative løkker (når man blir vandt til dem), fordi den begrenser hva man kan gjøre, og derfor gir mer kode som er enklere å forstå.

Sett at vi har en fuksjon som slår sammen tekst-representasjonen av to verdier. Vi kan da lage en funksjon vi kaller “stringifyList” som slår sammen alle elementene i en liste ved hjelp av nevnte funksjon:

var stringJoin = function(a, b) {
  return a + "" + b;
};

var stringifyList = function(lst) {
  var result = "";
  for(var i = 0; i < lst.length; i++) {
    result = stringJoin(result, lst[i]);
  }
  return result;
};

Og så kan vi velge å bruke reduce i stedet. Da ser det sånn ut:

var stringifyList2 = function(lst) {
  return reduce(lst, "", stringJoin);
};

Hver gang du itererer over en sekvens for å produsere en eller annen verdi så kan du bruke reduce. Begynner du å se etter slike steder så finner du dem over alt.

Og her er selve implementasjonen av reduce:

var reduce = function(lst, acc, f) {
  for(var i = 0; i < lst.length; i++) {
    acc = f(acc, lst[i]);
  }
  return acc;
};

“acc” er en vanlig forkortelse for accumulator.

Map og Filter vha. Reduce

Som antydet er reduce en mer generell funksjon enn map og filter. Og nå som vi har reduce i verktøyskrinet vårt så kan vi faktisk implementere map og filter ved hjelp av denne:

var map2 = function(lst, f) {
  return reduce(lst, [], function(acc, n) {
    acc.push(f(n));
    return acc;
  });
};

var filter2 = function(lst, p) {
  return reduce(lst, [], function(acc, n) {
    if(p(n))
      acc.push(n);
    return acc;
  });
};

Har du sett noe så vakkert?! :)

En advarsel

Hovedpoenget med å bruke disse funksjonene er altså å skrive kode som er enklere å forstå. Den er enklere å forstå fordi man følger visse regler. Man bruke map til å “mappe”, man bruker filter til å filtrere, og man bruker reduce til å redusere en liste til én ny verdi.

Men i mange programmeringsspråk kan disse reglene brytes. Man kan ikke nødvendigvis avslutte en iterasjon for tidlig eller hoppe over et element, men man kan gjøre ting som har uventede sideeffekter.

Dette er det viktig at man unngår. Skal man gjøre noe som bryter med konvensjonene så får man bruke et annet verktøy; en løkke eller rekursjon. Hvis ikke kan man ikke lenger gjøre de riktige antagelsene når man leser kode.., og man er like langt!

Event sourcing med s-expressions

Sunday, June 10th, 2012
2 kommentarer

Helt siden i september 2009 (da Greg Young var på besøk i Bergen) har jeg tenkt på event sourcing. Og i årene etter har dette blitt en mer og mer populær arkitektur blant de softwareutviklerne som er mest cutting edge.

Men jeg har ikke bygget noe med det i tiden som har gått siden da. Å treffe Greg igjen på NDC i år har derimot stimulert meg på nytt, og her følger en blogpost som forklarer hva det går i, og som implementerer et lite eksempel.

Blogposten er også en liten leksjon i programmeringsspråket Clojure. Eksempelet er en nedskalert begynnelse på et verktøy jeg kommer til å lage for eget bruk, og dette språket er ideelt for oppgaven. Men kunnskap om Clojure er absolutt ikke nødvendig for å henge med i det jeg gjør, alt du behøver er et åpent sinn.

Hva er Event Sourcing?

Event sourcing består av to steg:

  1. Man lagrer alle endringer i et system som en sekvens av hendelser.
  2. Man bygger opp igjen systemets tilstand fra disse registrerte hendelsene.

event_sourcing

Se for deg hvordan en bank overfører penger fra en konto A til en konto B. De gjør ikke som i de typiske kodeeksemplene hvor man har en databasetransaksjon, og oppdaterer saldoen på de to kontoene i én atomisk operasjon. Nei, saldoen i banken din består av en serie med hendelser:

  1. Konto opprettet, saldo kr 0,-
  2. Innskudd a kr 30 000,-
  3. Overføring fra konto X a kr 560,-
  4. Betalt regning a kr 1 499,-
  5. 1,1% renter effektuert
  6. Debetkort belastet kr 19.50
  7. osv…

Selv om den ikke står der eksplisitt er det enkelt å beregne saldoen.

På grunn av at man kan bygge opp igjen systemets tilstand fra hendelsene kan man ofte klare seg uten noen annen form for lagring av tilstand. I praksis vil man nok ofte likevel lagre data i et annet format for eksempel for rapportering, og man vil også ta “snapshots” av tilstand for å slippe å kjøre gjennom hele loggen hver gang man trenger tilstand. Men rapportene og snapshotene kan når som helst forkastes og genereres på nytt fra loggen.

Fordelene med event sourcing

Fordelene med event sourcing er mange, men det kan være et komplekst tema å diskutere. Jeg har ikke mye erfaring, så jeg vil ikke gå i dybden, men her er de viktigste punktene slik jeg ser det:

  • Event sourcing innebærer såkalt append only data storage – man behøver ALDRI oppdatere data. Dette løser flere problemer, spesielt knyttet til transaksjoner og samtidighet, og skrivehastigheten til systemet kan øke dramatisk. Skulle samtidighetskonflikter mellom endringer snike seg inn så er det også mye enklere å rydde opp i dem med en hendelseskø.
  • Et system som baserer seg på event sourcing er godt tilrettelagt for testing. Man kan simulere all mulig adferd ved å sette opp en sekvens av hendelser. Og opplever man problemer i produksjon kan feilene alltid gjenskapes ved å kjøre hendelsesloggen derfra. Dette er gull!
  • Hendelsesloggen inneholder ikke bare hvilke dataendringer som har skjedd, men eksplisitt hvorfor de skjedde. Meningen bak tilstandsendringene blir tatt vare på. Dette er noe som mangler i de fleste “vanlige” systemer i dag, og er verdifullt av mange grunner. En av dem er at man etter at systemet er i produksjon kan komme opp med nye måter å analysere bruken på, og så kjøre disse analysene på historiske hendelser.
  • Og finner man en bug i systemet; da fikser man den, ruller systemet tilbake, og alle hendelsene blir registrert på nytt med den nye koden. Feil i data vil “fikse seg selv”.

Hvorfor (og hva er) s-expressions?

For noen år siden var XML det ultimate dataformatet. Nå føles det som om JSON er blitt vårt standard format, både for lagring og for kommunikasjon mellom tjenester. Men det har lenge funnes et annet format i denne kategorien, et format som i sin tid ble utviklet for å representere både kode og data (i form av lister og trær) i programmeringsspråket LISP. Dette er såkalte symbolske uttrykk, gjerne forkortet til sexprs eller sexps.

Sexps er enkle å parse, mye mindre bråkete enn XML, og mere fleksible enn JSON. Du ser ikke mye til dette formatet utenfor Lisp-sfæren, men det er ekstremt kraftig. Hvorfor? Fordi de kan representere data og programkode på en og samme tid.

I denne blogposten skal jeg implementere en del av et todo-program. I programmet skal man kunne opprette todo-items, flytte dem rundt mellom ulike “bøtter”, markere dem som utført, slette dem, og lignende handlinger.

Hva om jeg lar todo-programmet logge alt som skjer til en fil, og jeg logger på et format som dette:

(todo-created (id t1) "Experiment with event sourcing s-expressions")
(todo-created (id t2) "Draw some good event sourcing illustrations")
(todo-created (id t3) "Blog about event sourcing")
(todo-moved   (id t1) (todo-bucket today))
(todo-moved   (id t2) (todo-bucket tomorrow))
(todo-moved   (id t3) (todo-bucket tomorrow))
(todo-done    (id t1))
(todo-created (id t4) "Create an awesome todo system")
(todo-moved   (id t4) (todo-bucket in-two-days))
(todo-created (id t5) "Rule the World!")
(todo-moved   (id t5) (todo-bucket later))
(todo-deleted (id t5) (reason "Just kidding"))

Det du ser over er kun en tekstfil. Men loggelinjene er formatert som sexps. Altså er de gyldige Lisp-uttrykk. Hva om jeg så implementerte funksjoner som het todo-created, todo-moved osv., og så kjørte loggfilen gjennom en Lisp-tolker – f.eks. Clojure?

Loggen vil da ha blitt til et program skrevet i et domenespesifikt språk som gjenskaper tilstanden som hendelsene loggen er generert ut ifra en gang før skapte.

Det kan tenkes du bør lese den forrige setningen mer enn én gang!

En implementasjon i Clojure

Så la oss skrive litt kode som er i stand til å gjøre dette. Jeg begynner med å deklarere et navnerom, og oppretter så en variabel todos (globalt i navnerommet). Denne peker til en hash / dictionary / lookup table som vil holde programmets todo-items.

(ns worklog.core)

;; Todo items are stored here
(def todos (atom {}))

Deretter begynner jeg å skrive implementasjoner for nøkkelordene i loggfilen. Jeg tar de enkleste først – dette er små makroer som transformerer symbolene de omkranser til spesifike datatyper i Clojure (henholdsvis string for id og keyword for todo-bucket). Reason, som brukes i siste linje i loggfilen, tar en streng og ikke et symbol som parameter, og returnerer den derfor bare som den er. Dette er et eksempel på en funksjon som bare eksisterer for å gjøre loggfilen mere leselig.

(defmacro id [id]
  (str id))

(defmacro todo-bucket [bucket-id]
  (keyword bucket-id))

(def reason identity)

Og så har jeg kommet til den første seriøse funksjonen, nemlig todo-created. Parametrene er en todo-id og en todo-beskrivelse. Hvis et todo-item med gitt id allerede finnes i todos kaster den et exception. Hvis ikke legger den til et nytt objekt i en default bøtte (staging) og med default tilstand (open):

(defn todo-created [id description]
  (if (@todos id)
    (throw (Exception.
             (str "Todo with id " id
                  " already exist, can't create!")))
    (swap! todos assoc id { :id     id
                            :desc   description
                            :bucket :staging
                            :state  :open })))

Før jeg implementerer de resterende tre funksjonene trenger jeg en liten makro – dette er for å unngå at jeg repeterer meg selv i koden. De tre funksjonene skal nemlig oppdatere et todo-item, så da lager jeg en makro jeg kaller update-todo! som gjør det for en gitt todo-id, et feltnavn, og en verdi.

Makroen kontrollerer at et todo-item med den gitte identifikatoren eksisterer, og kaster et exception hvis det ikke gjør det:

(defmacro update-todo! [id key val]
  `(if-let [todo# (@todos ~id)]
     (swap! todos assoc-in [~id ~key] ~val)
     (throw (Exception.
              (str "Unable to find todo " ~id)))))

OBS: Nærmere gjennomlesning fikk meg til å innse at jeg her har laget en makro helt unødvendig – dette kunne ha vært en helt vanlig funksjon. Jaja..!

Når jeg har update-todo! er det trivielt å implementere funksjonene for å flytte, markere som utført, og å slette et todo-item:

(defn todo-moved [id bucket]
  (update-todo! id :bucket bucket))

(defn todo-done [id]
  (update-todo! id :state :done))

(defn todo-deleted [id _reason-ignored]
  (update-todo! id :state :deleted))

Den siste funksjonen illustrerer også et case hvor loggfilen inneholder informasjon som jeg ikke behøver for å gjenskape programmets tilstand – nemlig årsaken til hvorfor et todo-item ble slettet.

På tide å laste event-historikken

Alt jeg nå trenger å gjøre for å laste todo-listen min er å laste logfilen og eksekvere den. Følgende funksjon gjør det (den er egentlig bare en wrapper som gir meg en funksjon med et litt mere spesifikt navn):

(defn load-events-from-file [file]
  (load-file file)
  :ok)

Hvis jeg nå fyrer opp en REPL i terminalen min og laster DSL-koden så kan jeg kjøre funksjonen og gi den stien til loggfilen min (altså filen gjengitt i toppen av bloggposten):

worklog.core=> (require 'worklog2.core :reload)
nil
worklog.core=> (load-events-from-file "./todo.log")
:ok

Å skrive ut todo-listen

load-events-from-file svarer med nøkkelordet “ok”. todos-variabelen vil nå inneholde todo-itemene mine. Vi kan se på innholdet direkte, men i stedet tar jeg meg tid til å vise deg litt kode jeg har skrevet for å presentere listen litt bedre.

Funksjonene er små og har gode navn, så se om du klarer å skjønne hva de gjør (det kommer ingen forklaring):

;; Formating functions to print todo list

(defn todo-state-to-str [t]
  (condp = (:state t)
    :done    "DONE"
    :deleted "XXXX"
    :open    "    "))

(defn todo-2-str [t]
  (str "  [" (todo-state-to-str t)  "] "
       (:desc t)))

(defn todos-for-bucket [b todos]
  (filter #(= (:bucket %) b) todos))

(defn bucket-to-str [b-id]
  (let [s (name b-id)]
    (str (.toUpperCase (subs s 0 1))
         (.toUpperCase (subs s 1)))))

(defn print-bucket [id todos]
  (println (bucket-to-str id))
  (doseq [t (todos-for-bucket id todos)]
    (println (todo-2-str t))))

(defn print-todos
  ([] (print-todos (vals @todos)))
  ([t]
    (println "--- TODO-LIST ----------------------------")
    (doall (map #(print-bucket % t)
                [:today :tomorrow :in-two-days :later]))
    'EOF))

Om jeg nå går tilbake til REPL’en og evaluerer funksjonen print-todos så får jeg en formatert utskrift av todo-listen min slik den ble etter at jeg lastet inn alle eventene fra todo.log:

worklog.core=> (print-todos)
--- TODO-LIST ----------------------------
TODAY
  [DONE] Experiment with event sourcing s-expressions
TOMORROW
  [    ] Blog about event sourcing
  [    ] Draw some good event sourcing illustrations
IN-TWO-DAYS
  [    ] Create an awesome todo system
LATER
  [XXXX] Rule the World!
EOF

Som du kan se er alle items flyttet inn i riktige bøtter, dagens todo-item er utført, og verdensherredømme er kansellert.

Konklusjon

Du har sett meg fortelle om event sourcing, som er en teknikk hvor man lagrer alle hendelser i et softwaresystem, for så å kunne gjenskape nåtilstand (eller tilstanden for et hvilket som helst tidspunkt) ved å kjøre hendelsene på nytt. Dette eliminerer mer eller mindre behovet for klassisk lagring av tilstand, og gir en rekke nye muligheter.

Jo mer jeg tenker på disse mulighetene, jo mer verdifull blir denne teknikken for meg. Jeg kan f.eks. veldig enkelt lage ulike versjoner av DSL’en / parseren som utfører ulike ting basert på hendelsene i loggen. Mulighetene rundt testing, både hva angår enkelhet og styrke, er også veldig interessante.

Se ikke bort ifra at jeg kommer til å begynne å logge i sexps-format overalt fremover. Du er herved advart! :D

Vakker kode fungerer bedre

Monday, April 2nd, 2012
3 kommentarer

beautyeyeJeg er en sånn programmerer som bruker mye magefølelse og intuisjon. Kode skal se bra ut, den skal føles riktig.

For noen dager siden hadde Computerworld en artikkel hvor de skrev om Vakker programvarekode. De fortalte at det er svært vanlig å vurdere hvor vakker kode er, og at de mest erfarne utviklerne er de mest estetikk-orienterte. Artikkelen snakket om studier som viser at vi vurderer estetiske kvaliteter i kode før vi vurderer korrekthet, og at mye tyder på at identifikasjon av stygg kode brukes som indikator på problematisk kode.

Men artikkelen skraper bare i overflaten av hvorfor estetikk er viktig – hvorfor eksperter dømmer kode ut i fra hvor vakker den er. For skal ikke kode bare gjøre jobben sin.., spiller det noen rolle hvordan den ser ut? Datamaskinen bryr seg jo ikke. I går kom jeg til eksempel over en blogpost som hardnakket hevder:

“Your job is to solve a business problem, not to create a thing of beauty. Your ideals—what you feel is attractive, innovative, or effective—are secondary to what your client needs.”

Det er flere grunner til at vakker kode er viktig. Men det jeg synes er mest interessant er hva “vakkert” er for noe. Siden vi mennesker gjennom evolusjonen har utviklet denne egenskapen – å synes at noe er fint – så må det jo nesten ha en funksjon.

Og dette vet vi faktisk en god del om. Når du synes noe er vakkert så kan du se på det som at intuisjonen din forsøker å fortelle deg at det du ser er bra. Noe som er “riktig”, noe som vil fungere godt. Studier har gang på gang vist at vakre utgaver av fremkomstmidler, redskaper o.l. er bedre enn de mindre vakre utgavene (se linker nederst). Tiltalende websider er enklere å bruke enn stygge websider. Dette er ikke fordi vakkerhet gjør ting bedre, men fordi vi synes at velfungerende ting er vakre!

Hjernen vår har to moduser som fungerer side ved side. L-modusen (det vi tradisjonelt har kalt venstre hjernehalvdel) er sekvensiell og analytisk. Det er den vi resonerer med, det er den som er vår indre monolog. R-modusen derimot (trad: høyre) kan du se på som en prossess av veldig mange parallelle tråder. Den er utrolig flink til å søke i hukommelsen vår og finne mønstre blant alle våre erfaringer, og gjennom det løse problemer.

Men R-modusen har ikke noe språk – den bruker ikke ord – og for at vi skal bli bevisste på hva R-modusen kommer opp med må det kommuniseres gjennom andre kanaler. Bilder. Følelser. Intuisjon. Estetikk!

Brain2

Dyktige utviklere (og dyktige utøvere innen de fleste andre felt) lærer seg å bruke R-modusen som en viktig ressurs for å løse problemer. Gjennom erfaring har de opparbeidet seg det man kaller en intuitiv forståelse. I foredraget Hammock-driven Development forklarte Rich Hickey i 2010 hvordan han laster opp hjernen sin med så mye informasjon som mulig om et problem, for så å slappe av (i hengekøyen) og bevisst la være å tenke på problemet. Å “la være å tenke” vil si å roe ned L-modusen, slik at R-modusen får jobbe i fred. Da kommer løsningene raskere, helt “av seg selv”.

Andre kjente teknikker for å aktivere R-modusen er å gå seg en tur, spille foosball på pauserommet, eller på andre måter aktivere kroppen og distrahere den indre monologen. Å sitte stille forran PC-skjermen er ikke den beste måten å løse komplekse problemer på.

Så skjønnhet er viktig! Når du synes kode er vakker er det kanskje hjernen din som bruker en snarvei til å fortelle deg at den er bra.

Men du skal vær bare klar over at R-modusen ikke er feilfri. Vi har alle våre bugs, og intuisjonen vår tar ofte feil. Kjente forelesere i software craftsmanship-bevegelsen som for eksempel Dan North snakker om såkalte cognitive biases (vurderings-skjevhet). R-modus kan brukes til å komme opp med løsningsforslag eller forslag til sannheter, men så bør man analysere (bruke L-modus, bevisst logikk) for å verifisere. I Pragmatic Thinking & Learning (som jeg på det sterkeste vil anbefale) sier Andy Hunt:

“Lead with R-mode; follow with L-mode.”

I den moderne, vitenskapsbaserte verden – og kanskje spesielt i Vesten – er vi veldig fokusert på L-modusen. Det er den vi aktiviserer på skolen, og vi lærer svært lite om hvordan vi kan utvikle intuisjonen vår. Men skal man bli virkelig dyktig er det ingen vei utenom om tappe potensialet som ligger “i høyresiden”.

Vakker kode er ikke bedre i seg selv. Men utviklere synes at kode som kommuniserer godt, er fleksibel og har lav kompleksitet, er vakrere enn annen kode. Og dette kan man bruke som en snarvei; men kan skrive kode man synes er fin, mens det som egentlig skjer er at underbevisstheten hjelper deg og passer på at du gjør ting så godt som mulig, basert på dine tidligere erfaringer.

Så forsøk å gjøre koden din vakker – for det er en grunn til at du har en estetisk sans.

Vakker kode er som regel bedre!

Referanser:
* Emotional Design: Why We Love (or Hate) Everyday Things
* Apparent Usability vs. Inherent Usability
* Aesthetics and Apparent Usability
* A Neuropsychological Theory of Positive Affect and Its Influence on Cognition
* Automatic Effects of Brand Exposure on Motivated Behavior: How Apple Makes You “Think Different”

Template Method del 4: Multippel arv

Thursday, February 2nd, 2012
Ingen kommentarer

tp_del4Du var kanskje ikke klar over det, men Common Lisp er et objektorientert språk. Det påstås faktisk at det har det kraftigste og mest fleksible objekt-systemet av alle språkene vi har, og gjennom å implementere Template Method pattern i Common Lisp håper jeg å gi deg en grei introduksjon til noen av språkets muligheter. Du vil også få se at multippel arv løser utfordringen jeg hadde med den klassiske implementasjonen i del 1.

Pass på at du har designet fra del 1 klart for deg når du ser på denne løsningen.. Og husk at parantesene ikke er farlige :D

(PS: Jeg ser bort fra diskusjonen om det faktisk er gyldig å kalle det jeg presenterer i denne serien for Template Method pattern – delta i debatten her.)

Først trenger jeg å lage selve templaten – noe som definerer skjelettet til algoritmen, men overlater detaljene til en konkret rapport-implementasjon. Templaten opprettes ikke i en klasse som i del 1 og del 2, men heller ikke som en høyereordens funksjon som jeg brukte i del 3. logReport er en prosedyre som tar et rapport-objekt som parameter, og typen av dette rapport-objektet vil avgjøre detaljene i loggfil-prosesseringen.

 2 (defun log-report (report)
 3   "A template for reporting from log files"
 4   (log-report-init report)
 5   (loop for line in (log-report-read report)
 6         do (log-report-process-line report line))
 7   (log-report-cleanup report))

Når jeg i den tredje linjen sier (log-report-init report) så er det det samme som å kalle metoden log-report-init på objektet report. Det neste jeg skal gjøre er å opprette disse metodene. Jeg bruker en makro som heter DEFGENERIC – du kan gjerne se på det som om jeg bruker dem til å opprette abstrakte metoder, slik jeg gjorde i LogProcessor i del 1. Metodene henger derimot ikke direkte på en klasse – klasser i Common Lisp har bare properties, mens metodene defineres for seg:

10 (defgeneric log-report-init (report))
11 (defgeneric log-report-read (report))
12 (defgeneric log-report-process-line (report line))
13 (defgeneric log-report-cleanup (report))

Så bruker jeg DEFMETHOD til å lage noen fornuftige default-implementasjoner av de abstrakte metodene. For initialisering og cleanup lager jeg bare noen tomme metoder, mens metoden som prosesserer en linje bare skriver den ut.

17 (defmethod log-report-init (report))
18 (defmethod log-report-process-line (report line)
19   (format t "~a~%" line))
20 (defmethod log-report-cleanup (report))

Jeg kan også lage en konkret variante av metoden som leser en fil, og jeg mocker fillesingen slik jeg har gjort i de andre delene i denne bloggserien:

28 ; Faking reading the file as usual..
29 (defmethod log-report-read (report)
30   (list "20120125180000000 DEBUG Tick!"
31   "20120125180100000 DEBUG Tick!"
32   "20120125180132112 ERROR Some error occurred"
33   "20120125180133056 ERROR Some other error..."
34   "20120125180200000 DEBUG Tick!"))

Nå kunne jeg ha kjørt koden ved å skrive for eksempel (log-report nil). Nil er nemlig også et objekt i Common Lisp – alt er objekter – og jeg har laget default implementasjoner av metodene som vil fungere for alle typer objekter! Alle linjene fra filen vil bli skrevet ut.

Opprette en klasse

Det er på tide å se hvordan man definerer en ny klasse. Jeg vil nå opprette en klasse for å rapportere errors fra loggfiler, sånn som jeg gjorde i del 1. Til det bruker jeg DEFCLASS:

36 (defclass error-report () ())

Klassen arver ikke fra noe spesielt, og den har ingen properties, så den ble ganske minimal. Men som du vil se er den viktig likevel.

Jeg kan nå opprette nye metoder for de stegene i algoritmen/templaten som er interessante for error-rapporten. Disse metodene vil bli brukt i tilfeller hvor report-objektet er en instans av error-report.

42 (defmethod log-report-init :before ((report error-report))
43   (format t "Errors:~%")) ; Printing a header..
44 
45 (defmethod log-report-process-line ((report error-report) line)
46   (let ((log-type (subseq line 18 23)))
47     (if (equal log-type "ERROR")
48       (format t "~a: ~a~%"
49         (subseq line 0 17)
50         (subseq line 24)))))

Metoden som prosesserer linjene vil erstatte default-implementasjonen, siden den ikke “kaller base/super” (i Common Lisp ville jeg gjort det ved å kalle en funksjon som heter CALL-NEXT-METHOD). Initialisering-metoden vil derimot bli lagt til i tillegg til eventuelt andre initialiseringsmetoder. I dette tilfellet skjer det fordi jeg har brukt :before-nøkkelordet. Before-metoder kaller i forkant av de virkelige metodene, og er bare en av mange måter man kan opprette metoder på.

Hvis jeg nå kjører koden (LOG-REPORT (MAKE-INSTANCE ‘ERROR-REPORT)) så vil den skrive ut headeren og error-linjene fra filen jeg har mocket.

En klasse for FTP-stegene

Nå oppretter jeg en ny klasse for FTP-stegene jeg trenger. Denne klassen arver heller ikke fra noen spesiell klasse, men inneholder én slot (property) for å holde på URLen til loggfilen.

58 (defclass ftp-report ()
59   ((url :initarg :url)))
60 
61 (defmethod log-report-init :before ((report ftp-report))
62   (format t "Fetching ~a~%" (slot-value report 'url)))
63 
64 (defmethod log-report-cleanup :after ((report ftp-report))
65   (format t "Deleting ~a~%" (slot-value report 'url))
66   (format t "Archiving local copy"))

Om jeg nå hadde evaluert (LOG-REPORT (MAKE-INSTANCE ‘FTP-REPORT :URL “some url..”)) ville metodene for å hente og slette FTP-loggen bli brukt, og default metoder for lesing og prosessering hadde også blitt brukt, slik at hele innholdet av filen hadde blitt vist. Men det er ikke det jeg er ute etter…

Multippel arv

Jeg skal nemlig nå lage en ny klasse som arver fra både error-report og ftp-report:

68 (defclass ftp-error-report (ftp-report error-report) ())

Når jeg oppretter en instans av denne klassen, og så kjører log-report, vil metodene for klassene som arves bli kombinert:

71 (log-report (make-instance 'ftp-error-report
72          :url "ftp://foobar.com/logs/my.log"))

Dette gir altså følgende output:

Fetching ftp://foobar.com/logs/my.log
Errors:
20120125180132112: Some error occurred
20120125180133056: Some other error...
Deleting ftp://foobar.com/logs/my.log
Deleting local copy as well

Et UML-diagram over denne modellen, om den hadde vært gjort i et mere klassisk OO-språk, ville sett ut som dette:

multippelarv

Konklusjon

Jeg har altså brukt multippel arv til å gjøre Template Method mindre rigid enn løsningen jeg kom opp med i C# i del 1; nå kan jeg kombinere de ulike klassene på ulike måter i stedet for å ha en fastlåst arverekkefølge. Dette gjør denne løsningen mer utvidbar.

Jeg har ikke jobbet mye med objektorientering i Common Lisp, men synes systemet med de generiske funksjonene som ikke er knyttet direkte til klassene er ganske elegant. Det er kanskje ikke lett å se hvor fleksibelt dette er uten å forsøke litt selv, spesielt ikke om man er vandt til typisk klasse-basert objektoerientering fra språk som C#, Java eller C++, men det finnes nok av folk som skryter hemningsløst av Common Lisp’s objektsystem. Dette er noe jeg må eksperimentere mer med.

Ønsker du en innføring i Common Lisp + objekter kan du ta en titt på Practical Common Lisp (online bok), kapittel 16 og 17.

I løpet av denne serien har du sett at et objektoerientert designpattern kan ha svært ulike implementasjoner. Et designpattern er ikke noe man kan pugge, og bare bruke igjen og igjen på samme måte. Man må tilpasse det omstendighetene, og hele tiden være klar over hvilke begrensninger det har. Jeg håper koden min har gitt deg noen ideer, og at du vil eksperimentere videre med hvordan du løser lignende problemer.

Template Method Intermesso

Wednesday, February 1st, 2012
2 kommentarer

tm_intermessoDet blir et kort opphold i den oppsatte sendeplanen for å gjøre plass til en litt grundigere forklaring av hva det er vi snakker om her.

Da jeg presenterte min “funksjonelle” løsning i del 2 av Template Method-serien min dukket det opp et spørsmål: Er ikke dette egentlig Strategy Pattern? Jeg svarte at det fortsatt var Template Method, men at det nå lå bedre til rette også for ulike strategier.

Lars-Petter fulgte opp på mail med å si at han mener “..forskjellen på TM og Strategy nettopp er om du bruker arv eller ikke. Det er i hvertfall det jeg kan lese ut av mine bøker, GoF inkudert.”

Jeg setter stor pris på spørsmålet, og vil forsøke å forklare mitt ståsted…

Forskjellen på Template Method og Strategy

Strategy Pattern og Template Method Pattern er på mange måter ganske like, men målet med dem er forskjellig. Strategy brukes for å tillate ulike implementasjoner av en algoritme eller operasjon, som så kan velges dynamisk i runtime. Målet med Template Method er ikke å tillate adferd å bli implementert på ulike måter, men å forsikre at en bestemt adferd/algoritme blir implementert på én måte.

Med andre ord: Mens Strategy fokuserer på å tillate variasjon, fokuserer Template Method på å forsikre konsistent adferd. (kilde)

Kanskje noen grafiske representasjoner kan hjelpe på å se det bedre. Her ser du en representasjon av en typisk Template Method Pattern implementasjon:

templatemethod_graphical

Den abstrakte klassen eksisterer for å forsikre én bestemt algoritme, men åpner for ulike implementasjoner av enkelte detaljer. De to konkrete klassene representerer ulike implementasjoner av disse detaljene. Sammenlign dette med følgende representasjon av strategy:

strategy_graphical

Strategy definerer et grensesnitt – et sett med steg om du vil. Disse settene er utbyttbare, og kan brukes om hverandre av ulike algoritmer. Målet er derimot ikke å sikre en bestemt algoritme i seg selv – Strategy Pattern begynner svært ofte som et rent interface uten noe funksjonalitet. Strategy dreier seg ofte mer om å gi forventninger om et bestemt resultat, men ikke hvordan det implementeres (tenk f.eks. ulike strategier for sortering).

Så hva var det da jeg lagde i del 2 av min Template Method-serie? Vel, se på denne representasjonen:

functionaltemplate_graphical

Jeg implementerte en konkret klasse som definerte en fast allgoritme, men hvor enkelte steg var utbyttbare. Motivasjonen min er altså den samme som for Template Method pattern. Forskjellen er at min implementasjon, siden den ikke baserer seg på arv, ikke krever at hvert av stegene kommer fra samme kilde.

Template Method kan derimot danne grunnlaget for ulike strategier, og det var det jeg snakket om da jeg hevdet at den klassiske implementasjonen var regid, mens min var mer fleksibel. Med min kunne jeg enklere kombinere ulike byggestener til nye strategier basert på templaten. Så bloggserien handlet på en måte om Strategy også.

Min løsning er altså Template Method Pattern “i ånden”. Spørsmålet er da om jeg får lov til å kalle det det når jeg ikke implementerer det slik som The Gang of Four gjorde!? For å svare på det må jeg utdype mitt syn på konseptet design patterns…

Hva er egentlig Design Patterns?

Mange ser på design patterns som implementasjons-oppskrifter. WikiPedia legger også vekt på dette. Der står det blant annet:

“Design patterns are composed of several sections. Of particular interest are the Structure, Participants, and Collaboration sections. These sections describe a design motif: a prototypical micro-architecture that developers copy and adapt to their particular designs to solve the recurrent problem described by the design pattern.”

Jeg ser derimot på design patterns først og fremts som problembeskrivelser. De delene av patterns-beskrivelsen jeg legger mest vekt på er Intent, Motivation (Forces) og Consequences. Som regel (ok da, omtrent ALLTID) beskrives mønstrene i kontekst av objektorienterte språk, men motivasjonen eller utfordringene bak dem er ikke begrenset til det domenet, og lærdommen bak dem strekker seg lengre – selv om både dynamiske språk og funksjonelle språk gjør mange av problemene trivielle i forhold til hvordan de må løses i språk som C++ eller Java.

Design Patters kan altså (ifølge meg) diskuteres uavhengig av implementasjonsteknikk – løsningene er (kun) eksempler, og må tilpasses. Mønstrene er kommunikasjonsredskaper som fungerer på tvers av programmeringsparadigmene. De oppfattes i alle fall slik av meg, som startet med statisk objektorientering, for så utvide horisonten flere år senere.

Jeg finner flere som støtter mitt syn på dette. Budskapet i dette foredraget om “funksjonelle design patterns” som et eksempel – presentert av PhD in Computer Science Aino Corry, som har gjort en Master Thesis om Design Patterns – er i tråd med min oppfatning.

Konklusjon

Jeg oppfatter altså spørsmålet som er stilt som at man henger seg for mye opp i eksempelimplementasjonen i beskrivelsene av  design patterns, og at man kanskje går glipp av det som jeg oppfatter som det viktigste; problemet som skal løses, og den generelle og mer eller mindre universelle strategien for å løse det.

Eksemplene er viktige for dem som bruker et “begrenset” språk; mange av mønstrene fra Gang of Four fokuserer på C++, og hvordan man best lager fleksibel kode der – mens de vil se ganske anderledes ut i for eksempel Ruby. Det er til eksempel ingen vits å basere Strategy Pattern på et interface når man har duck typing.

Jeg føler jeg allerede har lært mye av å holde patterns-foredraget mitt, skrive denne bloggserien, og svare på dette spørsmålet. Om du ser noen hull eller selvmotsigelser i det jeg sier nå er jeg veldig interessert i å høre hva du har å si, for det betyr bare at jeg får en mulighet til å lære enda mer!

Ward Cunningham er en smart fyr, og jeg vil avslutte med å sitere noe han har skrevet på The Portland Pattern Repository:

“Patterns link together in the mind so that one pattern leads to another and another until familiar problems are solved. That is, patterns form languages, not unlike natural languages, within which the human mind can assemble correct and infinitely varied statements from a small number of elements.”

PS: Vil du lese et par interessante artikler om patterns kan jeg anbefale Steve Yegge’s Singleton Considered Stupid fra 2004 (fra hans tid som utvikler i Amazon) og Code’s Worst Enemy fra 2007 (etter at han begynte å kode for Google). Steve provoserer alltid, men har uansett mye fornuftig å si i refleksjonene sine.

Template Method del 3: Bare funksjoner

Tuesday, January 31st, 2012
Ingen kommentarer

tp_del3I del 1 og del 2 har du sett meg implementere Template Method pattern i C# – først i en tradisjonell, objektorientert variant, og så i en mere fleksibel variant inspirert av funksjonell programmering. Nå er det på tide å se hvordan det samme kan gjøre kun med funksjoner. Det er på tide å finne frem F#.

Først trenger vi selve templaten, eller algoritme-skjelettet om du vil. processLog er en funksjon som har fire andre funksjoner som parametre, og bruker disse til å prosessere loggfilen. En slik funksjon kalles en høyereordens funksjon.

10 let processLog init read processLine cleanup =
11     init()
12     for line in read() do
13         processLine line
14     cleanup()

Deretter oppretter jeg et par funksjoner for å finne og rapportere errors i loggfiler:

22 let errorInit() = printfn "Errors:"
23 
24 let errorLineProcessor line =
25     let m = Regex("^(\\d{17})\\s(\\w+)\\s(.+)$").Match(line)
26     if m.Success then
27         if m.Groups.Item(2).Value = "ERROR" then
28             printfn "%s: %s"
29                 (m.Groups.Item(1).Value)
30                 (m.Groups.Item(3).Value)

Det neste jeg trenger er FTP-stegene. Nedenfor oppretter jeg en funksjon som tar som innput en URL og returnerer to funksjoner – en for Initialize-steget i algoritmen og en for Cleanup-steget. Disse to funksjonene er lexical closures, fordi de har tilgang til URL-variabelen (mer om dette mange andre steder i bloggen).

33 (* Evaluates to a tuple of two closures *)
34 let makeFtpSteps url =
35     let setup = fun () -> printfn "Fetching log file from %s" url
36     let teardown = fun () ->
37         printfn "Archiving local log copy..."
38         printfn "Deleting log file %s" url
39     (setup, teardown)

Så kan jeg bruke funksjone jeg nettopp laget til å opprette de to closure’ene:

42 let (ftpFetch, ftpCleanup) =
43     makeFtpSteps "ftp://foobar.com/logs/my.log"

Å komponere funksjoner..

Jeg har nå to forskjellige funksjoner som skal kalles i Initialize-steget i templaten: ftpFetch og errorInit. I del 1 løste jeg dette ved at FTP-initialiseringsmetoden kalte baseklassens Initialize. I del 2 løste jeg det ved å ha en builder-klasse som kunne kombinere flere Action-delegater. Nå befinner jeg meg derimot i et funksjonelt språk, og da er det ingen sak å slå sammen to funksjoner til én:

48 (* Using forward composition operator to compose two functions *)
49 let errorInitWithFtpFetch = ftpFetch >> errorInit

errorInitWithFtpFetch er nå en ny funksjon som først evaluerer ftpFetch og deretter evaluerer errorInit. Om ftpFetch hadde hatt parametre ville den nye funksjonen også hatt det. Om ftpFetch hadde returnert noe, ville dette blitt sendt inn som argumenter til errorInit. Og om errorInit hadde hatt en returverdi så hadde dette vært returverdien til den nye metoden.

På tide å teste programmet

Da gjenstår det bare å mocke lesing av fil:

55 (* Faking it as usual... *)
56 let read() = [
57     "20120125180000000 DEBUG Tick!";
58     "20120125180100000 DEBUG Tick!";
59     "20120125180132112 ERROR Some error occurred";
60     "20120125180133056 ERROR Some other error...";
61     "20120125180200000 DEBUG Tick!"]

… og å kjøre selve programmet ved å kalle processLog-funksjonen med de riktige argumentene:

64 processLog
65     errorInitWithFtpFetch
66     read
67     errorLineProcessor
68     ftpCleanup

Output er identisk med løsningene fra del 1 og del 2.

Konklusjon

Løsningen jeg har kommet opp med her er betydelig enklere enn det du har sett før – her har vi ingen klasser som pakker inn koden og bestemmer hva vi kan og ikke kan gjøre. Løsningen er ekstremt fleksibel, og vil la meg kombinere funksjoner akkurat slik jeg ønsker. Objektene har vist seg å være helt overflødige – dette fordi Template Method i objektorientert design egentlig er et forsøk på å gjøre det samme som higher-order funksjons allerede gjør mye bedre.

Det eneste det kan se ut som om jeg har mistet er det å ha en LogProcessor-instans/objekt som jeg kan sende rundt og eksekvere når jeg måtte ønske. Men det løser vi selvsagt også lett. Å pakke inn et funksjonskall i en ny funksjon uten parametre slik som jeg gjør her kalles thunking:

71 (* Creating a thunk *)
72 let logProcessor() =
73     processLog
74         errorInitWithFtpFetch
75         read
76         errorLineProcessor
77         ftpCleanup
78 
79 (* Evaluating the thunk *)
80 logProcessor()

logProcessor er nå i prinsippet et objekt.

I del 4 vil jeg avslutte serien om Template Method ved å se på hvordan jeg kan bruke multippel arv til å gjøre en objektorientert implementasjon like fleksibel som som den løsningen du nå fikk se.

Template Method del 2: På vei mot funksjonell programmering

Monday, January 30th, 2012
4 kommentarer

tp_del2I del 1 så du et ganske typisk, objektorientert design som kalles Template Method. I denne oppfølgeren vil jeg forsøke å gjøre designet mer fleksibelt uten å miste det jeg ønsket å oppnå – nemlig å definere skjelettet til algoritmen kun én gang.

Om du ikke har lest gjennom del 1 allerede bør du gjøre det først..

Fra abstrakt til konkret

Den første endringen jeg gjør er å endre LogProcessor fra å være en abstrakt klasse til å bli en konkret klasse jeg kan opprette instanser av. C# har noen delegat-typer som heter Action og Func, og jeg bytter du de abstrakte metodene i LogProcessor med private felt av tilsvarende delegattype. Til slutt legger jeg til en konstruktør som lar meg opprette LogProcessor med alle de manglende bitene til templaten:

 10 public class LogProcessor
 11 {
 12     public void Execute()
 13     {
 14         Initialize();
 15         IEnumerable<string> log = ReadLog();
 16         foreach (var line in log)
 17             ProcessLine(line);
 18         Cleanup();
 19     }
 20 
 21     private readonly Action Initialize;
 22     private readonly Func<IEnumerable<string>> ReadLog;
 23     private readonly Action<string> ProcessLine;
 24     private readonly Action Cleanup;
 25 
 26     public LogProcessor(
 27         Action initializer,
 28         Func<IEnumerable<string>> logReader,
 29         Action<string> lineProcessor,
 30         Action cleanuper)
 31     {
 32         Initialize = initializer;
 33         ReadLog = logReader;
 34         ProcessLine = lineProcessor;
 35         Cleanup = cleanuper;
 36     }
 37 }

Builder Pattern

For å gjøre det enklere å opprette en fornuftig LogProcessor har jeg så brukt et annet pattern som kalles Builder: LogProcessorBuilder er en klasse som steg-for-steg lar meg definere hvordan en LogProcessor skal fungere.

Studer Initialize-propertien nøye – den gav flere personer en aha-opplevelse under NNUG-foredraget mitt som koden er hentet fra. Her gjør jeg det mulig å sette propertien flere ganger, noe som vil føre til at Action-delegatene vil bli kombinert. Jeg burde gjort det samme for de andre propertiene også, men lar være for å spare litt plass.

 39 // More pattern fun: Adding a Builder
 40 public class LogProcessorBuilder
 41 {
 42     private Action _Initialize;
 43     public Action Initialize
 44     {
 45         get
 46         {
 47             return _Initialize;
 48         }
 49         set
 50         {
 51             if (_Initialize != null)
 52             {
 53                 var temp = _Initialize;
 54                 _Initialize = () =>
 55                 {
 56                     value.Invoke();
 57                     temp.Invoke();
 58                 };
 59             }
 60             else
 61                 _Initialize = value;
 62         }
 63     }
 64 
 65     public Func<IEnumerable<string>> ReadLog { get; set; }
 66     public Action<string> ProcessLine { get; set; }
 67     public Action Cleanup { get; set; }
 68 
 69     public LogProcessor GetProcessor()
 70     {
 71         if (   Initialize  == null
 72             || ReadLog     == null
 73             || ProcessLine == null
 74             || Cleanup     == null)
 75             throw new Exception("Some step has not been defined!");
 76 
 77         return new LogProcessor(
 78             initializer: Initialize,
 79             logReader: ReadLog,
 80             lineProcessor: ProcessLine,
 81             cleanuper: Cleanup);
 82     }
 83 }

Utfylling av templaten

Selve programmet nedenfor består av tre metoder. Den første tar en LogProcessorBuilder og legger til funksjonaliteten for å rapportere errors fra loggfilen. Den neste legger til funksjonaliteten for FTP-overføringene. Selve Main-metoden oppretter en builder, kaller de to foregående metodene for å klargjøre templaten, oppretter LogProcessor-instansen, og utfører.

 85 class Program
 86 {
 87     static void SetErrorReporting(LogProcessorBuilder processor)
 88     {
 89         processor.Initialize = () => Console.WriteLine("Errors:");
 90 
 91         processor.ProcessLine = line =>
 92         {
 93             var regex = new Regex("^(\\d{17})\\s(\\w+)\\s(.+)$");
 94             var match = regex.Match(line);
 95             if (match.Success && match.Groups[2].Value == "ERROR")
 96             {
 97                 Console.WriteLine("{0}: {1}",
 98                     match.Groups[1].Value,
 99                     match.Groups[3].Value);
100             }
101         };
102     }
103 
104     static void AddFtpReportingSteps(LogProcessorBuilder processor,
105                                         string url)
106     {
107         processor.Initialize = () =>
108         {
109             Console.WriteLine("Fetching log file from {0}", url);
110         };
111 
112         processor.Cleanup = () =>
113         {
114             Console.WriteLine("Archiving local log copy...");
115             Console.WriteLine("Deleting log file on {0}", url);
116         };
117     }
118 
119     static void Main(string[] args)
120     {
121         var builder = new LogProcessorBuilder();
122         SetErrorReporting(builder);
123         AddFtpReportingSteps(builder, "ftp://foobar.com/logs/my.log");
124 
125         // faking out log reading
126         builder.ReadLog = () => new[] {
127                 "20120125180000000 DEBUG Tick!",
128                 "20120125180100000 DEBUG Tick!",
129                 "20120125180132112 ERROR Some error occurred",
130                 "20120125180133056 ERROR Some other error...",
131                 "20120125180200000 DEBUG Tick!",
132             };
133 
134         builder.GetProcessor().Execute();
135 
136         Console.ReadLine();
137     }
138 }

Konklusjon for del 2

Dette er fortsatt Template Method pattern, men designet er ikke lenger så rigid. Bruk av Action og Func lar meg legge til funksjonalitet i LogProcessor uten å måtte arve eller definere konkrete typer for denne funksjonaliteten. Jeg sender i prinsippet funksjoner til LogProcessor, som den så kan bruke når den skal prosessere loggfilen. Dermed kan jeg enkelt definere nye LogProcessor-instanser med kun små endringer i funksjonalitet – blande stegene fritt, uten at antall klasser i løsningen eksploderer. Og designet er fortsatt ganske rent, ryddig og forståelig.

Det jeg har gjort er å tenke som en funksjonell programmerer, og i del 3 vil jeg ta steget fullt ut og implementere en løsning i F# som kun baserer seg på funksjoner.

Template Method del 1: Statisk OOP

Monday, January 30th, 2012
3 kommentarer

tp_del1I foredraget mitt på NNUG Bergen i januar tok jeg for meg noen utvalgte design patterns, viste noen objektorienterte eksempelimplementasjoner i C#, for så å sammenligne dette med tilsvarende løsninger i F# hvor jeg kun brukte funksjoner. Dette er første blogpost i en serie på fire hvor jeg vil gå gjennom ett av mønstrene jeg brukte – nemlig Template Method design pattern.

(Koden er noe endret i forhold til det som ble vist i foredraget.)

I denne første blogposten vil jeg vise en typisk løsning implementert i C# – designet er slik du ofte finner det i statisk typede språk. I del 2 vil jeg gjøre visse endringer i C#-løsningen for å vise hvordan jeg kan gjøre designet mere fleksibelt ved å bevege meg mot en funksjonell tankegang.

I del 3 vil du få se min F#-løsning. Da vil du forhåpentligvis se hvor mye enklere og mere fleksibel koden blir når man dropper de tyngste abstraksjonene og bare bruker funksjoner. I den siste delen vil jeg vise en implementasjon i Common Lisps fleksible objektsystem, hvor jeg blant annet vil utnytte multippel arv.

Poenget her er altså å studere ulike måter å løse et problem rent designmessig – og hvordan ulike programmeringsspråk tilbyr ulike hjelpemidler.

En “klassisk” løsning

Problemet jeg skal modellere ved hjelp av Template Method pattern dreier seg om prosessering av loggfiler. Template Method er en mønster hvor man definerer skjelettet til en algoritme, men gir andre (sub-klasser) mulighet til å definere eller endre enkelte av stegene i algoritmen. Jeg har identifisert at jeg alltid prosesserer loggfiler omtrent på samme måte, men at enkelte av stegene endrer seg fra gang til gang. Template Method er altså en god kandidat å bruke her.

Nedenfor ser du koden som definerer selve templaten. Det er en abstrakt klasse, hvor definisjonen av detaljene i hvert steg av algoritmen er overlatt til en fremtidig, konkret implementasjon:

 10 public abstract class LogProcessor
 11 {
 12     public void Execute()
 13     {
 14         Initialize();
 15         IEnumerable<string> log = ReadLog();
 16         foreach (var line in log)
 17             ProcessLine(line);
 18         Cleanup();
 19     }
 20 
 21     protected abstract void Initialize();
 22     protected abstract IEnumerable<string> ReadLog();
 23     protected abstract void ProcessLine(string line);
 24     protected abstract void Cleanup();
 25 }

Hvis jeg så har behov for å f.eks. skrive ut alle linjene i en loggfil som inneholder feilmeldinger så kan jeg lage en ErrorReporter som arver fra LogProcessor-templaten min:

 30 public class ErrorReporter : LogProcessor
 31 {
 32     protected override void Initialize()
 33     {
 34         Console.WriteLine("Errors:"); // just a header for the report
 35     }
 36 
 37     protected override IEnumerable<string> ReadLog()
 38     {
 39         // Simulating reading a file:
 40         yield return "20120125180000000 DEBUG Tick!";
 41         yield return "20120125180100000 DEBUG Tick!";
 42         yield return "20120125180132112 ERROR Some error occurred";
 43         yield return "20120125180133056 ERROR Some other error...";
 44         yield return "20120125180200000 DEBUG Tick!";
 45     }
 46 
 47     protected override void ProcessLine(string line)
 48     {
 49         var regex = new Regex("^(\\d{17})\\s(\\w+)\\s(.+)$");
 50         var match = regex.Match(line);
 51         if (match.Success && match.Groups[2].Value == "ERROR")
 52         {
 53             Console.WriteLine("{0}: {1}",
 54                 match.Groups[1].Value,
 55                 match.Groups[3].Value);
 56         }
 57     }
 58 
 59     protected override void Cleanup() // No cleanup needed
 60     {
 61     }
 62 }

Som du ser gjør jeg eksempelet litt enklere ved å lure meg unna selve lesingen av loggfilen.

ErrorReport-klassen kan brukes direkte, men i tillegg ønsker jeg at programmet mitt skal hente loggfilen fra en FTP-server når rapporten skal kjøres. Når jeg er ferdig skal filen arkiveres, og filen på FTP-serveren skal slettes. Derfor lager jeg et nytt nivå – en klasse som arver fra ErrorReport, som re-definerer Initialize- og Cleanup-stegene:

 66 public class FtpErrorReporter : ErrorReporter
 67 {
 68     private readonly string _url;
 69     public FtpErrorReporter(string url)
 70     {
 71         _url = url;
 72     }
 73 
 74     protected override void Initialize()
 75     {
 76         Console.WriteLine("Fetching log file from {0}", _url);
 77         base.Initialize();
 78     }
 79 
 80     protected override void Cleanup()
 81     {
 82         base.Cleanup();
 83         Console.WriteLine("Archiving local log copy...");
 84         Console.WriteLine("Deleting log file on {0}", _url);
 85     }
 86 }

Designet jeg har laget ser altså ut som dette:

template_design1

Med følgende program kan jeg teste ut koden min:

 90 public class Program
 91 {
 92     public static void Main()
 93     {
 94         LogProcessor errorReporter =
 95             new FtpErrorReporter("ftp://foobar.com/logs/my.log");
 96 
 97         errorReporter.Execute();
 98     }
 99 }

Og output ser slik ut:

Fetching log file from ftp://foobar.com/logs/my.log
Errors:
20120125180132112: Some error occurred
20120125180133056: Some other error...
Archiving local log copy...
Deleting log file on ftp://foobar.com/logs/my.log

En foreløpig konklusjon

Designet jeg har valgt løser oppgaven på en tilsynelatende grei måte. De tre klassene har fått hvert sitt tydelig definerte ansvarsområde: LogProcessor definerer en generell algoritme som kan gjenbrukes. ErrorReporter definerer hvordan jeg finner feilene i loggfilen og skriver dem ut. FtpErrorReporter håndterer henting og sletting av loggfiler over FTP.

Dette er altså del 1 i min behandling av Template Method, og er som en introduksjon å regne. Men det er allerede nå verdt å legge merke til at at template method har gjort designet mitt ganske så regid. Hva må jeg f.eks. gjøre om jeg vil rapportere på andre ting enn errors, men fortsatt hente filene over FTP? Skal jeg følge designet må jeg opprette to nye klasser, hvor den ene stort sett vil være en kopi av FtpErrorReporter. Det er ikke bra! Jeg står heller ikke fritt til å gjenbruke bare deler av enten ErrorReporter eller FtpErrorReporter.

I del 2 vil du få se hva jeg kan gjøre med dette.

Siste kommentarer

Torbjørn
PS: Takk til Børge Hansen, som delte SCARF-modellen med meg!...
Børge Hansen
Denne likte jeg veldig godt. Du skriver godt og har gode betraktninger  Keep it up – flere trenger å tørre å lære mer om ledelse – du l...
Tormod
Er egentlig ikke overrasket. F# sin fortè er programmererens produktivitet/kvalitet og anledning til parallell kjøring. Men kjøremotoren har ...
Stian
Ville også prøvd med et større problem (x100 eller x1000 f.eks). Når man snakker så små brøkdeler av et sekund som her så kan tiden for en ell...
Torbjørn
Har ikke sjekket - tar en titt i morgen hvis tid :)...
Einar W. Høst
Mhp tco: hva sier ILSpy?...
Torbjørn
Har ikke sett noe på PSeq før, men kjenner til den typen funksjoner fra blant annet Clojure. Og problemet med slike funksjoner i sammenhenger som de...
Håvard
Veldig bra sammenligning! Har du sett på ytelsen av PSeq.* fra powerpakken? Tipper den vil gi performancehit på små mengder, men kan kanskje resul...
Torbjørn
Jeg kom på en demonstrasjon-variant til jeg burde inkludere, nemlig bruk av list comprehension (en type computation expression (også kalt monads)). ...
Einar W. Høst
Interessant, det blir en trade-off mellom eleganse og fart på en måte. Den funksjonelle løsningen med vanlig filter er ren og pen, mens den imperat...
Creative Commons-lisens
Innholdet på denne bloggen er tilgjengelig under Creative Commons Navngivelse-Ikkekommersiell-DelPåSammeVilkår 3.0 Norge lisens.

Programmeringsbloggen
Kjempekjekt.com

© 2006-2013 Torbjørn Marø

Jeg har vært en profesjonell programmerer siden 1999, og dette er min blogg. Målet med bloggen er å stimulere meg selv og alle andre til kontinuerlig eksperimentering og læring.

Jeg forsøker å være allsidig, og programmerer blant annet i C#, Ruby, Erlang og Clojure.

Jeg praktiserer TDD og andre smidige utviklingspraksiser. Jeg er opptatt av kvalitet og ren kode.

Dette og ganske mye mer kan du lese om på denne bloggen!