The Factory design pattern in C#
Factories are methods or objects whose only role is to create other objects. The Factory Method and Abstract Factory patterns are two creational design patterns originally identified in the seminal “Gang of Four” Design Patterns book. They are still crucial today in .NET. This article explains, with concrete examples, when to use this pattern in modern C# applications and how to make sure that they are properly applied.
Example: setting up the scene
This article will use a concrete, real-world example. Let’s set up the scene.
Let’s say you are a software developer at a startup. You need to read and write binary files. Initially, you might have chosen to use local file storage because it was simple and met the immediate needs of your business – to make the demo work for potential investors.
Your company gets more funding, and management decides it’s time to deploy the application to the cloud. File-system storage is no longer sufficient. You now need to support cloud storage too. For the sake of simplicity in this article, we will assume that the cloud storage has a simple HTTP interface. Since your app can still be used on-premises as well as on the cloud, you decide to abstract storage access under the following interface:
internal interface IStorageAdapter
{
Task<Stream> OpenReadAsync();
Task WriteAsync( Func<Stream, Task> write );
}
The implementations of these classes, FileSystemStorageAdapter
and HttpStorageAdapter
, are not very interesting. Please check the article’s source code for details.
Step 0. Without the factory pattern
When drafting the first prototype of an app, it’s common to start by directly creating objects where they are needed. For instance, in the context we mentioned before, you might instantiate specific implementations like FileSystemStorageAdapter
or HttpStorageAdapter
directly in the code.
internal class Program
{
private static async Task Main( string[] args )
{
var url = args[0];
// Direct instantiation
IStorageAdapter storageAdapter = url.StartsWith( "https://" )
? new HttpStorageAdapter(url)
: new FileSystemStorageAdapter( "url" );
// Using the adapter.
await using var stream = await storageAdapter.OpenReadAsync();
using var reader = new StreamReader( stream );
Console.WriteLine( await reader.ReadToEndAsync() );
}
}
This approach works well for simple use cases here, but what if we need to get a storage adapter from dozens of other locations. As you can see, simplicity comes at the cost of flexibility.
Control flow
Here is the representation of the control flow without any factory pattern. We will see in this article that it gets more complex as we add abstraction.
flowchart LR
Consumer --calls--> Adapter
The problem with directly calling constructors
As the project grows, directly creating objects can lead to tight coupling between the code and specific implementations. For example, if a new requirement emerges to dynamically select a specialized HTTP adapter based on the domain name, selectively handling authentication, significant changes may be needed. The Main method (or any other consumer of these adapters) would need logic to determine which adapter class to create. This setup not only violates the Single Responsibility Principle but also makes the code harder to test and maintain. A more scalable solution is required as the complexity increases.
Step 1. The Factory Method pattern
A Factory Method is a creational design pattern used in object-oriented programming to define an interface for creating objects, but allowing subclasses to alter the type of objects that will be created. Instead of instantiating objects directly, the Factory Method delegates the responsibility of object creation to a dedicated method or class, promoting loose coupling and scalability.
The Factory Method is typically a static method. It can be either in the class that it instantiates or in a dedicated class.
The CreateStorageAdapter
method will return an instance of the HttpStorageAdapter
or FileSystemStorageAdapter
based on the URL’s scheme (HTTP or file path):
namespace Factory
{
internal sealed class StorageAdapterFactory
{
public static IStorageAdapter CreateStorageAdapter( string pathOrUrl )
{
// Logic to determine storage adapter type
if ( pathOrUrl.StartsWith( "https://" ) )
{
return new HttpStorageAdapter( pathOrUrl );
}
else
{
return new FileSystemStorageAdapter( pathOrUrl );
}
}
}
}
The instantiation logic can now be removed from the consumers. The client code will remain agnostic to specific implementations, allowing the OpenReadAsync
method to be called without knowing the exact type of the concrete storage adapter being used or its family.
internal class Program
{
private static async Task Main( string[] args )
{
var storageAdapter = StorageAdapterFactory.CreateStorageAdapter( "path/to/file.txt" );
await using var stream = await storageAdapter.OpenReadAsync();
using var reader = new StreamReader( stream );
Console.WriteLine( await reader.ReadToEndAsync() );
}
}
As you can see, this pattern provides a way to encapsulate the instantiation logic and make the system more flexible and extensible. By using a Factory Method, you can introduce new classes with minimal changes to existing code, as new object types can be added by extending the factory method rather than modifying existing client code. For example, if a new storage adapter is required to read data from a database, a developer can create a DatabaseStorageAdapter
class and update the factory method to include this new type.
Control flow
Here is a representation of the control flow with the Factory Method pattern:
flowchart LR
Consumer --calls--> Factory
Factory --creates--> Adapter
When to use a Factory Method?
Case 1. Dynamically choosing the right class and constructor
The main use case for the Factory Method pattern is when a class can’t anticipate the class of objects it must create. This pattern is particularly useful when the exact class of objects to be created is determined at runtime, based on user input, configuration settings, or other factors. By delegating object creation to subclasses, the Factory Method pattern allows for greater flexibility and extensibility in the system.
As we saw, our CreateStorageAdapter
factory method uses a simple string comparison to determine the type of storage adapter to create. In a real-world scenario, the logic for choosing the right constructor might be more complex, involving:
- runtime multiple conditions
- certain calculations
- specific configurations
- external dependencies
As an example, let’s say we want to perform some validations before creating the storage adapter. We can add this logic to the factory method without affecting the client code.
public static IStorageAdapter CreateStorageAdapter(string input)
{
// Logic to determine storage adapter type
if (input.StartsWith("https://"))
{
// Check if URL is reachable
if (IsReachable(input))
{
var httpClientFactory = new DefaultHttpClientFactory(); // Simulated DI
return new HttpStorageAdapter(httpClientFactory, input);
}
else
{
throw new InvalidOperationException($"Cannot access the URL {input}");
}
}
else
{
// Validate the file path or check disk space before creating the adapter
if (File.Exists(input))
{
return new FileSystemStorageAdapter(input);
}
else
{
throw new FileNotFoundException($"The file at {input} does not exist.");
}
}
}
This Factory Method will now encapsulate this decision-making process within a single method by determining which constructor to use and whether the conditions for its invocation are met.
Case 2. Async object creation
Another scenario where the Factory Method pattern can be useful is when object creation involves asynchronous operations. For example, if the creation of a storage adapter requires fetching data from a remote server or performing other asynchronous tasks, the Factory Method can handle these operations without blocking the main thread.
In the previous code snippet, we can modify the CreateStorageAdapter
method to asynchronously perform the reachability and file existence validations before creating the storage adapter. Doing so, the Factory Method will return a Task<IStorageAdapter>
instead of an IStorageAdapter
allowing the method to perform asynchronous operations without blocking the main thread.
public async Task<IStorageAdapter> CreateStorageAdapter(string input)
{
// Logic to determine storage adapter type
if (input.StartsWith("https://"))
{
var isSecure = await CheckUrlSecurityAsync(input); // Asynchronous validation
// Check if URL is reachable and secure
if (IsReachable(input) && isSecure)
{
return new HttpStorageAdapter(input);
}
else
{
throw new InvalidOperationException($"Cannot access the URL {input} or it is not secure.");
}
}
else
{
var fileExists = await Task.Run(() => File.Exists(input)); // Simulated async file check
if (fileExists)
{
return new FileSystemStorageAdapter(input);
}
else
{
throw new FileNotFoundException($"The file at {input} does not exist.");
}
}
}
Case 3. Improving maintainability
As we mentioned, the project will grow, and new requirements will emerge. The Factory Method pattern makes it easier to maintain and extend the system over time without affecting the code consuming the factory method.
You can easily:
-
Add or modify implementation types: Adding support for new implementation types becomes seamless with the Factory Method. If a new
RedisStorageAdapter
is required, it can be implemented and incorporated into the factory without modifying existing client code. This approach adheres to the Open/Closed Principle, enabling the application to extend functionality by adding new product types without modifying the core factory logic or client interactions. -
Modify the construction logic: The Factory Method pattern lets you make code changes without converting them into “breaking changes”. For instance, if you need to change how a
FileSystemStorageAdapter
is initialized (perhaps to add a logging mechanism or encryption), this can be done directly within the factory without altering the consumers of the factory. Since consumers interact only with the factory and not the implementation details, the changes remain localized and avoid ripple effects in the codebase.
Case 4. Controlling object lifetime
The Factory Method pattern provides a natural mechanism to control the lifetime of objects, enabling reuse rather than creating new instances every time.
Initially, a factory simply returns new instances of a class each time it is called. However, if the object creation process becomes expensive, such as initializing a resource-heavy storage adapter, it can easily be changed to a resource pool, optimizing performance and resource utilization.
In our storage context, consider an HttpStorageAdapter
that establishes a persistent connection to a remote server. Repeatedly creating new instances could involve unnecessary overhead, such as reinitializing the connection and duplicating validation steps. Instead, the factory can cache and reuse a single instance of the HttpStorageAdapter
for a given URL.
Limitations
While Factory Methods encapsulate object creation logic and provide numerous benefits, they introduce a tight coupling between the factory and the specific types. This occurs because the factory knows about all the concrete classes it can create. For example, the StorageAdapterFactory
directly instantiates HttpStorageAdapter
or FileSystemStorageAdapter
, making it responsible for managing the creation of these classes.
This tight coupling can also make testing difficult. For example, if the factory directly creates an HttpStorageAdapter
that depends on real network connections, testing the behavior of the factory in isolation becomes a challenge without creating intricate mocks or stubs. While dependency injection can mitigate some of these problems, it adds another layer of complexity. To avoid these drawbacks, an alternative is to use an Abstract Factory or delegate the instantiation logic entirely to external DI or configuration containers, allowing the application to decouple the build process from the specific implementation details.
Step 2. The Abstract Factory pattern
The Abstract Factory pattern addresses the limitations of the Factory Method by making the factory logic abstract and therefore replaceable.
The principal use case is unit testing. Unit tests, as you know, must be fast and should not perform I/O, except when you are intentionally testing I/O adapters.
Translate this to our example: unless we are explicitly testing HttpStorageAdapter
and FileSystemStorageAdapter
, we will want to run all other unit tests under an in-memory implementation of IStorageAdapter
. Instead of the default StorageAdapter
, our unit tests will use a TestStorageAdapter
that will return an in-memory implementation regardless of the protocol of the URL.
To make this possible, we need to abstract the factory as an interface. This is why this pattern is called the Abstract Factory.
internal interface IStorageAdapterFactory
{
IStorageAdapter CreateStorageAdapter( string url );
}
The default implementation of IStorageAdapterFactory
, used in production, is the following:
internal class StorageAdapterFactory : IStorageAdapterFactory
{
private readonly IHttpClientFactory _httpClientFactory;
public StorageAdapterFactory( IHttpClientFactory httpClientFactory )
{
this._httpClientFactory = httpClientFactory;
}
public IStorageAdapter CreateStorageAdapter( string url )
{
if ( url.StartsWith( "https://" ) )
{
return new HttpStorageAdapter( this._httpClientFactory, url );
}
else
{
return new FileSystemStorageAdapter( url );
}
}
}
Abstract factories are often added to the service collection (also called the dependency injection container). Here is how you would initialize your app:
var services = new ServiceCollection();
services.AddSingleton<IStorageAdapterFactory, StorageAdapterFactory>();
services.AddHttpClient();
Here is how the abstract factory is typically consumed:
var factory = serviceProvider.GetRequiredService<IStorageAdapterFactory>();
var storage = factory.CreateStorageAdapter( "https://www.google.com" );
Control flow
As we see in the control flow diagram, we added one more abstraction layer between the consuming code and the service.
flowchart LR
Consumer --calls--> IServiceProvider
IServiceProvider --creates--> Factory
Factory --creates--> Adapter
When to use abstract factories?
We will cover some scenarios where the Abstract Factory pattern is particularly useful and how it can help you address complex object creation requirements.
Case 1. Better support of unit testing
The most common use of an Abstract Factory is to improve testability by allowing you to easily replace real implementations with test-specific ones. For example, during unit tests, a TestStorageAdapterFactory
can implement the IStorageAdapterFactory
interface and return mock adapters that simulate the behavior of file or network storage without relying on external resources. The consuming code is not aware of these substitutions because it only interacts with the Abstract Factory interface.
Unit tests would use the following implementation instead of the default one:
internal class TestStorageAdapterFactory : IStorageAdapterFactory
{
private readonly ConcurrentDictionary<string, TestStorageAdapter> _storageAdapters = new();
public IStorageAdapter CreateStorageAdapter( string url )
=> this._storageAdapters.GetOrAdd( url, s => new TestStorageAdapter() );
}
internal class TestStorageAdapter : IStorageAdapter
{
private byte[] _buffer = [];
public Task<Stream> OpenReadAsync()
=> Task.FromResult<Stream>( new MemoryStream( this._buffer ) );
public async Task WriteAsync( Func<Stream, Task> write )
{
using var memoryStream = new MemoryStream();
await write( memoryStream );
this._buffer = memoryStream.ToArray();
}
}
Case 2. More abstraction needed over the construction process
The Abstract Factory pattern stands out in scenarios where a higher level of abstraction is required, particularly when managing a family of related objects. By encapsulating multiple factories under a unified interface, it allows client code to remain agnostic to the particular classes being created. This abstraction ensures that the client code only depends on the factory interface and not the specific implementations, promoting decoupling and adherence to the Dependency Inversion Principle. It is particularly beneficial in complex systems where the object creation logic must be consistent across all interrelated components, yet flexible enough to adapt to new requirements or configurations.
In the storage example, let’s say we want to support multiple storage families such as:
- Local file storage (i.e., Local, Network attached storage - NAS)
- Database Storage (i.e., SQL Server, MongoDB)
- Cloud storage (i.e., AWS S3, Azure Blob)
Then, we can create a new SqlServerStorageAdapterFactory
, MongoDbStorageAdapterFactory
, S3StorageAdapterFactory
, … that implement the AdapterFactory
interface to create the corresponding storage adapter. Each of the factories will encapsulate the creation logic for their respective storage systems, such as establishing database connections or configuring cloud storage services.
Now, to instantiate these factory classes, we need another factory class, a factory of factories, which are often named Providers in the Microsoft space. The provider can be implemented with a hardcoded switch...case
or can support a plug-in model using, for instance, MEF.
flowchart LR
Consumer --calls--> IServiceProvider
IServiceProvider --creates--> Provider
Provider --creates--> Factory
Factory --creates--> Adapter
This approach allows the application to switch between different storage families seamlessly without altering the client code, providing a scalable and maintainable solution for managing diverse storage requirements.
Validating the Abstract Factory pattern
Let’s be honest, we are human, and we can make mistakes. What happens if you get distracted and instantiate a concrete factory product (e.g., HttpStorageAdapter
) from somewhere else in your code? This would break the abstraction and the benefits of the Abstract Factory pattern.
Generally, the StorageAdapterFactory
class is the only class that should be allowed to create instances of concrete adapters. More precisely, any class that implements the IStorageAdapterFactory
interface should be the only one responsible for creating instances of the corresponding storage adapters. The only potential exceptions may be unit tests.
To validate this constraint, we can use Metalama architecture validation and define a custom aspect that verifies if the constructor of a concrete adapter is only called from a class that implements the IStorageAdapterFactory
interface or from a test namespace. If the caller is not the factory or a test, the aspect can raise a warning to let the developer know that the code is violating the intended design.
internal class ConcreteStorageAdapterAttribute : TypeAspect
{
public override void BuildAspect( IAspectBuilder<INamedType> builder )
=> builder.Outbound.SelectMany( t => t.Constructors )
.CanOnlyBeUsedFrom(
scope => scope.Namespace( "**.Tests" )
.Or()
.Type( typeof(IStorageAdapterFactory) ),
"""
The class is a concrete factory and can be only instantiated
from a class implementing IStorageAdapterFactory.
""" );
}
Here we’re applying the aspect to the FileSystemStorageAdapter
class to ensure that their constructors are only called from the factory or test classes. The same applies to the HttpStorageAdapter
class.
[ConcreteStorageAdapter]
internal class FileSystemStorageAdapter : IStorageAdapter
{
private readonly string _filePath;
public FileSystemStorageAdapter( string filePath )
{
this._filePath = filePath;
}
public Task<Stream> OpenReadAsync()
{
return Task.FromResult( (Stream) File.OpenRead( this._filePath ) );
}
public async Task WriteAsync( Func<Stream, Task> write )
{
await using var stream = File.OpenWrite( this._filePath );
await write( stream );
}
}
Thus, if you want to create an instance of FileSystemStorageAdapter
from a class that is not a factory or a test (I’ll do it from the program class for the sake of the example), Metalama will raise a warning.
Can we automatically generate the boilerplate?
In theory, Metalama could also generate the factory classes and the abstract factory pattern. You could define a custom aspect for it, and you have the tools to do it.
We tried, but it proved impractical. It only works if the only thing factory methods are doing is calling a constructor with exactly the same signature as the factory method. We found that the aspect added too little value for too much complexity and would not recommend you go this way in most cases.
Relationships with other patterns
The beauty of design patterns is that they can be combined to address complex problems and provide more flexible and scalable solutions. The Factory and Abstract Factory patterns can be used in conjunction with other patterns to create more sophisticated systems that meet specific requirements. Here are some patterns that can be combined with the Factory and Abstract Factory patterns to enhance their capabilities.
Builder pattern
The Abstract Factory can provide builders as its products when families of complex objects are needed. The Builder pattern is used to construct complex objects step by step, allowing the construction process to vary independently from the final object representation.
Builders are objects that must be created, and the Factory patterns are a possible way to instantiate them, offering all the benefits presented in this article.
For example, instead of creating storage adapters just based on the url
parameter, a factory could provide a more sophisticated interface allowing for step-by-step configuration of different options such as authentication headers, timeouts, and caching policies.
public interface IStorageAdapterOptions
{
string Url { get; }
int Timeout { get; }
}
public interface IStorageAdapterOptionsBuilder
{
string Url { get; set; }
int Timeout { get; set; }
IStorageAdapterOptions Build();
}
public interface IStorageAdapterFactory
{
IStorageAdapterOptionsBuilder CreateStorageAdapterOptionsBuilder();
IStorageAdapter CreateStorageAdapter(IStorageAdapterOptions options);
}
Singleton pattern
As we briefly said earlier, the Singleton pattern can be used to manage the lifecycle of an Abstract Factory, to ensure that only one instance of the factory is created and used throughout the application.
Conclusion
In this article, we explored the Factory and Abstract Factory design patterns, two creational patterns that help you create objects without specifying the exact class of object that will be created. We discussed the benefits of using these patterns, such as encapsulating object creation logic, promoting loose coupling, and improving scalability and maintainability. We also covered the limitations of these patterns, such as tight coupling and testing challenges, and how to address them. By understanding the Factory and Abstract Factory patterns, you can design more flexible and extensible systems that adapt to changing requirements and support complex object creation scenarios.