Wednesday, March 31, 2010

Open/Closed Principle, Part II

In the first post, we took a look at what the Open/Closed Principle is, why it is important, and how to use it. I’d like to continue the discussion with another example, and how the Strategy design pattern can help us out.

First, let’s take a look at the example code that we will be working with:

   1: public class ReportBuilder
   2: {
   3:     public ReportBuilder(Report report)
   4:     {
   5:         this.Report = report;
   6:     }
   7:  
   8:     public Report Report { get; private set; }
   9:  
  10:     public void BuildReport()
  11:     {
  12:         foreach (ReportField field in Report.Fields)
  13:         {
  14:             string valueToFormat = field.Data.ToString();
  15:             string valueToWrite;
  16:             CultureInfo current =
  17:                 CultureInfo.CurrentCulture;
  18:  
  19:             switch (field.FieldType)
  20:             {
  21:                 case "Date":
  22:                     DateTime dt =
  23:                         DateTime.Parse(valueToFormat);
  24:                     valueToWrite =
  25:                         dt.ToString("d", current);
  26:                     break;
  27:                 case "Currency":
  28:                     Double dbl =
  29:                         double.Parse(valueToFormat);
  30:                     valueToWrite =
  31:                         dbl.ToString("c", current);
  32:                     break;
  33:                 default:
  34:                     valueToWrite = valueToFormat;
  35:                     break;
  36:             }
  37:  
  38:             Report.Write(valueToWrite);
  39:         }
  40:     }
  41: }

This example takes a Report object, iterates its fields, and formats each based on its FieldType. This example is very simple, but is enough to illustrates code that is rather typical. So let’s see where we can make some improvements.

The most obvious place where we will get burned is in the BuildReport method. This method violates both the Single Responsibility Principle (SRP) and the Open/Closed Principle (OCP). What happens when the client wants a new format type? We compound the violation of both, thus making the code harder to read, test, and maintain. Remember, we want to try and affect as little existing code as we can, and change behavior through new code. To start, let’s introduce a simple IFieldFormatter interface.

   1: public interface IFieldFormatter
   2: {
   3:     public string Write(object data);
   4: }

Now, let’s create the formatters that will replace the logic in the switch statement in our BuildReport method:

   1: public class CurrencyFormatter : IFieldFormatter
   2: {
   3:     public string Write(object data)
   4:     {
   5:         CultureInfo current = CultureInfo.CurrentCulture;
   6:         double value = 0.00;
   7:  
   8:         if (!ReferenceEquals(data, null))
   9:         {
  10:             value = double.Parse(data.ToString());
  11:         }
  12:  
  13:         return value.ToString("c", current);
  14:     }
  15: }
  16:  
  17: public class DateFormatter : IFieldFormatter
  18: {
  19:     public string Write(object data)
  20:     {
  21:         CultureInfo current = CultureInfo.CurrentCulture;
  22:  
  23:         if (!ReferenceEquals(data, null))
  24:         {
  25:             DateTime value = DateTime.Parse(data.ToString());
  26:             return value.ToString("d", current);
  27:         }
  28:  
  29:         return string.Empty;
  30:     }
  31: }
  32:  
  33: public class NullFormatter : IFieldFormatter
  34: {
  35:     public string Write(object data)
  36:     {
  37:         if (ReferenceEquals(data, null))
  38:         {
  39:             return string.Empty;
  40:         }
  41:         return data.ToString();
  42:     }
  43: }

And now here is the updated BuildReport method:

   1: public void BuildReport()
   2: {
   3:     foreach (ReportField field in Report.Fields)
   4:     {
   5:         IFieldFormatter formatter;
   6:  
   7:         switch (field.FieldType)
   8:         {
   9:             case "Date":
  10:                 formatter = new DateFormatter();
  11:                 break;
  12:             case "Currency":
  13:                 formatter = new CurrencyFormatter();
  14:                 break;
  15:             default:
  16:                 formatter = new NullFormatter();
  17:                 break;
  18:         }
  19:  
  20:         Report.Write(formatter.Write(field.Data));
  21:     }
  22: }

This gets us closer to keeping in line with SRP and OCP.

The next step is to refactor that switch statement. In this case, I am going to move it into a new factory class:

   1: public interface IReportFieldFormatterFactory
   2: {
   3:     IFieldFormatter CreateFormatter(string fieldType);
   4: }
   5:  
   6: public class ReportFieldFormatterFactory 
   7:     : IReportFieldFormatterFactory
   8: {
   9:     public IFieldFormatter CreateFormatter
  10:         (string fieldType)
  11:     {
  12:         switch (field.FieldType)
  13:         {
  14:             case "Date":
  15:                 formatter = new DateFormatter();
  16:                 break;
  17:             case "Currency":
  18:                 formatter = new CurrencyFormatter();
  19:                 break;
  20:             default:
  21:                 formatter = new NullFormatter();
  22:                 break;
  23:         }
  24:     }
  25: }

You may be thinking that all I have really done is move the creation logic from one class to another, and technically you would be correct, but let us look a little deeper. Now our ReportBuilder can do just that, build reports without having to worry about low level details. We also have a class whose sole responsibility is to create formatters. This eliminates the need to update ReportBuilder every time we need to add a new formatter type.

Next, let’s update our ReportBuilder to accept an IReportFieldFormatterFactory parameter and update our BuildReport method:

   1: public class ReportBuilder
   2: {
   3:     private IReportFieldFormatterFactory 
   4:         fieldFormatterFactory;
   5:  
   6:     public ReportBuilder(Report report, 
   7:         IReportFieldFormatterFactory 
   8:             fieldFormatterFactory)
   9:     {
  10:         this.Report = report;
  11:         this.fieldFormatterFactory = 
  12:             fieldFormatterFactory;
  13:     }
  14:  
  15:     public Report Report { get; private set; }
  16:  
  17:     public void BuildReport()
  18:     {
  19:         IFieldFormatter formatter;
  20:  
  21:         foreach (ReportField field in Report.Fields)
  22:         {
  23:             formatter = fieldFormatterFactory
  24:                 .CreateFormatter(field.FieldType);
  25:  
  26:             Report.Write(formatter.Write(field.Data));
  27:         }
  28:     }
  29: }

Now when we need to add a new formatter, we can easily extend our existing factory and inject it into our ReportBuilder.

By adhering to the concepts behind the Open/Closed Principle we are able to modify existing functionality by adding new code and leaving the existing codebase alone. It also forces us to use other SOLID design principles which will keep us on the path to writing clean, flexible, testable, and maintainable code.

No comments:

Post a Comment