NNUG / community

Artikler og nyheter om .NET miljøet, spesielt i Bergen, men også andre ting i regi av Microsoft eller andre.

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

OOP vs. FP på NNUG Bergens januarmøte

I går avholdt vi årets første møte i Norwegian .NET User Group i Bergen. Thomas Pedersen ledet an med et bra foredrag om JavaScript The Good Parts, og jeg forsøkte følge opp med å si noe nyttig om funksjonell programmering for objektorienterte utviklere gjennom å vise hvordan enkelte design patterns kan løses i C# og F#.

agenda

Påmeldingen og oppmøtet var imponerende bra – nesten 50 stykker tok seg turen for å spise pizza på Dolly og å høre på det vi hadde å si. Foredraget mitt gikk greit – noen ting føler jeg fungerte bra, andre ting kunne jeg forbedret. Det er vanskelig å holde et foredrag med såpass mye fokus på egne meninger, uten at det f.eks. er en konkret teknologi man skal vise frem, og jeg føler jeg lærte en god del av dette.

Tilbakemeldingene jeg fikk var også stort sett positive, med flere konstruktive tips til hva jeg kunne ha gjort bedre. Blant annet kunne jeg brukt litt mindre tid på introen, og kanskje droppet ett eller to av eksemplene. Tempoet virket det som om folk var fornøyde med. Har du flere tilbakemeldingen er jeg veldig interessert i å høre…

Det var spesielt én teknikk jeg viste som tydeligvis gav flere blant publikum en aha-opplevelse, og det var hvordan jeg kombinerte/slo sammen flere funksjoner til en funksjon. Hvis dette er noe folk begynner å bruke etter å ha sett min presentasjon i går, så er det nok til at det var verdt hele greiene for meg :) Kanskje jeg burde skive en liten blogpost om akkurat den teknikken…

Kvelden ble forresten for anledningen avsluttet med et besøk på Kontoret for de mest ivrige av oss.

Takk for en flott kveld! Liker veldig det gode engasjementet i utviklermiljøet i Bergen for tiden!

Conway’s Game of Life

I kveld har Bergen CodingDojo hatt sitt andre møte. Jeg skulle vært med, men måtte desverre trekke meg i siste liten pga. sykdom.

Oppgaven det skulle jobbes med i dag var Conway’s Game of Life. Dette er en god dojo-oppgave fordi det er mange måter å angripe problemet på, og det finnes flere løsninger med ulike kvaliteter. Corey Haines og de andre som arrangerer Code Retreats rundt omkring i verden bruker også denne oppgaven. Er du interessert i å vite mer om hva det er for noe anbefaler jeg å ta en titt på Corey’s introduksjon til en Code Retreat som ble arrangert i Cleveland.

Jeg løste Game of Life i forrige uke i påvente av kveldens dojo. Da brukte jeg CoffeeScript og HTML. Jeg brukte ingen enhetstester, men testet interaktivt i browseren hele tiden.

Og jeg tok det opp på video. Har du 12 minutter til overs kan du se hva jeg gjorde. Bruk full screen, og pass på at du ser den i HD. Skru også opp lyden, for jeg la på litt kul musikk.

Conway & Coffee from Torbjørn Marø on Vimeo.

Etter at jeg lagde videoen utvidet jeg også løsningen til å støtte et uendelig grid. Har du Chrome (virker i alle fall ikke i IE) kan du se løsningen live her (pass på, den begynner å bli ressurskrevende etterhvert hvis griddet vokser seg stort).

Vi jobbet også med Conway’s Game of Life i en intern coding dojo i PSWinCom i forrige uke, og vi har tenkt å gjøre det igjen i flere uker fremover – som en Code Retreat strukket utover i tid. Og vi begynner på nytt hver gang – fokuset er ikke å fullføre en komplett løsning, men å trene på å skrive “perfekt kode”.

Nå er jeg spent på å høre hvordan det har gått på kveldens dojo…

Første møte i Bergen CodingDojo

CIMG0911

I kveld har jeg vært på det aller første møtet i Bergen CodingDojo. 17 utviklere med ønske om å trene og bli bedre møttes i Miles’ lokaler under Puddefjordsbroen. Stemningen var god, og det ble en veldig kjekk kveld.

CIMG0914

Oppgaven, som skulle gjøres med TDD, var å lage en løsning som analyserte minesweeper-brett – oppgaveteksten tilgjengelig her. Vi jobbet i par, men siden vi var odde utviklere endte jeg opp som tredjeperson på ett av parene. Å sitte tre stykker forran en liten laptop er ikke så lett, så det gikk ikke lang tid før jeg begynte på en løsning for meg selv.

Til gjengjeld ble jeg først ferdig – tre kvarter før neste par. Ikke at det er noe poeng i seg selv, men det er jo kjekt å vinne! ;D

CIMG0913

Grunnen til at jeg var så rask er nok at jeg har gjort en del lignende oppgaver før; jeg begynner etterhvert å få ganske mye TDD-erfaring. Det jeg fortsatt synes er vanskelig er å bruke veldig små steg, og det klarte jeg egentlig ikke denne gangen heller.

I det siste har jeg derimot forsøkt å fokusere på å angripe hjertet av problemet direkte, og det følte jeg gikk veldig bra på denne oppgaven. Jeg løste hovedproblemet med bare én test, og uten å opprette særlig med abstraksjoner – dem la jeg til etterhvert.

CIMG0912

Flertallet var forresten Java-utviklere. De fleste andre brukte C#, mens jeg og ett av parene brukte Ruby. Nedenfor ser du løsningen min.

minesweeper.rb
 1 # Helper function for building arrays
 2 def build_array; a = []; yield a; a; end
 3 
 4 # Module to parse, solve and output minesweeper data
 5 module MineIO
 6   def self.solve_all input
 7     games = self.parse_all input
 8     games.each {|g| g.solve }
 9     self.to_s games
10   end
11   def self.parse_all input
12     lines = input.split /\n/
13     build_array do |games|
14       until lines.first == "0 0"
15         games << Minesweeper.new(self.parse_field(lines))
16       end
17     end
18   end
19   def self.parse_field input
20     build_array do |out|
21       n, m = input.shift.split.map {|x| x.to_i }
22       n.times { out << input.shift.split("") }
23     end
24   end
25   def self.to_s games # Creates the output format
26     index = 0
27     (games.inject("") do |memo, game|
28       "#{memo}Field ##{index += 1}:\n#{game.output}\n"
29     end).chop
30   end
31 end
32 
33 # Represents and solves a single minesweeper field
34 class Minesweeper
35   def initialize field
36     @field = field
37   end
38   def solve
39     (0...@field.size).each do |y|
40       (0...@field[0].size).each do |x|
41         next if @field[y][x] == "*"
42         @field[y][x] = Cell.new(x, y, @field).sum
43       end
44     end
45   end
46   def output
47     out = ""
48     @field.each do |row|
49       row.each do |cell|
50         out += cell.to_s
51       end
52       out += "\n"
53     end
54     out
55   end
56 
57   # Class used to analyse a position in the game field
58   class Cell
59     def initialize x, y, field
60       @x, @y, @field = x, y, field
61     end
62     def sum
63       north + south + east + west +
64         north_west + north_east + south_east + south_west
65     end
66     def north;      get_neighbour(@x, @y-1); end
67     def south;      get_neighbour(@x, @y+1); end
68     def east;       get_neighbour(@x+1, @y); end
69     def west;       get_neighbour(@x-1, @y); end
70     def north_west; get_neighbour(@x-1, @y-1); end
71     def north_east; get_neighbour(@x+1, @y-1); end
72     def south_east; get_neighbour(@x+1, @y+1); end
73     def south_west; get_neighbour(@x-1, @y+1); end
74     def get_neighbour x, y
75       return 0 unless x >= 0 and y >= 0 and @field[y] and @field[y][x]
76       if @field[y][x] == "*" then 1 else 0 end
77     end
78   end
79 end
80 
81 # Parse and output ananlysis if file is run directly
82 if $0 == __FILE__
83   puts MineIO.solve_all(File.readlines(ARGV[0]).join)
84 end

Jeg er spesielt fornøyd med Cell-klassen. Den gjør én ting, og den gjør det godt!

test_minesweeper.rb
 1 require "./minesweeper"
 2 require "test/unit"
 3 
 4 class Spec < Test::Unit::TestCase
 5   def setup
 6     @field = [["*", "*", ".", ".", "."],
 7               [".", ".", ".", ".", "."],
 8               [".", "*", ".", ".", "."]]
 9     @m = Minesweeper.new @field
10   end
11   def test_individual_cells
12     @m.solve
13     assert_equal 1,   @field[0][2]
14     assert_equal "*", @field[0][1]
15     assert_equal 2,   @field[1][2]
16     assert_equal 0,   @field[1][3]
17     assert_equal 3,   @field[1][1]
18   end
19   def test_output
20     expected = <<EOF
21 **100
22 33200
23 1*100
24 EOF
25     @m.solve
26     assert_equal expected, @m.output
27   end
28   def test_parse
29     input = <<EOF
30 3 5
31 **...
32 .....
33 .*...
34 EOF
35     assert_equal @field, MineIO.parse_field(input.split(/\n/))
36   end
37   def test_complete
38     input = <<EOF
39 4 4
40 *...
41 ....
42 .*..
43 ....
44 3 5
45 **...
46 .....
47 .*...
48 0 0
49 EOF
50     output = <<EOF
51 Field #1:
52 *100
53 2210
54 1*10
55 1110
56 
57 Field #2:
58 **100
59 33200
60 1*100
61 EOF
62     assert_equal output, MineIO.solve_all(input)
63   end
64 end

Til slutt et bilde av min kjære, gamle laptop. Drivstoffet står til høyre.

CIMG0915

Følgefredag: @magnars

magnarsDet er fredag igjen, og jeg fortsetter med å anbefale spennende folk å følge på twitter om man er norsk utvikler. Og i dag presenterer jeg Magnar Sveen, en glad framsideutvikler med Mastergrad fra NTNU, som jobber og er deleier i konsulentselskapet Kodemaker. For tiden er det først og fremst FINN som får nyte godt av Magnars kompetanse og kreativitet.

Magnar er ganske aktiv på Twitter. I tillegg har han en meget solid blogg han kaller Framsideutvikling, hvor han blogger om ting som HTML5 og JavaScript – inkludert jQuery og testdreven webutvikling. Dessuten har han startet et meget kult screencast-prosjekt om testdreven javascript, zombier og mafia på zombietdd.com. Veldig lærerikt!

zombietdd

Sjekk også ut pomodoro-verktøyet Magnar har laget på mytomatoes.com, og eventyrspillet Adventur Delux. På twitter avslørte han også for noen dager siden at han holder på å lage et nytt scriptspråk for Adventure, så det er klart at Magnar behersker litt av hvert.

Så hvis du driver med webutvikling er dette en fyr du helt sikkert kan lære mye av!

Følgefredag: @HavardStranden

For å utvikle seg og bli en bedre programmerer er det blant annet viktig å se hva andre, dyktige utviklere gjør og får til. Twitter har siden starten vært en glimrende plattform for å følge diskusjonene i utviklermiljøet, og få innput og inspirasjon fra likesinnede.

Hver fredag en stund fremover vil jeg derfor forsøke å trekke frem, anbefale og gi litt oppmerksomhet til norske twitterbrukere det er verdt å følge med på. Og først ut er Håvard Stranden!

3d41700Håvard har en Master of Computer Science-grad fra NTNU med spesialisering innen kunstig intelligens. I dag er han seniorpartner og konsulent hos Conceptos Consulting i Trondheim. På twitter beskriver han seg selv som far, ektemann, konsulent og geek.

Håvard er spesielt interessert i software design og design patterns. Han deler dessuten min interesse for programmeringsspråk, og behersker C#, Python, Java, C++, C, PHP og Javascript. Han har en blogg han kaller Circles & Crosses, hvor han skriver litt om det han holder på med.

Han holder også til på Github, hvor han har flere, spennende prosjekter. Det ferskeste og hotteste er OpenID for Node.js, men .NET-utviklere bør også ta en titt på Copyable, et biblotek for å klone .NET-objekter. Og driver du med WinForms kan Strongbind være av interesse.

Så følg etter denne mannen – han har mye interessant å fortelle!

Lenker: Håvard på Twitter, Github, LinkedIn, og her er bloggen hans.

Høydepunkter fra NDC 2011

Her følger min oppsummering fra Norwegian Developers Conference 2011. Det har vært en kanonbra konferanse med mange høydepunkt, og dette blir derfor en lang bloggpost med litt for enhver smak.

For å friske opp oppsummeringen har jeg blant annet lånt litt bilder tatt under konferansen av Tomas Eilev Tveit Rundkås. Alle bildene han knipset under konferansen er samlet her. De mindre bra bildene i artikkelen, som stort sett har dårlig fokus, er typisk tatt av meg selv eller mine kollegaer.

The Uncle Bob Experience

Jeg gleder meg alltid til å høre Robert C. Martin – a.k.a. Uncle Bob – og i år fikk jeg med meg hele tre inspirerende foredrag. Det første kalte han “The Last Programming Language”, hvor han argumenterte for at det er på tide at bransjen standardiserer seg på ett (eller kanskje mer realistisk et par-tre) programmeringsspråk. Han påpekte at andre bransjer har gjort det samme, og at det er mange fordeler ved å gjøre det.

Som f.eks. når man skal ansette utviklere. Hvilken type utviklere trenger man? Spiller ingen rolle – alle bruker det samme. Og alle kodeeksempler er skrevet i samme språk. Alle lærer det samme på skolen, og alle kan derfor kommunisere enklere. Jeg vet ikke om vi er der helt ennå, men det er en spennende tanke.

Av språkene vi bruker i dag følte Uncle Bob at Clojure er det nermeste vi kommer et ultimat språk som vi kan forene oss om. Han sa ikke at det kommer til å bli Clojure, men at det bør bli et språk som har mange av de samme egenskapene.

Du kan høre kortversjonen av Roberts argumentasjon på hans videoblog.

IMG_9644

Det neste foredraget hadde tittelen “The Transformation Priority Premise”. Her hevdet Uncle Bob å ha kommet opp med en metode for å utlede algoritmer i små steg. Ved å følge noen enkle regler skal man kunne komme frem til den optimale algoritmen for et gitt problem.

Jeg er ikke sikker på om jeg kjøper teorien fullt ut. Robert hadde tross alt ikke noe bevis for at teorien holdt vann. Men teknikken er uansett interessant, og illustrerte ganske bra hvordan TDD og små steg gjør det enklere å utvikle komplisert kode.

IMAG0296

Det siste Uncle Bob-foredraget jeg så hadde han kalt “WTF is a Monad?”. Jeg har også stilt, og forsøkt å svare på, dette spørsmålet i en blogpost for et lite år siden. Det var et underholdene foredrag, men som vanlig fikk monadene hjernen min til å eksplodere.

Det er egentlig ganske merkelig. Monader består av noen ganske enkle byggestener, og jeg vet også stort sett hvordan jeg kan bruke dem. Men når jeg forsøker å forstå monadene så nekter hjernen å sammarbeide.

HTML5, JavaScript og andre webutviklingsgreier

JavaScript (spesielt jQuery) og HTML5 er ganske hot for tiden, og hadde også en sentral plass på konferansen. Bruce Lawson fra Opera viste seg å være en meget dyktig foredragsholder, og fortalte oss hva HTML5 er, hva det ikke er, og hvordan vi kan ta det i bruk i dag. Nedenfor ser du et av de morsomme lysbildene fra presentasjonen.

IMG_9561

Christian Johansen hadde også en spennende sesjon hvor han demonstrerte testdreven utvikling i praksis med JavaScript. Vi fikk dessuten tips til en rekke nyttige HTML5/JavaScript-verktøy i løpet av konferansen, som f.eks. html5shiv og Modernizr.

Generelt sett har JavaScript vokst til å bli et meget spennende språk.., eller som noen på twitter sa det: “Maybe JavaScript is The Last Programming Language?”.

The Three Amigos

En av de mer underholdene sesjonene jeg deltok på var en “smackdown” mellom rammeverkene Ruby on Rails og ASP.NET MVC. Utviklerne på senen var Hadi Hariri (.NET) og Rob Conery (Rails), mens Scott Bellware var kommentator.

Det ble mye morsom “dissing” mellom Hadi og Rob når det kom til valg av utviklingsverktøy og rammeverk, men demoen viste egentlig at ASP.NET MVC har kommet et godt stykke, og ikke står så langt tilbake for Rails. Personlig synes jeg derimot at utvikling med kommandolinjeverktøy, vim og et dynamisk språk er mye mer behagelig enn et tregt Visual Studio som crasher hele tiden (Hadi måtte selvfølgelig kjøre en tvunget restart av VS under demoen).

.NET Rocks!

Vi fikk også anledning til å overvære en innspilling av et .NET Rocks! podcast. Richard Campbell og Carl Franklin arrangerte en paneldebatt om mobile plattformer, noe som ble en heftig debatt om iPhone vs. Android vs. Windows Phone 7 og native apps vs. HTML-baserte apps.

IMG_9678

Og gjett hvems bakhode som har sneket seg inn på bildet her?! Er ganske sikker på at det er mitt ja.

Andre som bare må nevnes

Jeg hadde gledet meg til å gå på ett av Douglas Crockfords foredrag. Det viste seg at han ikke var noe særlig spennende foredragsholder, men han hadde såpass mye interessant å si, og så mye erfaring å trekke på, at det var kjekt likevel.

I sitt foredrag om kvalitet påpekte Crockford at programmering er det mest komplekse vi mennesker kan gjøre. Det er faktisk så komplisert, sier han, at det er utrolig at vi i det hele tatt får det til. Han påpekte også at programmerere er evige optimister – mer optimistiske enn folk flest. Det er vi nødt til å være for å ha tro på at vi kan få til det vi driver med.

IMG_9514

En annen fyr jeg hadde gledet meg til var Jon Skeet (bilde over). I foredraget “Async Deep Dive” – som han døpte om til “goto considered awesome” i løpet av konferansen – åpnet han opp panseret på .NET 5’s spennende async-feature, og viste oss steg for steg hvordan det hele er implementert. Anbefaler på det sterkeste at du får med deg dette foredraget når det slippes på video, men les deg opp på async og await først – dette er ikke en sesjon for beginners.

Rob Conerys foredrag “Kill your ORM” var også et høydepunkt. Han dro gjennom hvordan vi har forholdt oss til databaser og database-aksess de siste 20 årene, og konkluderte med at ORM-rammeverkene vi har blitt så glad de siste årene har blitt for store, for kompliserte, skjuler for mye av hva som skjer, og at det er på tide å (delvis) gå back to basics. Han viste oss så hvordan man kan lagen en mye enklere databaseaksess i .NET 4, og introduserte oss til slutt for MASSIVE, som er Robs 400-linjers løsning på nettopp dette.

Til slutt vil jeg nevne Kevlin Henney. Jeg gikk på hans foredrag om “Cognitive Biases and Effects You Should Know About”. Dette er et veldig viktig tema, som vi også hørte mye om på QCon i fjor. Foredraget fortalte om hvordan vi ubevisst lar oss styre av grunnleggende, menneskelige effekter, og hvordan det hindrer oss i å velge riktige løsninger. For å løse dette problemet anbefaler jeg at du leser boken Pragmatic Thinking and Learning: Refactor Your Wetware – en fantastik bok jeg selv akkurat har fullført, med en masse tips og triks til hvordan du kan bli en bedre problemløser og mere effektiv programmerer.

The Party

Festen på torsdagskvelden var som forventet en stor suksess. Til å begynne med hang jeg mye på standen vår (vi hadde egen øl), hvor det var mange som kom innom for å spille .NET Ninja Quiz (se nedenfor). Men da Loveshack entret scenen måtte jeg bort og rocke til 80-talls hits.

IMG_9958

En del av poenget med å dra på en konferanse som NDC er å møte folk, og det ble mange anledninger til å snakke med både foredragsholdere, gamle kjente, og folk jeg bare har kjent via nettet. Det var f.eks. veldig hyggelig å endelig treffe Ameth (et kjent navn fra kommentar-feltet på denne bloggen), som brukte deler av torsdagsfesten til å vise meg noen av Haskells finurligheter.

The .NET Ninja Quiz

PSWinCom-standen var godt besøkt under konferansen, og det var mye takket være .NET Ninja Quizen vi kjørte. Enkelte ble helt hekta, men det var få som klarte å nå øverste nivå.

IMAG0303 IMAG0304

Over til venstre ser du den aller første ninjaen vi kåret, og over til høyre har du han som ble ninja takket være standhaftighet og pågangsmot – han hang stort sett rundt standen i tre dager og gav seg ikke før han klarte alle spørsmålene.

IMAG0233b IMAG0286

Mange forelesere våget også etterhvert å forsøke konkurransen. Over til venstre Scott Bellware, over til høyre Corey Haines. Ingen av dem er i dag .NET-utviklere, så de slet bigtime.

IMAG0300 2011-06-09 21.42.41

I løpet av festen på torsdagen ble quizen i praksis et team-arbeid, og over til venstre ser du en gjeng som til slutt klarte å bli ninjaer. Over til høyre står Sebastien Lambla og fnyser av hvor enkle spørsmålene er…

Oppsummering

NDC 2011 ble en stor suksess. Gjennomføringen var upåklagelig, og spesielt kjekt er det når flere av de internasjonale foredragsholderne sier at det er deres favorittkonferanse.

Personlig er jeg også veldig fornøyd; jeg hadde det kjekt, og fikk med meg mange inspirerende foredrag. Men for å klage på noe så vil jeg si at jeg kunne ønske meg litt flere sesjoner med et høyere teknisk nivå. Blant foredragene som handlet om konkret teknologi ble det litt mange “introduksjoner” liksom, og jeg hadde satt pris på om det var noen flere som forutsatte at tilhørerne allerede har litt erfaring (sånn som Jon Skeets Async Deep Dive).

Likevel, NDC 2011 var en fantastisk vitamininnsprøytning, og man skal ikke se bort fra at jeg/vi kommer tilbake neste gang.


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