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.
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);
}
}
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:
public void Generate()
{
CustomerOrigin customerOrigin = Customer.USCustomer;
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);
}
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);
}
}
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
".
public interface IReportGenerator
{
void Generate();
}
He also creates a concrete class "USReportGenerator
" implementing this interface.
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);
}
}
He then creates a factory class "ReportGeneratorFactory
" whose method "GetReportGenerator
" returns a concrete class object, i.e., USReportGenerator
for the current context.
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.
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.
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);
}
}
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.
public class GermanReportGenerator : IReportGenerator
{
public void Generate()
{
}
}
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.
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);
}
}
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
.
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
{
Strategy1,
Strategy2,
Strategy3
}
Concrete classes:
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;
}
}
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;
}
}
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.:
ITableGeneratingStrategy tableGeneratingStrategy;
public IndianReportGenerator()
{
tableGeneratingStrategy =
TableGeneratingStrategyResolver.ResolveTableGeneratingStrategy
(TableGeneratingStrategy.Strategy3);
}
public void Generate()
{
DataTable dataTable = tableGeneratingStrategy.GenerateReportTable();
}
whereas USReportGenerator
class code looks like this:
public class USReportGenerator : IReportGenerator
{
ITableGeneratingStrategy tableGeneratingStrategy;
public USReportGenerator()
{
tableGeneratingStrategy =
TableGeneratingStrategyResolver.ResolveTableGeneratingStrategy
(TableGeneratingStrategy.Strategy1);
}
public void Generate()
{
DataTable dataTable = tableGeneratingStrategy.GenerateReportTable();
}
}
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
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.