The Factory Pattern is a go-to solution for creating objects, but what if there were a way to make it more flexible and scalable? Let's explore how an enum can be used to drive a dynamic object creation process.
Say you have a program that wants to ingest some data. The said data could be read from a variety of sources and formats, like text dumps, PDF files, XML gateways, SQL data dumps, text scraped from websites, etc.
With an interface IDataReader
as our super type, we could create a separate concrete class for reading each type of data. Let's talk about selecting the right concrete class for reading data in the calling client class.
The Factory Pattern Technique:
Typically, you'd go about implementing this by creating a Factory class. To put it simply, it is a class that takes care of the logic by which we determine which object to create. In the example below, we determine which concrete object of the IDataReader
type needs to be returned using a factory class that employs a switch logic. The driving variable of the switch statement, called readerType
could be configured or received through an API call, among other ways.
//C#
using FactoryPatternEnumDemo.DataReaders;
namespace FactoryPatternEnumDemo;
public static class DataReaderFactory
{
public static IDataReader GetDataReader(string readerType)
{
switch(readerType)
{
case "CSV":
return new CSVReader();
case "XML":
return new XMLReader();
case "JSON":
return new JSONReader();
case "DBReader":
return new DBReader();
default:
return new AnyTextReader();
}
}
}
public static class DataReaderClient
{
private static void Main(string[] args)
{
//Example of object creation via a Factory Class
IDataReader dataReader = DataReaderFactory.GetDataReader("JSON");
}
}
The Enum Technique:
Another, and a comparatively terse technique for object creation, is the Enum method. We could achieve this using only the DataReaderClient
class, an Enum, and an extremely generic and reusable extension method to read the data annotations in an enum, without needing to create a separate Factory class.
Let's see the implementation of that approach in the code below.
//C#
public enum DataReadersEnum
{
[Description("FactoryPatternEnumDemo.DataReaders.CsvReader")]
Csv,
[Description("FactoryPatternEnumDemo.DataReaders.JsonReader")]
Json,
[Description("FactoryPatternEnumDemo.DataReaders.XmlReader")]
Xml,
[Description("FactoryPatternEnumDemo.DataReaders.DbReader")]
Db,
[Description("FactoryPatternEnumDemo.DataReaders.AnyTextReader")]
AnyText
}
public static class DataReaderExtensions
{
public static string GetClassName(this DataReadersEnum source)
{
FieldInfo fi = source.GetType()?.GetField(source.ToString());
DescriptionAttribute[] attributes = (DescriptionAttribute[])fi.GetCustomAttributes(
typeof(DescriptionAttribute), false);
if (attributes != null && attributes.Length > 0) return attributes[0].Description;
else return string.Empty;
}
}
public class DataReaderClient
{
private static void Main(string[] args)
{
//Object creation using the data annotations in the Enum
string dataReaderClassName =DataReaders.JSON.GetClassName();
Type type = Type.GetType(dataReaderClassName);
IDataReader dataReader2 = (IDataReader) Activator.CreateInstance(type);
}
}
NOTE: We could have used a placeholder type <T> in our extension method to make it more and truly generic, but for the sake of clarity and simplicity, we've kept it specific to the DataReaderEnum
in this article.
This method allows us to create objects dynamically using reflection. Let's compare the two techniques along with their pros and cons.
The Factory Pattern: The Pros
Decoupling the calling class from concrete implementations. By separating the logic of actually creating the classes from the rest of the flow of the code, we ensure that if we ever need to add more types IDataReader
classes, we need not touch the main calling code logic.
A SOLID-proof technique. This structure of code upholds the relevant SOLID principles - the Single Responsibility Principle (the caller code is separated from object creation logic), the Open/Closed Principle (we only need to add more cases to the switch statement to extend without modifying the existing statements), and the Dependency Injection Principle.
DRY? Check! DRY refers to the 'Don't Repeat Yourself' principle. The logic of object creation (the switch statement in our case) need not be repeated in multiple places where we need to create the IDataReader
object(s).
Centralized object creation means that we are easily able to keep track of the objects that are created at runtime if we need to. The program runs in a highly controlled manner, and we can ascertain the behavior of the code at compile time to ensure there are no sudden surprises during runtime.
All this makes our code super easy to maintain and test.
The Factory Pattern: The Cons
We end up creating extra classes. There are some cases where this might be overkill. This can make the code harder to follow for someone new to the project.
Dependency Injection can be more complicated and outdated than what is offered by modern DI frameworks (eg, Unity Containers), which often handle object creation and dependency management automatically.
Can lead to a 'God' factory. As an application grows, there's a risk of creating a single, massive factory class responsible for creating many different types of objects. This "God" factory becomes a central point of change and a maintenance nightmare.
The Enum technique: The Pros
Adherence to the Open/Closed Principle (in a different way): This approach can be seen as highly "open for extension." You can add a new reader type simply by adding a new entry to the enum and creating the corresponding class. There is no question of modifying the factory's switch statement, which means the core creation logic remains "closed for modification."
Reduced Boilerplate for Simple Cases: When the object creation logic is a straightforward one-to-one mapping from an identifier to a class, this approach can seem less verbose than a full-fledged factory. You avoid the explicit case statement for each type in the factory.
The Enum Technique: The Cons
Loss of Type safety: Making sure the data annotation descriptions do correspond to an actual module in the assembly is a delicate operation and requires thorough testing. The IDE fails to automatically refactor any renaming operations. Any renaming operations can result in bugs that are not immediately obvious and can take some time to track down. In contrast, errors of a similar nature can be caught during compile time while using the factory class.
Performance Issues: Anytime we use reflection in any scenario, we must exercise caution. Because reflection loops through all members of an assembly, we can inadvertently and often cause a quadratic runtime. The mantra is 'reflection is slow'. Although the reflection library was refactored in the .NET 8 release and is supposed to have made the operation speedier, when in doubt, stick to the aforementioned mantra. Especially when using it with any sort of collections. Much can be said about reflection; please be on the lookout for subsequent articles where we benchmark reflection libraries from different .NET frameworks and discuss use cases and best practices for it.
Hard to debug: This is another side effect of using reflection. It's not straightforward to dry-run the code, especially for a less seasoned eye. Sometimes we are compelled to run the code to understand the flow of the code. We may inadvertently end up running some sensitive lines of code. This can potentially escalate into a reputational nightmare for an organization, think of an intern accidentally sending an email to the production user base.
In Conclusion
You might have rightly concluded by now that the factory pattern is the recommended approach over dynamic object creation using Enums. The latter is actually an anti-pattern and should not be used unless there is a dire situation. While the Enum technique may look neat in trivial cases, in production scenarios, it usually creates more problems than it solves.
The working demo of the code included in this article can be obtained at this link.
Got thoughts? I'd love to hear from you in the comment section below.
Happy coding!
Found the article too boring? Read it here in the voice of Rita Skeeter.
© Ketki Ambekar 2025
Top comments (0)