Module 2 · 2. Creational Patterns · Lesson 4 of 12
Factory Method and Abstract Factory
Learning outcome
By the end of this lesson you will be able to: (1) explain the intent and forces behind Factory Method and Abstract Factory, (2) pick the right pattern for runtime-vs-deployment configuration, and (3) evaluate testability trade-offs and common misuses in production C# code.
Intuition
Factory Method: give a class a protected/virtual creation method that subclasses override to provide a concrete product. Use it when a class (often a framework-type base class) needs to defer creation to subclasses so the base class can remain generic.
Abstract Factory: provide an interface that creates a family of related products. Use it when a component needs a set of collaborating objects and you want to swap entire families at deployment time (via DI or configuration).
Small code sketch showing shape (not the runnable example):
Deep dive
Forces and consequences:
- Who chooses the concrete type, and when? Factory Method pushes the decision to subclasses (compile-time decision by which subclass you instantiate). Abstract Factory centralizes family selection and is normally chosen at startup and injected throughout the app.
- Testability: Abstract Factory is easier to replace with test doubles because you depend on an interface (IFamilyFactory) and can register a fake at test startup. Classic Factory Method implementations that rely on inheritance can be harder to mock unless you refactor the creator into an injectable factory interface.
- Deployment configuration: Abstract Factory maps naturally to configuration-driven composition: decide the concrete factory once (from environment or config) and register it with the DI container. Factory Method is a better fit when behavior must vary by subclass identity or when the framework itself expects subclassing.
Trade-offs:
- Abstract Factory is more verbose and can be overkill when you only need to vary a single product.
- Factory Method is simpler in small frameworks but less flexible for run-time switching.
Failure modes
- Overusing Abstract Factory to wrap unrelated products (leads to pointless indirection). If products are unrelated, prefer simple factory delegates or direct construction.
- Using Factory Method when a delegate or simple injected Func<T> would be simpler and easier to test.
- Tight coupling through inheritance: subclass explosion and testing pain when behavior is controlled by many small concrete creators.
- Hidden dependencies: factories that read global state or configuration at creation time make behavior non-deterministic and hard to unit-test.
Interview drill
Short answers to practice: 1) When is Factory Method preferable to Abstract Factory? (Answer: when a single class needs to defer creation to subclasses; subclassing keeps creation close to the behavior.) 2) How does Abstract Factory improve deployment-time configuration? (Answer: choose a concrete factory at startup and register it once with DI; all consumers receive the family.) 3) How would you make a Factory Method design more testable? (Answer: extract the virtual creation into an injectable factory interface or accept a Func<T> delegate.)
Micro-tests you can ask in an interview:
- Given a class that creates two collaborating objects, ask the candidate to design an Abstract Factory interface and explain how to register it with Microsoft.Extensions.DependencyInjection.
- Ask to refactor a class using protected virtual CreateX() into one that accepts an IFactory in its constructor and explain testing benefits.
Exercises (with brief answers):
- Exercise: You have a processor that needs a parser and a renderer. Which pattern and why? Answer: Abstract Factory — parser+renderer are a family that should be swapped together.
- Exercise: You have a base class that wants to create a single helper instance that derived classes must customize. Which pattern? Answer: Factory Method (or accept an injected helper in constructor if you prefer composition over inheritance).
Revision checklist
- Can the creation decision be made once at startup? If yes, prefer Abstract Factory + DI.
- Do products form a family that must be used together? Abstract Factory likely.
- Is the creator already a base class intended for extension? Factory Method may be appropriate.
- Can you replace subclassing with an injected delegate or interface without losing clarity? Prefer the injectable option for testability.
Production code
When wiring up Abstract Factory in a real .NET service, choose the concrete factory at startup and register it with DI so tests can replace the registration with a fake factory. Example registration sketch:
Why this is preferable to simpler alternatives: if you need to swap multiple collaborating objects consistently across the app (e.g., loggers, formatters, transport) you avoid scattered if/else branches and duplicated wiring.
Misuse warning: if only a single product type varies and the decision can be injected with a Func<T> or simple factory delegate, don't introduce Abstract Factory; it adds boilerplate.
Code walkthrough
Below is a compact runnable demo that includes both patterns, shows how Abstract Factory is swapped at startup, and demonstrates a fake factory used for testing. The program prints deterministic lines and includes explicit notes about why each pattern is preferable in certain cases and common misuse warnings. The runnable example is in the attached Program.cs file (in the codeExamples). Walk the file and observe:
- Factory Method section: Creator class with protected override CreateLogger(); shows subclassing-based creation.
- Abstract Factory section: ILoggingFactory produces both formatter and a logger that depends on that formatter; a JsonLoggingFactory and a SimpleLoggingFactory demonstrate family selection.
- Testability: FakeLoggingFactory shows how Abstract Factory can be replaced by a deterministic test double.
Review the example: it demonstrates the patterns, why one is preferable to a simpler alternative in each case, and prints clear, deterministic output so you can reason about behavior in interviews and code reviews.
Executable code examples
FactoryMethod vs AbstractFactory Demo
Program.cs
using System;
namespace PatternDemos
{
// Common product
interface ILogger { string Log(string message); }
// ----------------- Factory Method -----------------
// Creator with virtual/abstract factory method
abstract class LoggerCreator
{
protected abstract ILogger CreateLogger();
// Base-class behaviour uses the product created by subclass
public string LogInfo(string message)
{
var logger = CreateLogger();
return logger.Log("INFO: " + message);
}
}
class ConsoleLogger : ILogger
{
public string Log(string message) => $"ConsoleLogger: {message}";
}
class TestLogger : ILogger
{
public string Log(string message) => $"TestLogger: {message}";
}
// Concrete creators override creation
class ConsoleLoggerCreator : LoggerCreator
{
protected override ILogger CreateLogger() => new ConsoleLogger();
}
class TestLoggerCreator : LoggerCreator
{
protected override ILogger CreateLogger() => new TestLogger();
}
// ----------------- Abstract Factory -----------------
// Family factory that produces related products
interface IFormatter { string Format(string level, string message); }
interface ILoggingFactory { ILogger CreateLogger(); IFormatter CreateFormatter(); }
class SimpleFormatter : IFormatter
{
public string Format(string level, string message) => $"[{level}] {message}";
}
class JsonFormatter : IFormatter
{
public string Format(string level, string message) => $"{{\"level\":\"{level}\",\"message\":\"{message}\"}}";
}
// Logger that depends on a formatter (a collaborating product)
class FormattedLogger : ILogger
{
private readonly IFormatter _formatter;
public FormattedLogger(IFormatter formatter) => _formatter = formatter;
public string Log(string message) => $"FormattedLogger: {_formatter.Format("INFO", message)}";
}
// Two concrete factories representing two families
class SimpleLoggingFactory : ILoggingFactory
{
public ILogger CreateLogger() => new FormattedLogger(new SimpleFormatter());
public IFormatter CreateFormatter() => new SimpleFormatter();
}
class JsonLoggingFactory : ILoggingFactory
{
public ILogger CreateLogger() => new FormattedLogger(new JsonFormatter());
public IFormatter CreateFormatter() => new JsonFormatter();
}
// Fake factory useful for deterministic tests
class FakeLoggingFactory : ILoggingFactory
{
public ILogger CreateLogger() => new FakeLogger();
public IFormatter CreateFormatter() => new SimpleFormatter();
}
class FakeLogger : ILogger
{
public string Log(string message) => "FakeLogger: injected fake";
}
class Program
{
static void Main()
{
// Factory Method demo: creation decision is encoded by which subclass you instantiate
Console.WriteLine("Factory Method demo (subclassing-based creation):");
var creator1 = new ConsoleLoggerCreator();
Console.WriteLine(creator1.LogInfo("factory method example"));
Console.WriteLine("Factory Method demo (alternative subclass):");
var creator2 = new TestLoggerCreator();
Console.WriteLine(creator2.LogInfo("factory method example"));
Console.WriteLine();
// Abstract Factory demo: choose the family at startup and inject the factory
Console.WriteLine("Abstract Factory demo (family injection at startup):");
ILoggingFactory factory = new JsonLoggingFactory(); // chosen at composition time
var logger = factory.CreateLogger();
Console.WriteLine(logger.Log("abstract factory example"));
Console.WriteLine();
// Testability demonstration: replace with a fake factory in tests
Console.WriteLine("Abstract Factory testability check using fake factory:");
var fake = new FakeLoggingFactory();
var fakeLogger = fake.CreateLogger();
Console.WriteLine(fakeLogger.Log("ignored"));
Console.WriteLine();
// Misuse warnings printed explicitly so reviewers can see them in deterministic output
Console.WriteLine("Misuse warning:");
Console.WriteLine("- Factory Method is overkill when a simple constructor or delegate suffices.");
Console.WriteLine("- Abstract Factory is overkill when product families are unrelated or static.");
Console.WriteLine();
Console.WriteLine("End of demo.");
}
}
}