This post is part of the series SOLID Wash Tunnel.

Definition

"Simple Factory describes a class that has one creation method with a large conditional, that based on method parameters chooses which class to instantiate and then return."

- Refactoring Guru

Based on the definition the creation method should have a large conditional, but there comes a catch with that! Over time, this method might become too big, with many conditionals and you have to constantly modify it to account for newly added class types.

This clearly violates the OCP!!!

The factory class should not have to change just because a new class type has been added. To overcome this we inject some kind of "type provider" which supplies all the available class types and their creation method delegates. The factory than can perform a quick look-up on the types, and invoke the correct delegate, which in turn returns the correct object.

It is easy to confuse simple factories with other factory design patterns like Factory Method or Abstract Factory. Also declaring a simple factory as abstract, does not magically make it an abstract factory pattern.


Implementation

Looking at the PriceCalculatorFactory, we can see that it implements the IPriceCalculatorFactory, with a single method Create which takes a CustomerType enum.

The constructor accepts a dependecy of type IDictionary<CustomerType, Func<IPriceCalculator>>. This represents all available customer types and a Func delegate to invoke and return the corresponding calculator.

Inside the constructor we lazy initialize the field _calculatorsMap, which means that a calculator instance creation is deferred until it is first used. This is important since we do not want to create the different types of calculators right away, when they might not be used at all, or at least at a later point in time.

The implementation of the Create method, simply checks wether _calculatorsMap contains the provided type and invokes Func<IPriceCalculator> which in turn returns a calculator instance for the type of customer.

public enum CustomerType
{
Individual,
Company
}

public interface IPriceCalculatorFactory
{
IPriceCalculator Create(CustomerType type);
}

public class PriceCalculatorFactory : IPriceCalculatorFactory
{
private readonly Lazy<IDictionary<CustomerType, Func<IPriceCalculator>>> _calculatorsMap;

public PriceCalculatorFactory(IDictionary<CustomerType, Func<IPriceCalculator>> calculators)
{
_calculatorsMap = new Lazy<IDictionary<CustomerType, Func<IPriceCalculator>>>(calculators);
}

public IPriceCalculator Create(CustomerType type)
{
if (!_calculatorsMap.Value.TryGetValue(type, out Func<IPriceCalculator> _func))
{
throw new NotSupportedException($"No calculator was found for customer type {type}");
}

return _func.Invoke();
}
}

This is very elegant and scalable but you might (correctly so) ask:

Q: If I add a new type of calculator, I need to add it somewhere, right?

A: YES, correct!

How do we usually do this when we need extensibility in our software? Well, usually we have some sort of a config file, or database table were we store the types. When we need a new type we add an entry to the file or insert a row to the database table.

In our case we have simplified it by having a static config map class that returns these dictionaries.

internal static class ConfigMap
{
internal static IDictionary<CustomerType, Func<IPriceCalculator>>
GetPriceCalculators(ICurrencyRateConverter converter) =>
new Dictionary<CustomerType, Func<IPriceCalculator>>()
{
{ CustomerType.Individual, () => new IndividualPriceCalculator(converter) },
{ CustomerType.Company, () => new CompanyPriceCalculator(converter) }
};
}

The factory is registered as a singleton via delegate invocation into our IoC container, and the values get loaded from the ConfigMap.

public static class ServiceRegistrations
{
public static IContainer AddWashTunnel(this IContainer container)
{
container.AddSingleton<IPriceCalculatorFactory>(() =>
new PriceCalculatorFactory(ConfigMap.GetPriceCalculators(converter)));

return container;
}
}

Principles


PrincipleAppliesExplanation
SRPPriceCalculatorFactory deals only with creation of price calculators based on the customer type.
OCPDifferent calculators and more enum members can be added, yet the factory does not have to be modified.
LSPThere is no inheritance involved, so LSP has no applicability.
ISPPriceCalculatorFactory (the client) makes use of all the methods of IPriceCalculatorFactory.
DIPHigh level modules like PriceCalculatorFactory do not depend on the low level modules like IndividualPriceCalculator, CompanyPriceCalculator. They depended on the abstraction IPriceCalculator.

Continue the series on Singleton.

If you found this article helpful please give it a share in your favorite forums 😉.
The solution project is available at GitHub.