Design Patterns: Factory

Design Patterns: Factory

The factory is a creational pattern that can be used to retrieve instances of objects, without having to new them up directly in calling code.

At my job, I find I’m using creational patterns constantly; and most of the time it’s a factory.

The factory pattern

In class-based programming, 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. This is done by creating objects by calling a factory method—either specified in an interface and implemented by child classes, or implemented in a base class and optionally overridden by derived classes—rather than by calling a constructor.

From Wikipedia

This pattern is very related to the strategy pattern - at least as far as I’m concerned. In the previous post on the strategy pattern we learned that you can use multiple implementations of a single interface as differing “strategies”. In the post, we were deciding based on some pretend run time situation of which strategy to use:

1
2
3
4
5
6
ILogger logger = null;
if (args[0] == "file")
logger = new FileLogger(); // create a file logger because the consumer specified it in some way.
else
logger = new ConsoleLogger(); // create a console logger as the fall back strategy.

The above could be an example of the application choosing a strategy based on some run time input (the value in args[0]).

Why is the snippet a problem? It probably won’t be the first time it happens, and when your codebase is very simple. As your codebase evolves however, and you get perhaps more places where you would want to instantiate a ILogger, and more ILoggers get added, you start needing to update more and more code. What do I mean by that? Well, imagine you added this “if/else” logger logic to 50 additional files. That if/else logic now exists in 50 files!

Every time a “branch” occurs in code, that makes the code harder to understand. This may be only one simple 4 line set of instructions, with a simple to follow branch, but what if this same sort of situation were throughout your codebase, applying to more than just an ILogger?

What if, even worse, you add a MsSqlLogger, and a MongoLogger to your possibilities of loggers, now you have an if/else branch to update in a hypothetical 50 files; that’s no good!

How can we avoid some of this hassle? The factory method to the rescue!

Implementation

We’ll be using the same ILogger strategy and implementation from the previous post as a base line. The few additions are:

1
2
3
4
5
6
7
8
9
10
public enum LoggerType
{
Console,
File
}

public interface ILoggerFactory
{
ILogger GetLogger(LoggerType loggerType);
}

That’s it for the “abstraction“ part of our factory. Now the implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class LoggerFactory : ILoggerFactory
{
public ILogger GetLogger(LoggerType loggerType)
{
switch (loggerType)
{
case LoggerType.Console:
return new ConsoleLogger();
case LoggerType.File:
return new FileLogger();
default:
throw new ArgumentException($"{nameof(loggerType)} was invalid.");
}
}
}

and a (bad) example of how to use it (since we aren’t for this example using dependency injection like we should in the real world):

1
2
3
4
5
6
7
8
9
10
11
static void Main(string[] args)
{
ILoggerFactory loggerFactory = new LoggerFactory();
ILogger logger = null;

logger = loggerFactory.GetLogger(LoggerType.Console);
logger.Log($"Doot doot, this should be a {nameof(ConsoleLogger)}. {logger.GetType()}");

logger = loggerFactory.GetLogger(LoggerType.File);
logger.Log($"Doot doot, this should be a {nameof(FileLogger)}. {logger.GetType()}");
}

Reasons to use this pattern

How does the previous section actually help us? If you recall, in our hypothetical scenario our original “if/else” branching logic occurred in 50 files. We needed to then add two additional strategies, meaning we needed to update 50 files. How did the factory help us? Well now, that branching logic is completely contained within the factory implementation itself. We simply add our MsSql and Mongo values to our enum, and add two new case statements to our factory implementation - a total of 2 files updated, rather than 50.

This not only saves us a ton of time, it help ensure that we don’t miss making updates in any of our 50 files. One additional thought is the factory itself is very testable. It’s easy to test all the “logic” that’s involved with choosing the correct strategy, because all of that logic is completely contained within the factory itself, rather than across 50 files!

References

Author

Russ Hammett

Posted on

2020-02-27

Updated on

2022-10-13

Licensed under

Comments