.NET & C# Development · Lesson 31 of 229
Factory Method & Abstract Factory
Factory Method and Abstract Factory
Factory patterns decouple the creation of objects from their usage. Instead of new ConcreteClass(), you ask a factory for an instance — the factory decides what to create.
Why Factories?
C#
// Without factory — tightly coupled to concrete type
var notifier = new EmailNotifier(); // what if we need Slack? SMS? Push?
notifier.Send("Order shipped!");
// With factory — decoupled
var notifier = notifierFactory.Create(user.Preferences);
notifier.Send("Order shipped!"); // factory decides which typeFactory Method
A method (or class) that creates objects. Subclasses override it to change what gets created.
C#
public interface INotifier
{
void Send(string message, string recipient);
}
public class EmailNotifier : INotifier
{
public void Send(string message, string recipient)
=> Console.WriteLine($"Email to {recipient}: {message}");
}
public class SmsNotifier : INotifier
{
public void Send(string message, string recipient)
=> Console.WriteLine($"SMS to {recipient}: {message}");
}
public class SlackNotifier : INotifier
{
public void Send(string message, string recipient)
=> Console.WriteLine($"Slack to #{recipient}: {message}");
}
// Factory method — static factory
public static class NotifierFactory
{
public static INotifier Create(NotificationType type) => type switch
{
NotificationType.Email => new EmailNotifier(),
NotificationType.Sms => new SmsNotifier(),
NotificationType.Slack => new SlackNotifier(),
_ => throw new ArgumentException($"Unknown type: {type}")
};
}
// Usage
var notifier = NotifierFactory.Create(user.PreferredNotification);
notifier.Send("Your order has shipped!", user.Contact);Registration-Based Factory (Open/Closed)
The switch statement above requires modifying the factory to add new types. A registration-based factory is open for extension.
C#
public class NotifierFactory
{
private readonly Dictionary<NotificationType, Func<INotifier>> _registry = new();
public void Register(NotificationType type, Func<INotifier> creator)
=> _registry[type] = creator;
public INotifier Create(NotificationType type)
{
if (!_registry.TryGetValue(type, out var creator))
throw new KeyNotFoundException($"No notifier for {type}");
return creator();
}
}
// At startup — register all types
var factory = new NotifierFactory();
factory.Register(NotificationType.Email, () => new EmailNotifier());
factory.Register(NotificationType.Sms, () => new SmsNotifier());
// Adding a new type: just register it — no factory code changes!
factory.Register(NotificationType.Slack, () => new SlackNotifier());Factory with DI in ASP.NET Core
C#
// Register multiple implementations
builder.Services.AddScoped<EmailNotifier>();
builder.Services.AddScoped<SmsNotifier>();
builder.Services.AddScoped<SlackNotifier>();
// Factory that uses DI to create instances
builder.Services.AddScoped<INotifierFactory, NotifierFactory>();
public class NotifierFactory(IServiceProvider provider) : INotifierFactory
{
public INotifier Create(NotificationType type) => type switch
{
NotificationType.Email => provider.GetRequiredService<EmailNotifier>(),
NotificationType.Sms => provider.GetRequiredService<SmsNotifier>(),
NotificationType.Slack => provider.GetRequiredService<SlackNotifier>(),
_ => throw new ArgumentException($"Unknown type: {type}")
};
}Abstract Factory — Families of Objects
Abstract Factory creates families of related objects without specifying concrete types. Use it when objects must be compatible with each other.
C#
// Abstract factory — creates a family of UI components
public interface IUiFactory
{
IButton CreateButton();
ITextField CreateTextField();
IDialog CreateDialog();
}
// Concrete factory for each theme/platform
public class MaterialUiFactory : IUiFactory
{
public IButton CreateButton() => new MaterialButton();
public ITextField CreateTextField() => new MaterialTextField();
public IDialog CreateDialog() => new MaterialDialog();
}
public class WindowsUiFactory : IUiFactory
{
public IButton CreateButton() => new WindowsButton();
public ITextField CreateTextField() => new WindowsTextField();
public IDialog CreateDialog() => new WindowsDialog();
}
// Application uses the abstract factory — doesn't know which theme
public class FormBuilder(IUiFactory factory)
{
public Form Build()
{
var button = factory.CreateButton();
var textField = factory.CreateTextField();
return new Form(button, textField); // all components from the same family
}
}Real-World: Notification Strategy
C#
public class NotificationService(INotifierFactory factory)
{
public async Task NotifyAsync(User user, string message)
{
var notifier = factory.Create(user.PreferredChannel);
notifier.Send(message, user.ContactInfo);
// Also notify via secondary channel if configured
if (user.SecondaryChannel.HasValue)
{
var secondary = factory.Create(user.SecondaryChannel.Value);
secondary.Send(message, user.SecondaryContact!);
}
}
}Key Takeaways
- Factory Method encapsulates
new— callers get an instance without knowing the concrete type - Use a registration-based factory to follow Open/Closed — add new types without modifying the factory
- Abstract Factory ensures that a family of objects (all Material, all Windows) are compatible
- In ASP.NET Core, use
IServiceProviderin factories to get DI-resolved instances - The goal is decoupling — the caller shouldn't need to know what it's creating or how