Template Method del 2: På vei mot funksjonell programmering


mandag 30. januar 2012 OO Patterns Funksjonell programmering C#

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.


comments powered by Disqus