Click here to Skip to main content
15,892,298 members
Articles / Programming Languages / C#

Factory and Strategy Pattern Explained in C#

Rate me:
Please Sign up or sign in to vote.
4.90/5 (35 votes)
13 Mar 2016CPOL4 min read 36.2K   49   4
Explanation of Factory and Strategy patterns in C#

Introduction

This article tries to differentiate between two well known design patterns, i.e., Factory and Strategy pattern.
There are few articles explaining these two patterns but create some sort of confusion for the developer of their actual use and when to choose what. This article focuses on removing this confusion.

Background

Let's first read the definition of these two patterns.

Factory Pattern: The factory method pattern is a creational pattern that uses factory methods to deal with the problem of creating objects without having to specify the exact class of the object that will be created.

Strategy Pattern: The strategy pattern is a behavioural pattern that defines a family of algorithms, encapsulates each one of them and makes them interchangeable.

Now let's try to understand these definitions with the help of code.

Using the Code

Consider a scenario where a company "xyz" has a software which generates report for their customers. As of today, their customers are from U.S. and as per regulations, their report format requires three columns, i.e., FullName, Revenue and NetProfit.

Company's developer "John" has written a class "ReportGenerator" which has a method "Generate". This method has a responsibility of creating a datatable with needed columns and embeds the datatable into pdf file. It also writes "As per U.S. Regulations" in the footer section of the report.

C#
public class ReportGenerator
    {
        public void Generate()
        {
            DataTable dataTable = new DataTable();

            DataColumn FNColumn = new DataColumn();
            FNColumn.Caption = "Full Name";
            dataTable.Columns.Add(FNColumn);

            DataColumn RevenueColumn = new DataColumn();
            RevenueColumn.Caption = "Revenue";
            dataTable.Columns.Add(RevenueColumn);

            DataColumn NetProfitColumn = new DataColumn();
            NetProfitColumn.Caption = "Net Profit";
            dataTable.Columns.Add(NetProfitColumn);

            //Logic to populate the datatable with data coming from DB and embed to PDF.

            //Logic to write "As per U.S. Regulations" in footer section of PDF.
        }
    }

This software is gaining popularity and their customer base expanded with addition of U.K. customers. As per U.K. regulations, the report format is bit different than the existing report. It needs FullName, Revenue, EBITD and NetProfit. By the way, EBITD stands for Earnings Before Interest, Tax and Depreciation. John has been given a task to make the necessary changes in code.

John updates the "Generate" method by adding if-else conditions as shown below:

C#
public void Generate()
        {
            CustomerOrigin customerOrigin = Customer.USCustomer;// Get customer origin from DB.
            DataTable dataTable = new DataTable();

            if (customerOrigin == CustomerOrigin.USCustomer)
            {
                DataColumn FNColumn = new DataColumn();
                FNColumn.Caption = "Full Name";
                dataTable.Columns.Add(FNColumn);

                DataColumn RevenueColumn = new DataColumn();
                RevenueColumn.Caption = "Revenue";
                dataTable.Columns.Add(RevenueColumn);

                DataColumn NetProfitColumn = new DataColumn();
                NetProfitColumn.Caption = "Net Profit";
                dataTable.Columns.Add(NetProfitColumn);

                //Logic to populate the datatable with data coming from DB and embed to PDF.

                //Logic to write "As per U.S. Regulations" in footer section of PDF.
            }
            else if (customerOrigin == CustomerOrigin.UKCustomer)
            {
                DataColumn FNColumn = new DataColumn();
                FNColumn.Caption = "Full Name";
                dataTable.Columns.Add(FNColumn);

                DataColumn RevenueColumn = new DataColumn();
                RevenueColumn.Caption = "Revenue";
                dataTable.Columns.Add(RevenueColumn);

                DataColumn EBITDColumn = new DataColumn();
                EBITDColumn.Caption = "EBITD";
                dataTable.Columns.Add(EBITDColumn);

                DataColumn NetProfitColumn = new DataColumn();
                NetProfitColumn.Caption = "Net Profit";
                dataTable.Columns.Add(NetProfitColumn);

                //Logic to populate the datatable with data coming from DB and embed to PDF.

                //Logic to write "As per U.K. Regulations" in footer section of PDF.
            }
        }

This clearly violates the Open-Close principle. The "ReportGenerator" class should not be modified. John's code should be closed for modification but open for extension. Factory method pattern could have been the better approach here.

John creates an Interface "IReportGenerator" which has a method "Generate".

C#
public interface IReportGenerator
{
    void Generate();
}

He also creates a concrete class "USReportGenerator" implementing this interface.

C#
public class USReportGenerator : IReportGenerator
   {
       public void Generate()
       {
           DataTable dataTable = new DataTable();

           DataColumn FNColumn = new DataColumn();
           FNColumn.Caption = "Full Name";
           dataTable.Columns.Add(FNColumn);

           DataColumn RevenueColumn = new DataColumn();
           RevenueColumn.Caption = "Revenue";
           dataTable.Columns.Add(RevenueColumn);

           DataColumn NetProfitColumn = new DataColumn();
           NetProfitColumn.Caption = "Net Profit";
           dataTable.Columns.Add(NetProfitColumn);

           //Logic to populate the datatable with data coming from DB and embed to PDF.

           //Logic to write "As per U.S. Regulations" in footer section of PDF.
       }
   }

He then creates a factory class "ReportGeneratorFactory" whose method "GetReportGenerator" returns a concrete class object, i.e., USReportGenerator for the current context.

C#
public class ReportGeneratorFactory
   {
       public static IReportGenerator GetReportGenerator(CustomerOrigin customerOrigin)
       {
           IReportGenerator reportGenerator = null;
           if (customerOrigin == CustomerOrigin.USCustomer)
           {
               reportGenerator= new USReportGenerator();
           }

           return reportGenerator;
       }
   }

The client class calls this method and gets the report generator. Actual report generator is abstract to it. In this way, the client class is decoupled with the report generator class.

C#
IReportGenerator reportGenerator = 
   ReportGeneratorFactory.GetReportGenerator(CustomerOrigin.USCustomer);
reportGenerator.Generate();

Now, in order to add support for U.K. customers, John creates a new class "UKReportGenerator" which implements "IReportGenerator" interface. "Generate" method of this class creates a datatable as per U.K. report format.

C#
public class UKReportGenerator : IReportGenerator
   {
       public void Generate()
       {
           DataTable dataTable = new DataTable();

           DataColumn FNColumn = new DataColumn();
           FNColumn.Caption = "Full Name";
           dataTable.Columns.Add(FNColumn);

           DataColumn RevenueColumn = new DataColumn();
           RevenueColumn.Caption = "Revenue";
           dataTable.Columns.Add(RevenueColumn);

           DataColumn EBITDColumn = new DataColumn();
           EBITDColumn.Caption = "EBITD";
           dataTable.Columns.Add(EBITDColumn);

           DataColumn NetProfitColumn = new DataColumn();
           NetProfitColumn.Caption = "Net Profit";
           dataTable.Columns.Add(NetProfitColumn);

           //Logic to populate the datatable with data coming from DB and embed to PDF.

           //Logic to write "As per U.K. Regulations" in footer section of PDF.
       }
   }

By implementing factory method pattern, John has obeyed Open-Close principle, the code is open for extension but closed for modification. Now, if customer base includes German customers and if the format is different, a new class "GermanReportGenerator" will be created, implementing IReportGenerator interface. Its "Generate" method will create the datatable with desired columns.

C#
public class GermanReportGenerator : IReportGenerator
   {
       public void Generate()
       {
           //Logic to create data table as per German format.

           //Logic to populate the datatable with data coming from DB and embed to PDF.

           //Logic to write "As per German Regulations" in footer section of PDF.
       }
   }

Now, let's relate this example with the definition of Factory Method pattern.

The factory method pattern is a creational pattern that uses factory methods (ReportGeneratorFactory class and its GetReportGenerator method) to deal with the problem of creating objects without having to specify the exact class of the object that will be created (The client class Main.cs doesn't know anything about which concrete ReportGenerator object will be created. It just holds the reference to IReportGenerator and delegates the responsibility of creating the object to factory methods.)

So far so good. Company's customer base is increasing and this time, customers are from India. The Indian report columns are same as U.K. report columns but the footer must contain "As per Indian Regulations". As expected, John creates IndianReportGenerator class implementing IReportGenerator interface. This class has code of creating datatable same as of UKReportGenerator's Generate method.

C#
public class IndianReportGenerator : IReportGenerator
   {
       public void Generate()
       {
           DataTable dataTable = new DataTable();

           DataColumn FNColumn = new DataColumn();
           FNColumn.Caption = "Full Name";
           dataTable.Columns.Add(FNColumn);

           DataColumn RevenueColumn = new DataColumn();
           RevenueColumn.Caption = "Revenue";
           dataTable.Columns.Add(RevenueColumn);

           DataColumn EBITDColumn = new DataColumn();
           EBITDColumn.Caption = "EBITD";
           dataTable.Columns.Add(EBITDColumn);

           DataColumn NetProfitColumn = new DataColumn();
           NetProfitColumn.Caption = "Net Profit";
           dataTable.Columns.Add(NetProfitColumn);

           //Logic to populate the datatable with data coming from DB and embed to PDF.

           //Logic to write "As per Indian Regulations" in footer section of PDF.
       }
   }

Now if U.K. and Indian report regulations change to include another column, for example, OperatingIncome.

John changes the code in both classes, i.e., IndianReportGenerator and UKReportGenerator to include this newly required column. Maintaining the same code at two or multiple places is difficult and not recommended. So, what's the solution? Strategy Pattern

Strategy pattern allows us to define a family of algorithms, encapsulate each one of them and make them interchangeable.

  • Family of algorithms: different table generating behaviors
  • Encapsulate each one of them: each table generating behavior is encapsulated in class. e.g. table (FN, Revenue, Net profit) generating behavior is encapsulated in TableGeneratingBehavior1 class.
  • Make them interchangeable: At runtime, choose the correct table generating behavior.

John creates an ITableGeneratingStrategy interface, a strategy resolver class, and concrete strategy classes implementing ITableGeneratingStrategy.

C#
 public interface ITableGeneratingStrategy
 {
     DataTable GenerateReportTable();
 }

public class TableGeneratingStrategyResolver
    {
        public static ITableGeneratingStrategy ResolveTableGeneratingStrategy(
                            TableGeneratingStrategy tableGeneratingStrategy)
        {
            switch (tableGeneratingStrategy)
            {
                case TableGeneratingStrategy.Strategy1:
                    return new TableGeneratingStrategy1();
                case TableGeneratingStrategy.Strategy2:
                    return new TableGeneratingStrategy2();
                default:
                    throw new Exception();
            }
        }
    }

    public enum TableGeneratingStrategy
    {
        //FullName, Revenue and Net profit
        Strategy1,
        //FullName, Revenue, EBITD and Net profit
        Strategy2,
        //FullName, Revenue, EBITD, Operating Income and Net profit 
        Strategy3
    }

Concrete classes:

C#
public class TableGeneratingStrategy1 : ITableGeneratingStrategy
   {
       public DataTable GenerateReportTable()
       {
           DataTable dataTable = new DataTable();

           DataColumn FNColumn = new DataColumn();
           FNColumn.Caption = "Full Name";
           dataTable.Columns.Add(FNColumn);

           DataColumn RevenueColumn = new DataColumn();
           RevenueColumn.Caption = "Revenue";
           dataTable.Columns.Add(RevenueColumn);

           DataColumn NetProfitColumn = new DataColumn();
           NetProfitColumn.Caption = "Net Profit";
           dataTable.Columns.Add(NetProfitColumn);

           return dataTable;
       }
   }
C#
public class TableGeneratingStrategy2 : ITableGeneratingStrategy
   {
       public DataTable GenerateReportTable()
       {
           DataTable dataTable = new DataTable();

           DataColumn FNColumn = new DataColumn();
           FNColumn.Caption = "Full Name";
           dataTable.Columns.Add(FNColumn);

           DataColumn RevenueColumn = new DataColumn();
           RevenueColumn.Caption = "Revenue";
           dataTable.Columns.Add(RevenueColumn);

           DataColumn EBITDColumn = new DataColumn();
           EBITDColumn.Caption = "EBITD";
           dataTable.Columns.Add(EBITDColumn);

           DataColumn NetProfitColumn = new DataColumn();
           NetProfitColumn.Caption = "Net Profit";
           dataTable.Columns.Add(NetProfitColumn);

           return dataTable;
       }
   }
C#
public class TableGeneratingStrategy3 : ITableGeneratingStrategy
    {
        public DataTable GenerateReportTable()
        {
            DataTable dataTable = new DataTable();

            DataColumn FNColumn = new DataColumn();
            FNColumn.Caption = "Full Name";
            dataTable.Columns.Add(FNColumn);

            DataColumn RevenueColumn = new DataColumn();
            RevenueColumn.Caption = "Revenue";
            dataTable.Columns.Add(RevenueColumn);

            DataColumn EBITDColumn = new DataColumn();
            EBITDColumn.Caption = "EBITD";
            dataTable.Columns.Add(EBITDColumn);

            DataColumn OperatingIncomeColumn = new DataColumn();
            OperatingIncomeColumn.Caption = "Operating Income";
            dataTable.Columns.Add(OperatingIncomeColumn);

            DataColumn NetProfitColumn = new DataColumn();
            NetProfitColumn.Caption = "Net Profit";
            dataTable.Columns.Add(NetProfitColumn);

            return dataTable;
        }
    }

Indian and UK report generator classes will make use of Strategy resolver to get concrete table generating strategy, e.g.:

C#
ITableGeneratingStrategy tableGeneratingStrategy;
       public IndianReportGenerator()
       {
           tableGeneratingStrategy =
           TableGeneratingStrategyResolver.ResolveTableGeneratingStrategy
           (TableGeneratingStrategy.Strategy3);
       }
       public void Generate()
       {
           DataTable dataTable = tableGeneratingStrategy.GenerateReportTable();

           //Logic to populate the datatable with data coming from DB and embed to PDF.
           //Logic to write "As per Indian Regulations" in footer section of PDF.
       }

whereas USReportGenerator class code looks like this:

C#
public class USReportGenerator : IReportGenerator
   {
       ITableGeneratingStrategy tableGeneratingStrategy;
       public USReportGenerator()
       {
           tableGeneratingStrategy =
           TableGeneratingStrategyResolver.ResolveTableGeneratingStrategy
           (TableGeneratingStrategy.Strategy1);
       }
       public void Generate()
       {
           DataTable dataTable = tableGeneratingStrategy.GenerateReportTable();

           //Logic to populate the datatable with data coming from DB and embed to PDF.

           //Logic to write "As per U.S. Regulations" in footer section of PDF.
       }
   }

By implementing Strategy and factory pattern, John followed Open-Close principle and eliminated the possible duplicate code.

If concrete class and behavior have 1:1 mapping, then factory method alone will give you the desired results. Whereas if concrete class and behavior has n:1 mapping, i.e., multiple but not all concrete classes have the same behavior, then implementing strategy pattern is the right choice.

History

  • 14th March, 2016: Version 1

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Product Manager CantDisclose
India India
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 4 Pin
Andy Peralta30-Apr-17 15:10
Andy Peralta30-Apr-17 15:10 
PraiseBest among i have seen so far Pin
phani krishna reddy v6-Mar-17 2:57
phani krishna reddy v6-Mar-17 2:57 
GeneralMy vote of 5 Pin
Dmitriy Gakh15-Apr-16 1:36
professionalDmitriy Gakh15-Apr-16 1:36 
Some UML diagrams could make the article much better.
GeneralRe: My vote of 5 Pin
Rohit Sardana2-May-16 19:36
professionalRohit Sardana2-May-16 19:36 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.