OO-design/clean code

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..

Template Method del 4: Multippel arv

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

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

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

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

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.

"Rename method" kan være farlig

Denne historien fra virkeligheten forteller om en tilsynelatende uskyldig og ufarlig refakturering som førte til at en katastrofal feil ble deployet til produksjon. Jeg har anonymisert og forenklet koden for å beskytte den skyldige (som var undertegnede selv) og for å gjøre det enkelt å forstå hva problemet er.

Det hele begynte med en nokså sentral klasse som vi skal kalle SomeBaseClass. Den hadde blant annet en metode vi for anledningen kaller DoSomething.

1     class SomeBaseClass
2     {
3         public void DoSomething()
4         {
5         }
6     }

Etterhvert som systemet vokste ble det innført mange klasser som arvet fra SomeBaseClass. En av disse var SomeDescendant.

1     class SomeDescendant : SomeBaseClass
2     {
3         public void DoIt()
4         {
5             DoSomething();
6         }
7     }

Alt fungerte som det skulle i lange tider, og alle var glade. Men så en dag fant en utvikler ut at han ikke likte navnet på metoden DoSomething. Heldigvis hadde han CodeRush-plugin i Visual Studio, og var derfor ganske sikker på at han raskt og trykt kunne endre navnet på metoden med den automatisert refaktureringen rename.

Utvikleren bestemte seg for at DoIt var et mye bedre og mere beskrivende navn!

Uheldigvis fungerte refaktureringen som den skulle. Koden kompilerte, testene var grønne, og endringen ble med i neste release. Utvikleren husket ikke at han allerede hadde kalt en av metodene i SomeDescendant for det samme.

10     class SomeBaseClass
11     {
12         public void DoIt()
13         {
14         }
15     }
16 
17     class SomeDescendant : SomeBaseClass
18     {
19         public void DoIt()
20         {
21             DoIt();
22         }
23     }

Ser du problemet? DoIt i SomeDescendant er nå en evig rekursjon.

Blir den kalt vil den føre til et stack overflow exception som det ikke er mulig å fange. Eller hvis JIT-kompilatoren har klart å optimalisere rekursjonen (TCO), noe jeg faktisk tror var tilfellet her, så vil programmet bare stå og spinne inn i uendeligheten eller til serveren brenner opp.

Bug retrospective

Burde denne feilen ha blitt oppdaget før release? Var det tankeløst å gjennomføre en sånn refakturering? Burde C# tillate denne koden i det hele tatt?

Jeg har alltid sett på rename som en veldig trygg refakturering i et IDE som Visual Studio og et statisk typet språk som C#, og de fleste av oss gjør det sikkert hele tiden. Navngivning er veldig viktig, og føler man at et navn er feil så bør man endre det.

Endringen førte derimot til at kompilatoren genererte en warning. Jeg har forsøkt å bruke “treat warnings as errors” opsjonen, men i praksis er det så mange “false positives” i større prosjekter at det å vedlikeholde alle unntakene blir en urimelig oppgave. Jeg tar manuelle gjennomganger av warnings fra tid til annen, men det er ikke rart at ting faller under radaren.

Vi kunne ha innført en kodestandard fra starten hvor vi alltid kalte metoder i baseklassen eksplisitt – altså base.DoIt() – men det ville også vært vanskelig å overholde.

Den eneste feilen jeg er villig til å innrømme var å ha for dårlig test coverage på koden! Hadde SomeDescendant.DoIt() blitt kalt i en test, ville problemet ha blitt oppdaget. Grunnen til at dette ikke ble gjort i tilfellet her var at DoIt i utgangspunktet var logikkløs, og at funksjonaliteten var blitt testet andre steder. Det føltes veldig trygt å ikke teste. Og det er alltid i slike tilfeller man angrer bittert i etterkant!

Det jeg sitter igjen med er likevel spørsmålet om C# burde tillate dette i det hele tatt. Som warningen sier: ‘SomeDescendant.DoIt()’ hides inherited member ‘SomeBaseClass.DoIt()’. Use the new keyword if hiding was intended. Hvorfor kunne ikke dette vært en error i stedet? Finnes det reelle tilfeller hvor “hiding” uten “new” faktisk kan være nyttig? En error ville gjort at jeg hadde oppdaget problemet og fikset det uten problemer.

Message Passing Style

Hvor kommer objektorientert programmering fra? Hvilke problemer løser det? Kan man gjøre det samme i programmeringsspråk uten direkte støtte for objekter? I denne blogposten snakker jeg litt om røttene til OO, og demonstrerer – vha. JavaScript – hvordan man gjør OOP med bare funksjoner. Gjør deg klar for litt back to basics!

Og historien begynner med et spørsmål…

Hva er en closure?

lexicalclosureJeg har stilt og forsøkt å svare på dette tidligere i blogposten Lispy C#. En “lexical closure” er en funksjon pluss en referanse til de variablene som brukes i funksjonen men som ikke er definert der – såkalte frie variabler. Selv om variablene faller ut av scope, vil de fortsatt være tilgjengelig i funkjonen, fordi closuren holder referanser til dem.

Konseptet ble utviklet på 60-tallet, og ble angivelig for første gang implementert i programmeringsspråket Scheme. Imperative språk har tradisjonelt ikke hatt støtte for closures ettersom disse språkene hverken støttet ikke-lokale variabler eller høyererangs funksjoner.

Neste spørsmål…

Hva er et objekt?

objectEt objekt er en datastruktur som består av datafelter og metoder. Vi snakker ofte om encapsulation, som i praksis betyr at metodene fungerer som mellommenn mellom datafeltene og dem som ønsker å benytte dem. Ingen får røre objektets tilstand direkte, kun via metodene.

Ideen om objekter startet allerede på 50 og 60-tallet. Det første programmeringsspråket designet for å organisere koden på denne måten ble utviklet i Norge, og het Simula 67. Men det var supergeniet Alan Key som kom opp med begrepet “objektorientert programmering”, basert på sin forståelse av Lisp (hvor man brukte begrepet “objekt” om både tall, tekststrenger, symboler osv.).

På 70-tallet designet Alan Key språket Smalltalk, som kanskje har vært enda viktigere for utviklingen av dagens objektorientering enn det Simula var. Java, Objective-C, Ruby og mange andre språk hentet mye inspirasjon fra Smalltalks objektmodell.

Simula
..var en utvidelse med objektorienterte egenskaper til sterkt typet Allgol 60. Objektene eksisterte side om side med tradisjonelle datatyper som tall og strenger.
vs. Smalltalk
..var basert på det dynamiske, løst typede Lisp, men erstattet funksjoner og s-expressions med metoder og objekter. I Smalltalk er all data representert av objekter.

(Og så kom 80-tallet, og C++ tok over verden!)

Ser du på de to figurene mine er det slående hvor mye closures og objekter har til felles. Og det leder meg til det tredje spørsmålet…

Hva behøver man for å lage et objekt?

Svaret er opplagt. Man trenger funksjoner som har mulighet til å danne closures. Siden closures skjuler tilstand (gjennom frie variabler) kan de brukes til å lage objekter.

En teknikk for å gjøre dette presenteres blant annet i boken Structure and Interpretation of Computer Programs (Informatikk-pensum på MIT), og kalles  Message Passing Style. Det vi snakker om her nå er helt grunnleggende objektorientering, en teknikk som brukes til å implementere objektsystemer i programmeringsspråk – eller som lar deg lage objekter i språk hvor du ikke har dette out of the box.

I Smalltalk (og språk som er inspirert av Smalltalk) sier vi at det å kalle en metode på et objekt er det samme som å sende et objekt en melding. Er du kjent med Ruby, vet du for eksempel sikkert at de følgende to linjene gjør akkurat det samme:

1 an_array.length
2 an_array.send('length');

Message Passing Style i JavaScript

Så hvordan kan man implementere message passing uten å bruke “vanlige” objekter? Her følger et eksempel i JavaScript.

Jeg ønsker å kunne opprette objekter som skal representere bankkontoer. Kontoene skal ha en balanse, og skal kunne ta i mot meldinger om å sette penger inn på kontoen og å trekke penger ut av kontoen. Jeg trenger da en funksjon som fungerer som en konstruktør av kontoer – kaller jeg denne får jeg opprettet og returnert en ny konto. Her følger koden…

10   var createAccount = function(balance){
11     function deposit(amount){
12       return balance += amount;
13     };
14     function withdraw(amount){
15       return balance -= amount;      
16     };  
17     return function(msg){
18       switch(msg){ 
19         case "deposit":
20           return deposit(arguments[1]);
21         case "withdraw":
22           return withdraw(arguments[1]);
23         default:
24           throw "Unknown message " + msg;
25       };
26     };
27   };

deposit og withdraw er to funksjoner som defineres inne i createAccount, og de vil ikke være tilgjengelige på utsiden. Fordi de benytter seg av variabelen balance danner hver av dem en closure.

Den tredje funksjonen – som returneres fra createAccount – kalles en dispatch-funksjon. Denne danner også en closure, fordi den refererer de to funksjonene deposit og withdraw. Jeg sa at createAccount skulle returnere en konto, og i message passing style vil det altså si en closure som inneholder en dispatch-funksjon.

For å opprette kontoer kan jeg nå skrive:

29   var account1 = createAccount(100);
30   var account2 = createAccount(100);

Og for å legge til eller ta ut penger sender jeg de respektive meldingene til “objektet” sammen med beløpet det gjelder:

32   account1("deposit", 10); // returns 110
33   account1("withdraw", 5); // returns 105
34   account2("withdraw", 5); // returns 95

Legg merke til at det ikke finnes noen annen måte å modifisere balance på enn å kalle dispatch-metoden. Encapsulation er oppnådd!

Oppsummering

Det er egentlig ganske simple greier dette her, men likevel synes jeg det er fasinerende. Det hadde nå vært fristende å diskutere hva dette har med actor model og Erlang å gjøre. Jeg kunne også ha fortsatt med å snakke om forskjellen mellom Turing-modellen, språk basert på Von Neumann-arkitekturen, og Alonzo Church‘s Lambda Calculus – og hvordan det eneste man egentlig trenger for å lage et komplett programmeringsspråk er muligheten for å opprette og eksekvere lambdaer.., men det er nok på tide å gi seg mens leken er god.

Så det du skal huske fra denne blogposten er: Closures er funksjoner pluss referanser til frie variabler. Du kan lage objekter i  språk som ikke i utgangspunktet støtter dem, såsant du har førsterangs funksjoner som kan danne closures. I denne sammenhengen er en dispatch-funksjon en clojure som tar imot en melding og eksekverer kode for å aksessere tilstand som ikke kan nås på andre måter. Dispatch-funksjonen er i prinsippet et objekt. Dette kalles Message Passing Style.

Førsterangs funksjoner i C#

I dag brukte jeg førsterangs funksjoner (first-class functions) i C# på en måte jeg følte det var verdt å skive en liten blogpost om. Jeg bruker lambda-uttrykk til mye rart hele tiden, men jeg kan faktisk ikke huske at jeg har brukt Funcs til å referere til metoder på objekter før.

Jeg skulle bruke et tredjeparts API hvor jeg skulle kalle en av to ulike metoder avhengig av en boolsk tilstandsvariabel. Metodene hadde mange parametre, men signaturen var lik på begge. Her er hvordan koden først så ut (navnene er endret for å “beskytte de skyldige”):

10 private CustomerInfo CreateOrRemove(FooWebService client,
11                                     int customerId,
12                                     string phoneNumber,
13                                     bool create)
14 {
15   if (create)
16     return client.CreateFoo("some constant",
17                             customerId,
18                             phoneNumber,
19                             string.Empty,
20                             this.someField,
21                             string.Empty,
22                             this.someOtherField);
23   else
24     return client.RemoveFoo("some constant",
25                             customerId,
26                             phoneNumber,
27                             string.Empty,
28                             this.someField,
29                             string.Empty,
30                             this.someOtherField);
31 }

Det er det at metodene tar 7 parametre som gjør koden stygg. En bedre løsning hadde selvfølgelig vært å bruke ett objekt som inneholdt de syv variablene som argument til metodene – men med tredjeparts API’er kan man ikke alltid få det som man vil.

Men jeg hater denne typen kodeduplisering. I gamledager hadde jeg latt koden forbli som dette, men etter å ha jobbet mye i funksjonelle språk i det siste er det derimot en simpel sak å fikse på problemet. Jeg var ikke helt sikker på om C# sin Func støttet det jeg ville, men det funket heldigvis helt fint (pun intended):

33 private CustomerInfo CreateOrRemove(FooWebService client,
34                                     int customerId,
35                                     string phoneNumber,
36                                     bool create)
37 {
38   Func<string, int?, string, string, string, string, string, CustomerInfo> operation;
39 
40   if (create)
41     operation = client.CreateFoo;
42   else
43     operation = client.RemoveFoo;
44 
45   return operation("some constant",
46                    customerId,
47                    phoneNumber,
48                    string.Empty,
49                    this.someField,
50                    string.Empty,
51                    this.someOtherField);
52 }

Det er ikke akkurat vakkert å spesifisere funksjonssignaturen på denne måten, men det gjør jobben – dupliseringen er borte, og koden kommuniserer faktisk litt bedre hva den gjør (etter min mening).

Trekker jeg ut valget av create/remove i en egen metode blir det enda bedre:

54 private static Func<string, int?, string, string,
55         string, string, string, CustomerInfo>
56         GetOperation(FooWebService client, bool create)
57 {
58   if (create)
59     return client.CreateFoo;
60   else
61     return client.RemoveFoo;
62 }
63 
64 private CustomerInfo CreateOrRemove(FooWebService client,
65                                     int customerId,
66                                     string phoneNumber,
67                                     bool create)
68 {
69   return GetOperation(client, create)("some constant",
70                                       customerId,
71                                       phoneNumber,
72                                       string.Empty,
73                                       this.someField,
74                                       string.Empty,
75                                       this.someOtherField);
76 }

Så det var altså dagens mest spennende kode. Har du gjort noe spenstig med Funcs i det siste? Del dine erfaringer da vel!

Mitt første Io-objekt: Matrix

Io er et prototype-basert språk, på samme måte som JavaScript. Her har jeg laget en prototype for å definere en matrise. Den arver fra List og har fire funskjoner: en for å sette dimensjonen, en for å sette verdien for en bestemt koordinat, en for å hente ut igjen verdien, og til slutt en for å printe ut hele matrisen.

10 Matrix := List clone do(
11   dim := method(x, y,
12     for(i, 0, x-1,
13       self append(List clone)
14       self at(i) setSize(y)
15     )
16     return self
17   )
18   set := method(x, y, value,
19     self at(x) atPut(y, value)
20     return self
21   )
22   get := method(x, y,
23     self at(x) at(y)
24   )
25   print := method(
26     self foreach(x,
27       "[" print
28       x reduce(acc, y, acc .. ", " .. y) print
29       "]" println
30     )
31   )
32 )

Deretter lager jeg en metode for å gjøre det litt enklere å opprette nye matriser:

34 matrix := method(x, y, Matrix clone dim(x, y))

Og så bruker jeg det jeg har laget. Først oppretter jeg en ny matrise jeg kaller foo, og skriver den ut. Deretter setter jeg verdier for alle koordinatene, og skiver den ut på nytt. Til slutt skriver jeg ut en tekstreng hvor jeg henter ut verdien i rad 2, kolonne 1:

36 foo := matrix(3, 2)
37 foo println
38 
39 foo do(
40   set(0, 0, "Hello") set(0, 1, "World")
41   set(1, 0, "My")    set(1, 1, "name")
42   set(2, 0, "is")    set(2, 1, "Tobben")
43 )
44 foo println
45 
46 writeln("My name is " .. foo get(2, 1))

Og her er output:

C:\usr\local\bin>io_static.exe c:\temp\hello.io
[nil, nil]
[nil, nil]
[nil, nil]

[Hello, World]
[My, name]
[is, Tobben]

My name is Tobben

Se også forrige artikkel: Hello, world fra Io.

Min tredje state machine DSL

I juni 2009 blogget jeg hvordan man kan implementere en generisk tilstandsmaskin i C#, inspirert av Robert C. Martin’s overgangstabeller fra boken Agile Principles, Patterns, and Practises in C#. Jeg designet også et flytende interface for å konfigurere tilstandsmaskiner – her er et tilbakeblikk på hvordan det så ut:

10   _stateMachine = new StateMachine<State, Event>(State.LOCKED);
11
12   _stateMachine.Configure()
13       .Given(State.LOCKED)
14           .When(Event.COIN)
15               .ThenSetState(State.UNLOCKED).AndRun(unlock)
16           .When(Event.PASS)
17               .ThenSetState(State.LOCKED).AndRun(alarm)
18       .Given(State.UNLOCKED)
19           .When(Event.COIN)
20               .ThenSetState(State.UNLOCKED).AndRun(thankYou)
21           .When(Event.PASS)
22               .ThenSetState(State.LOCKED).AndRun(lockAction);
23

Et halvt år senere presenterte jeg en implementasjon i Ruby, og i en separat blogpost implementerte jeg en forbedret DSL som så ut som dette:

10   @state_machine.transition :if_state_is => :locked
11     :when_event => :coin, :then_set_state => :unlocked do
12     @unlock_called = true
13   end
14   @state_machine.transition :if_state_is => :locked
15     :when_event => :pass, :then_set_state => :locked do
16     @alarm_called = true
17   end
18   @state_machine.transition :if_state_is => :unlocked
19     :when_event => :coin, :then_set_state => :unlocked do
20     @thank_you_called = true
21   end
22   @state_machine.transition :if_state_is => :unlocked
23     :when_event => :pass, :then_set_state => :locked do
24     @lock_action_called = true
25   end

Og nå som jeg har begynt å leke med Clojure er det på tide å se hva jeg kan få til med det språket. Clojure, og Lisp generelt, har nesten ingen begrensninger i forhold til hva man kan få til når man designer en DSL. Jeg har forsøkt å lage en variant som er mere lesbar enn C# og Ruby-variantene, men uten at jeg har gjort noe spesielt avansert.

10 (ns fsm.sample (:use fsm.core))
11
12 (def turnstile (statemachine :locked (      
13
14      Given state :locked, event :coin
15         set state :unlocked, (println ” > Unlocking, you are free to pass.”)
16
17      Given state :locked, event :pass
18         set state :locked, (println ” > You haven’t paid yet. Alarm!!!”)
19       
20      Given state :unlocked, event :coin
21         set state :unlocked, (println ” > Thank you for that extra coin!”)
22
23      Given state :unlocked, event :pass
24         set state :locked, (println ” > Have a nice day! Locking up.”))))

Som du forhåpentlig vis ser er reglene strukturert likt i de tre variantene, og inneholder den samme informasjonsmengden. Men Clojure-varianten skiller seg fra de andre gjennom langt mindre “syntaks”. Hvordan jeg valgte å strukturere setningene er ganske tilfeldig.

Her er et lite eksempel hvor jeg bruker tilstandsmaskinen i en REPL:

CropperCapture[90]

Så hva er det som gjemmer seg bak denne turnstile? Det kan nesten virke som om det er et objekt, men det er det ikke. Det er en funksjon, men denne funksjonen har også en tilstand – den husker hvilken state tilstandsmaskinen den representerer er i. Og da skjønner du kanskje at dette er en closure (les Lispy C# (og hva er en closure) om du ikke vet eller husker hva det er for noe).

statemachine i linje 12 i kodeeksempelet over er altså et kall til en funskjon som tar alle reglene og genererer en closure-funksjon basert på dem. Her følger den komplette funksjonen. Det er nok mye her som vil være vanskelig å forstå om du ikke kan språket, men jeg skal trekke frem noen “høydepunkter” etterpå.

10 (ns fsm.core)
11
12 (defn statemachine [initial-state dsl]
13   {:pre [(> (-> dsl count) 0)
14          (= (-> dsl count (mod 9)) 0)]}
15   (letfn
16     [
17      (new-transition [g e ns a]
18        { :given g, :event e, :new-state ns, :action a })
19
20      (find-transition [state event transitions]                          
21       (first (filter #(and (= (:given %) state)
22                            (= (:event %) event)) transitions)))
23    
24      (perform-transition [transition state]
25        {:pre [(not-empty transition)]}
26        (reset! state (:new-state transition))
27        (eval (:action transition)))]
28
29     (loop [transitions '(), spec dsl]
30       (if (seq spec)
31         (let [[_ _ state _ event _ _ new-state action & more] spec]
32           (recur (cons (new-transition state event new-state action)
33                        transitions)
34                  more))
35
36         (let [current-state (atom initial-state)]
37           (fn [event]
38               (perform-transition (find-transition @current-state
39                                                    event
40                                                    transitions) 
41                                   current-state)))))))

Noe av det første du ser her er Clojure’s støtte for Design by Contract (DbC), eller mer presist preconditions. Linje 13 og 14 sier noe om hva som må være tilfredstilt for at et kall til statemachine-funksjonen skal være gyldig. Linje 13 sier at dsl-argumentet skal inneholde en liste med mer enn ett element. Linje 14 følger opp med å si at antall elementer skal være en multippel av 9. Og hvorfor det? Jo, fordi en regel i syntaksen jeg definerte består av nøyaktig 9 ord/elementer.

For å øke lesbarheten på koden inneholder definisjonen av statemachine-funksjonen tre “interne” funksjoner (linje 17, 20 og 24). Den siste av disse har også en precondition, og den slo faktisk til i REPL-sesjonen du så tidligere. Da jeg sendte eventet :foobar klarte ikke tilstandsmaskinen å finne en overgang, og kastet et assert exception. Ved å bruke preconditions på denne måten slipper jeg å rote til resten av koden med unntakshåndtering.

statemachine-funksjonen starter med å eksekvere en loop i linje 29. Koden plukker de 9 neste uttrykkene fra dsl-spesifikasjonen, og looper sålenge det finnes flere regler. Overgangene samles i verdien transitions. I linje 31 bruker jeg noe som heter destructuring for å plukke ut det som er interessant fra uttrykkene i DSL-listen. Ønsker jeg å endre hvordan jeg skriver reglene er det denne linjen jeg vil måtte endre på.

Når det ikke er flere regler å prosessere opprettes det en verdi for maskinens tilstand (linje 36). Og det eneste som gjenstår da er å opprette og returnere en anonym funksjon (linje 37-41) som representerer tilstandsmaskinen. Denne funksjonen benytter seg av overgangstabellen (transitions) og tilstanden (current-state), og blir dermed en closure. Og ingen andre kan aksessere disse verdiene utenfra, noe som også er ganske kult.

Dette har bare vært en liten smakebit av hva som er mulig å få til av interne DSL’er i Clojure. Og jeg behøvde ikke engang ty til makroer for å få det til. Jeg håper det gav mersmak.


Torbjørn: La oss anta to ulike definisjoner av Template Method pattern - de to ytterpunkte...

Lars-Petter: Hei igjen. Siden du inviterer til å ta diskusjonen i bloggen, og har tatt deg t...

Torbjørn: Lars-Petter: Det er én måte å se det på. Det er absolutt fortsatt Template M...

Lars-Petter: Hei. Har du ikke i prinsippet her gått over fra Template Method (arv) til Strat...

Christian Abildsø: I alle fall i C#, så føles dette som kode som blir mer fleksibel men vanskelig...

Torbjørn: Hei Henrik, og takk for tilbudet. Ble oppmerksom på Rasberry Pi for under en uk...

Henrik Sandaker Palm: Ang. større hobby prosjekt. Du er som er en slik rakker på programmering har j...

Øivind Nilsen: Slutt å bruke mobilnummeret mitt som eksempel !...

Bjørn Einar Bjartnes: Jeg har også latt meg fascinere av Clojure, uten at jeg har kommet så veldig l...

Bjørn Einar Bjartnes: Sweet :) Jeg tror egentlig jeg liker det som det er, med musikk. Litt av utford...

 Hold deg oppdatert

Søk i bloggen

Ferske innlegg

  • Template Method del 4: Multippel arv
  • Template Method Intermesso
  • Template Method del 3: Bare funksjoner
  • Template Method del 2: På vei mot funksjonell programmering
  • Kategorier

  • .net ninja (37)
  • Bøker (17)
  • Diverse prosjekter (35)
  • DSL (10)
  • Erlang (10)
  • F# (5)
  • Hardware (1)
  • Jobb (77)
  • Julekalender (51)
  • kjempekjekt.com (23)
  • LISP/Clojure (33)
  • NNUG / community (60)
  • O/RM & databaser (10)
  • Off topic (116)
  • OO-design/clean code (30)
  • Podcasts (14)
  • Polyglot (77)
  • Ruby (27)
  • Silverlight / RIA (3)
  • Software/verktøy (20)
  • Softwareutvikling (20)
  • Testing / TDD (30)
  • the contiki strip (13)
  • User experience (3)
  • WCF (3)
  • Webutvikling (32)
  • WPF (9)
  • WTF (12)
  • Last ned Wallpaper

    Programmeringsbloggens tøffe skrivebordsbakgrunn med snippets fra ulike språk laster du ned her!

    Abonner via epost

    Om du vil kan du få alle nye blogposter tilsendt til din epost. Abonner nå, det er kjempeenkelt!

    Meta