mandag 30. januar 2012 OO Patterns Funksjonell programmering C#
I 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..
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 }
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 }
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 }
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.