How to Implement Abstract Factory Pattern in C#: Step-by-Step Guide
Implementing the Abstract Factory pattern in C# requires understanding its structure and following a systematic approach. This step-by-step guide will walk you through implementing Abstract Factory from scratch, with complete code examples and best practices for C# development.
Whether you're building a UI framework, payment system, or any application requiring compatible object families, this guide provides the practical knowledge you need to implement Abstract Factory correctly.
This article focuses on: Step-by-step implementation mechanics and C# implementation details. This is the canonical implementation guide for the cluster—other articles reference this for detailed code structure. For conceptual foundation, see Abstract Factory Design Pattern: Complete Guide. For decision-making guidance, see When to Use Abstract Factory Pattern. For advanced practices, see Abstract Factory Pattern Best Practices.
For conceptual understanding and working examples, explore The Big List of Design Patterns.
Step 1: Identify Your Product Families
Proper planning is essential for a successful Abstract Factory implementation. Before writing any code, clearly define your product families and their relationships.
Before writing code, identify:
- What product types do you need? (e.g., Button, Dialog, Menu)
- What families/variants exist? (e.g., Windows, macOS, Linux)
- Do products need to be compatible? (Yes → Abstract Factory is appropriate)
Example: UI Component System
Product Types: Button, Dialog, Menu
Families: Windows, macOS
Compatibility: Yes - all UI elements must match the platform
Step 2: Define Abstract Product Interfaces
The foundation of Abstract Factory is defining abstract product interfaces. These interfaces represent the contract that all products must follow, regardless of which family they belong to. Create interfaces for each product type that define what all products can do, regardless of family:
// Step 2: Abstract Products
public interface IButton
{
void Render();
void Click();
string GetStyle();
}
public interface IDialog
{
void Show();
void Close();
string GetStyle();
}
public interface IMenu
{
void Display();
void SelectItem(int index);
string GetStyle();
}
Key Points:
- Interfaces define the contract, not implementation
- All products in a family implement the same interface
- Methods should be relevant to the product type
Step 3: Implement Concrete Products
Concrete products implement the abstract product interfaces. Each product provides family-specific behavior while adhering to the interface contract, ensuring they can be used polymorphically.
Create concrete classes for each product in each family:
// Step 3: Concrete Products - Windows Family
public class WindowsButton : IButton
{
public void Render()
{
Console.WriteLine("Rendering Windows-style button");
}
public void Click()
{
Console.WriteLine("Windows button clicked");
}
public string GetStyle() => "Windows";
}
public class WindowsDialog : IDialog
{
public void Show()
{
Console.WriteLine("Showing Windows-style dialog");
}
public void Close()
{
Console.WriteLine("Closing Windows dialog");
}
public string GetStyle() => "Windows";
}
public class WindowsMenu : IMenu
{
public void Display()
{
Console.WriteLine("Displaying Windows-style menu");
}
public void SelectItem(int index)
{
Console.WriteLine($"Windows menu item {index} selected");
}
public string GetStyle() => "Windows";
}
// Step 3: Concrete Products - macOS Family
public class MacButton : IButton
{
public void Render() => Console.WriteLine("Rendering macOS-style button");
public void Click() => Console.WriteLine("macOS button clicked");
public string GetStyle() => "macOS";
}
public class MacDialog : IDialog
{
public void Show() => Console.WriteLine("Showing macOS-style dialog");
public void Close() => Console.WriteLine("Closing macOS dialog");
public string GetStyle() => "macOS";
}
public class MacMenu : IMenu
{
public void Display() => Console.WriteLine("Displaying macOS-style menu");
public void SelectItem(int index) => Console.WriteLine($"macOS menu item {index} selected");
public string GetStyle() => "macOS";
}
Key Points:
- Each concrete product implements its interface
- Products from the same family share characteristics (style, behavior)
- Keep implementations focused and simple
Step 4: Define Abstract Factory Interface
The abstract factory interface is the central component that ties everything together. It declares methods for creating all product types, ensuring that any concrete factory can produce a complete set of compatible products. Create an interface that declares methods for creating all product types:
// Step 4: Abstract Factory
public interface IUIFactory
{
IButton CreateButton();
IDialog CreateDialog();
IMenu CreateMenu();
}
Key Points:
- One method per product type
- Methods return abstract product interfaces
- Factory doesn't know about concrete classes
Step 5: Implement Concrete Factories
Concrete factories implement the abstract factory interface. Each factory is responsible for creating products from one specific family, ensuring all products are compatible with each other.
Create concrete factory classes, one for each family:
// Step 5: Concrete Factories
public class WindowsUIFactory : IUIFactory
{
public IButton CreateButton() => new WindowsButton();
public IDialog CreateDialog() => new WindowsDialog();
public IMenu CreateMenu() => new WindowsMenu();
}
public class MacUIFactory : IUIFactory
{
public IButton CreateButton() => new MacButton();
public IDialog CreateDialog() => new MacDialog();
public IMenu CreateMenu() => new MacMenu();
}
Key Points:
- Each factory creates products from one family
- Factories are stateless (just object creators)
- All factories implement the same interface
Step 6: Implement Client Code
The client code demonstrates how to use the factory to create products. By depending on the abstract factory interface, the client code remains decoupled from specific implementations and can work with any factory. This is where you'll see the benefits of Abstract Factory in action—the client code doesn't need to know which specific factory it's using, only that it can create compatible products. Create client code that uses the factory:
// Step 6: Client Code
public class Application
{
private readonly IUIFactory _factory;
public Application(IUIFactory factory)
{
_factory = factory;
}
public void BuildUI()
{
// All UI components guaranteed to be from same family
var button = _factory.CreateButton();
var dialog = _factory.CreateDialog();
var menu = _factory.CreateMenu();
Console.WriteLine($"Button style: {button.GetStyle()}");
Console.WriteLine($"Dialog style: {dialog.GetStyle()}");
Console.WriteLine($"Menu style: {menu.GetStyle()}");
// All styles will match!
}
public void DemonstrateUI()
{
var button = _factory.CreateButton();
var dialog = _factory.CreateDialog();
var menu = _factory.CreateMenu();
button.Render();
dialog.Show();
menu.Display();
}
}
// Usage
class Program
{
static void Main(string[] args)
{
// Windows UI
var windowsFactory = new WindowsUIFactory();
var windowsApp = new Application(windowsFactory);
windowsApp.BuildUI();
// macOS UI
var macFactory = new MacUIFactory();
var macApp = new Application(macFactory);
macApp.BuildUI();
}
}
Complete Implementation Example
Here's the complete, runnable implementation that brings together all the steps we've covered. This example demonstrates a fully functional Abstract Factory implementation that you can compile and run:
using System;
namespace AbstractFactoryImplementation
{
// Step 1 & 2: Abstract Products
public interface IButton
{
void Render();
string GetStyle();
}
public interface IDialog
{
void Show();
string GetStyle();
}
// Step 3: Concrete Products - Windows
public class WindowsButton : IButton
{
public void Render() => Console.WriteLine("Windows button");
public string GetStyle() => "Windows";
}
public class WindowsDialog : IDialog
{
public void Show() => Console.WriteLine("Windows dialog");
public string GetStyle() => "Windows";
}
// Step 3: Concrete Products - macOS
public class MacButton : IButton
{
public void Render() => Console.WriteLine("macOS button");
public string GetStyle() => "macOS";
}
public class MacDialog : IDialog
{
public void Show() => Console.WriteLine("macOS dialog");
public string GetStyle() => "macOS";
}
// Step 4: Abstract Factory
public interface IUIFactory
{
IButton CreateButton();
IDialog CreateDialog();
}
// Step 5: Concrete Factories
public class WindowsUIFactory : IUIFactory
{
public IButton CreateButton() => new WindowsButton();
public IDialog CreateDialog() => new WindowsDialog();
}
public class MacUIFactory : IUIFactory
{
public IButton CreateButton() => new MacButton();
public IDialog CreateDialog() => new MacDialog();
}
// Step 6: Client
public class App
{
private readonly IUIFactory _factory;
public App(IUIFactory factory) => _factory = factory;
public void Run()
{
var button = _factory.CreateButton();
var dialog = _factory.CreateDialog();
Console.WriteLine($"Button: {button.GetStyle()}, Dialog: {dialog.GetStyle()}");
}
}
class Program
{
static void Main(string[] args)
{
new App(new WindowsUIFactory()).Run();
new App(new MacUIFactory()).Run();
}
}
}
Best Practices for C# Implementation
Following C# best practices ensures your Abstract Factory implementation is maintainable, follows conventions, and integrates well with the .NET ecosystem.
1. Use Interfaces, Not Abstract Classes
In C#, interfaces provide maximum flexibility and are the preferred approach for Abstract Factory. They allow multiple inheritance and don't lock you into a specific inheritance hierarchy. Prefer interfaces for flexibility:
// Good: Interface-based
public interface IUIFactory
{
IButton CreateButton();
}
// Avoid: Abstract class unless you need shared implementation
public abstract class UIFactory
{
public abstract IButton CreateButton();
}
2. Keep Factories Stateless
Factories should be simple object creators without internal state. This makes them thread-safe, easy to test, and predictable. Factories should only create objects, not store state:
// Good: Stateless factory
public class WindowsUIFactory : IUIFactory
{
public IButton CreateButton() => new WindowsButton();
}
// Bad: Stateful factory
public class WindowsUIFactory : IUIFactory
{
private int _buttonCount; // Don't do this!
public IButton CreateButton() => new WindowsButton();
}
3. Use Dependency Injection
Integrate with DI containers:
// Registration
services.AddScoped<IUIFactory, WindowsUIFactory>();
// Or with factory selector
services.AddScoped<IUIFactory>(sp =>
{
var platform = Environment.OSVersion.Platform;
return platform == PlatformID.Win32NT
? new WindowsUIFactory()
: new MacUIFactory();
});
// Usage
public class MyService
{
public MyService(IUIFactory factory) { }
}
4. Implement Factory Selector Pattern
When you need to choose a factory at runtime based on configuration or user input, a factory selector pattern provides a clean solution. For runtime factory selection:
public class UIFactorySelector
{
public static IUIFactory GetFactory(string platform)
{
return platform.ToLower() switch
{
"windows" => new WindowsUIFactory(),
"mac" => new MacUIFactory(),
"linux" => new LinuxUIFactory(),
_ => throw new ArgumentException($"Unknown platform: {platform}")
};
}
}
5. Use Modern C# Features
Modern C# provides many features that make Abstract Factory implementations more concise and readable. Features like expression-bodied members, pattern matching, and nullable reference types can simplify your factory code while maintaining clarity. Leverage C# language features:
// Expression-bodied members
public class WindowsUIFactory : IUIFactory
{
public IButton CreateButton() => new WindowsButton();
public IDialog CreateDialog() => new WindowsDialog();
}
// Pattern matching in factory selection
public static IUIFactory CreateFactory(Platform platform) => platform switch
{
Platform.Windows => new WindowsUIFactory(),
Platform.Mac => new MacUIFactory(),
Platform.Linux => new LinuxUIFactory(),
_ => throw new ArgumentException()
};
Common Implementation Mistakes
Understanding common mistakes helps you avoid them. These errors can make Abstract Factory implementations harder to maintain and less effective.
Mistake 1: Mixing Product Types
One common mistake is grouping unrelated products in the same factory. Abstract Factory requires products to be related and form a cohesive family.
// Bad: Unrelated products
public interface IBadFactory
{
IButton CreateButton();
ILogger CreateLogger(); // Not related to UI!
}
Fix: Only group related products that form a family.
Mistake 2: Factory Methods Returning Concrete Types
Factory methods should always return abstract interfaces, not concrete types. Returning concrete types defeats the purpose of the pattern and creates tight coupling:
// Bad: Returns concrete type
public interface IUIFactory
{
WindowsButton CreateButton(); // Should return IButton
}
Fix: Always return abstract interfaces:
// Good: Returns interface
public interface IUIFactory
{
IButton CreateButton();
}
Mistake 3: Adding Logic to Factories
// Bad: Factory with business logic
public class WindowsUIFactory : IUIFactory
{
public IButton CreateButton()
{
if (someCondition) // Don't do this!
return new SpecialButton();
return new WindowsButton();
}
}
Fix: Keep factories simple - they just create objects:
// Good: Simple creation
public class WindowsUIFactory : IUIFactory
{
public IButton CreateButton() => new WindowsButton();
}
Mistake 4: Not Handling Factory Selection
Hard-coding factory selection makes your code inflexible. Always provide a mechanism for selecting factories at runtime, whether through configuration, dependency injection, or a factory selector:
// Bad: Hard-coded factory
var factory = new WindowsUIFactory(); // Always Windows!
// Good: Configurable selection
var factory = UIFactorySelector.GetFactory(config.Platform);
Advanced Implementation: Factory Registry
A factory registry pattern provides a more sophisticated approach to factory management, allowing you to register and retrieve factories dynamically. This is useful for plugin architectures or when factories need to be discovered at runtime. For dynamic factory management:
public class FactoryRegistry
{
private readonly Dictionary<string, IUIFactory> _factories;
public FactoryRegistry()
{
_factories = new Dictionary<string, IUIFactory>
{
{ "windows", new WindowsUIFactory() },
{ "mac", new MacUIFactory() },
{ "linux", new LinuxUIFactory() }
};
}
public IUIFactory GetFactory(string key)
{
return _factories.TryGetValue(key.ToLower(), out var factory)
? factory
: throw new KeyNotFoundException($"Factory not found: {key}");
}
public void RegisterFactory(string key, IUIFactory factory)
{
_factories[key.ToLower()] = factory;
}
public IEnumerable<string> GetAvailableFactories()
{
return _factories.Keys;
}
}
Testing Your Implementation
Testing Abstract Factory implementations involves verifying that factories create the correct products and that all products from a factory belong to the same family. Both unit tests and integration tests are important.
Unit Tests
Unit tests verify that each factory creates products of the correct type and that products from the same factory are compatible. These tests ensure the factory pattern works correctly.
[Fact]
public void WindowsFactory_CreatesWindowsProducts()
{
// Arrange
var factory = new WindowsUIFactory();
// Act
var button = factory.CreateButton();
var dialog = factory.CreateDialog();
// Assert
Assert.IsType<WindowsButton>(button);
Assert.IsType<WindowsDialog>(dialog);
Assert.Equal("Windows", button.GetStyle());
Assert.Equal("Windows", dialog.GetStyle());
}
[Fact]
public void AllProductsFromFactory_MatchStyle()
{
// Arrange
var factory = new MacUIFactory();
// Act
var button = factory.CreateButton();
var dialog = factory.CreateDialog();
// Assert
Assert.Equal(button.GetStyle(), dialog.GetStyle());
}
Integration Tests
Integration tests verify that the factory works correctly within the context of your application:
[Fact]
public void Application_UsesFactoryCorrectly()
{
// Arrange
var factory = new WindowsUIFactory();
var app = new Application(factory);
// Act & Assert
app.BuildUI(); // Should not throw
}
Adding New Product Types
Adding new product types requires updating interfaces and implementations across all families. This process demonstrates how Abstract Factory maintains consistency while allowing extension.
To add a new product type (e.g., IMenu):
- Add interface:
public interface IMenu
{
void Display();
}
- Implement for each family:
public class WindowsMenu : IMenu { /* ... */ }
public class MacMenu : IMenu { /* ... */ }
- Add method to factory interface:
public interface IUIFactory
{
IButton CreateButton();
IDialog CreateDialog();
IMenu CreateMenu(); // New
}
- Implement in all factories:
public class WindowsUIFactory : IUIFactory
{
// ... existing methods
public IMenu CreateMenu() => new WindowsMenu();
}
Adding New Families
Adding new families is even easier than adding product types because it doesn't require modifying existing code. This demonstrates the Open/Closed Principle in action. To add a new family (e.g., Linux):
- Create products:
public class LinuxButton : IButton { /* ... */ }
public class LinuxDialog : IDialog { /* ... */ }
- Create factory:
public class LinuxUIFactory : IUIFactory
{
public IButton CreateButton() => new LinuxButton();
public IDialog CreateDialog() => new LinuxDialog();
}
- Update factory selector (if used):
"linux" => new LinuxUIFactory()
No changes needed to existing code!
Performance Considerations
Abstract Factory's performance impact is typically negligible in most applications. Understanding the performance characteristics helps you make informed decisions:
Abstract Factory has minimal performance impact:
- Creation overhead: One extra method call per object
- Memory: No additional memory beyond objects created
- CPU: Negligible - pattern adds indirection, not computation
Verdict: Performance is not a concern. Choose Abstract Factory based on design needs, not performance.
Integration with .NET Patterns
Abstract Factory integrates seamlessly with common .NET patterns like dependency injection and configuration. These integrations make factories more flexible and easier to manage.
With Dependency Injection
Dependency injection containers can resolve factories based on configuration, making it easy to switch between factory implementations at runtime.
// In Program.cs or Startup.cs
builder.Services.AddScoped<IUIFactory>(sp =>
{
var config = sp.GetRequiredService<IConfiguration>();
var platform = config["Platform"];
return UIFactorySelector.GetFactory(platform);
});
With Configuration
You can configure factory selection through application settings, making it easy to change behavior without recompiling:
public class UISettings
{
public string Platform { get; set; }
}
// In appsettings.json
{
"UISettings": {
"Platform": "Windows"
}
}
Conclusion
Implementing Abstract Factory in C# follows a clear pattern:
- Identify product families and types
- Define abstract product interfaces
- Implement concrete products for each family
- Create abstract factory interface
- Implement concrete factories
- Use factories in client code
The pattern provides:
- ✅ Guaranteed object compatibility
- ✅ Easy extensibility
- ✅ Clean, testable code
- ✅ Reduced conditional logic
Remember to keep factories simple, use interfaces, leverage dependency injection, and test thoroughly. With these practices, Abstract Factory becomes a powerful tool for managing object families in C#.
For more design pattern content, explore The Big List of Design Patterns for comprehensive coverage.
Frequently Asked Questions
Do I need to implement all product types immediately?
No. Start with the product types you need, then add more as requirements evolve. Abstract Factory makes adding new products straightforward—just extend the interface and implement in all factories.
Can factories have different numbers of products?
Technically yes, but it's not recommended. Abstract Factory works best when all factories create the same product types. If product counts vary significantly, reconsider if Abstract Factory is the right pattern.
How do I handle factory creation failures?
Factories typically don't fail—they just create objects. If object creation can fail, handle it in the product constructors or use a TryCreate pattern. Factories themselves should be simple and reliable.
Should factories be singletons?
Usually no. Factories are stateless object creators—create them as needed or register with DI. Only use Singleton if you have a specific need (e.g., expensive factory initialization).
Can I use Abstract Factory with async/await?
Yes, but factories typically create objects synchronously. If product creation is async, you might need async factory methods, though this is uncommon. Most Abstract Factory implementations are synchronous.
How do I version factories when products change?
Version the factory interface. Create IUIFactoryV2 with new methods, implement in factories, and migrate clients gradually. Or use optional methods with default implementations if supported.
Is Abstract Factory thread-safe?
Factories themselves are typically stateless and thread-safe. The products they create may or may not be thread-safe—that's a separate concern. If factories have state, make them thread-safe or use thread-local storage.
