Stop Using new Everywhere
Every time you write new ConcreteClass() in your code, you’re creating a hard dependency. Your code becomes rigid, difficult to test, and nearly impossible to extend without breaking existing functionality. What if there was a better way?
The Problem: Tight Coupling and Inflexibility
Imagine you’re building a notification system. Initially, you only send emails:
public class NotificationService
{
public void SendNotification(string message)
{
var notification = new EmailNotification(); // Hard-coded dependency
notification.Send(message);
}
}
This seems fine until business requirements change. Now you need SMS notifications, push notifications, and Slack messages. Suddenly, your code looks like this:
public void SendNotification(string message, string type)
{
if (type == "email")
{
var notification = new EmailNotification();
notification.Send(message);
}
else if (type == "sms")
{
var notification = new SmsNotification();
notification.Send(message);
}
else if (type == "push")
{
var notification = new PushNotification();
notification.Send(message);
}
// This will grow forever...
}
Problems with this approach:
- Violates the Open/Closed Principle (open for extension, closed for modification)
- Every new notification type requires modifying existing code
- Impossible to unit test without sending actual notifications
- The service knows too much about concrete implementations
The Solution: Factory Method Pattern
The Factory Method pattern delegates object creation to subclasses or specialized factory methods, allowing you to create objects without specifying their exact classes.
Definition: Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.
Pseudocode Structure
Creator (abstract)
├── FactoryMethod() : Product (abstract)
└── SomeOperation() : uses FactoryMethod()
ConcreteCreatorA : Creator
└── FactoryMethod() : returns ConcreteProductA
ConcreteCreatorB : Creator
└── FactoryMethod() : returns ConcreteProductB
Product (interface)
└── Operation()
ConcreteProductA : Product
ConcreteProductB : Product
Real-World Example: Multi-Channel Notification System
Let’s refactor our notification system using the Factory Method pattern.
Step 1: Define the Product Interface
public interface INotification
{
Task SendAsync(string recipient, string message);
string GetChannelName();
}
Step 2: Implement Concrete Products
public class EmailNotification : INotification
{
public async Task SendAsync(string recipient, string message)
{
// Simulate email sending
await Task.Delay(100);
Console.WriteLine($"📧 Email sent to {recipient}: {message}");
}
public string GetChannelName() => "Email";
}
public class SmsNotification : INotification
{
public async Task SendAsync(string recipient, string message)
{
await Task.Delay(50);
Console.WriteLine($"📱 SMS sent to {recipient}: {message}");
}
public string GetChannelName() => "SMS";
}
public class SlackNotification : INotification
{
public async Task SendAsync(string recipient, string message)
{
await Task.Delay(75);
Console.WriteLine($"💬 Slack message sent to {recipient}: {message}");
}
public string GetChannelName() => "Slack";
}
Step 3: Create the Factory
public abstract class NotificationFactory
{
public abstract INotification CreateNotification();
public async Task NotifyAsync(string recipient, string message)
{
var notification = CreateNotification();
Console.WriteLine($"Using {notification.GetChannelName()} channel...");
await notification.SendAsync(recipient, message);
}
}
public class EmailNotificationFactory : NotificationFactory
{
public override INotification CreateNotification() => new EmailNotification();
}
public class SmsNotificationFactory : NotificationFactory
{
public override INotification CreateNotification() => new SmsNotification();
}
public class SlackNotificationFactory : NotificationFactory
{
public override INotification CreateNotification() => new SlackNotification();
}
Step 4: Use the Factory
public class Program
{
public static async Task Main(string[] args)
{
// Client code works with factories through the base class
NotificationFactory factory;
string userPreference = "email"; // Could come from database/config
factory = userPreference switch
{
"email" => new EmailNotificationFactory(),
"sms" => new SmsNotificationFactory(),
"slack" => new SlackNotificationFactory(),
_ => new EmailNotificationFactory()
};
await factory.NotifyAsync("user@example.com", "Your order has shipped!");
}
}
Advanced Example: Document Processor with .NET Core Features
Here’s a more sophisticated example using modern C# features:
public interface IDocumentProcessor
{
Task<string> ProcessAsync(Stream content);
}
public class PdfProcessor : IDocumentProcessor
{
public async Task<string> ProcessAsync(Stream content)
{
await Task.Delay(200);
return "PDF processed: extracted text content";
}
}
public class WordProcessor : IDocumentProcessor
{
public async Task<string> ProcessAsync(Stream content)
{
await Task.Delay(150);
return "Word document processed: converted to HTML";
}
}
public class ExcelProcessor : IDocumentProcessor
{
public async Task<string> ProcessAsync(Stream content)
{
await Task.Delay(180);
return "Excel processed: extracted data rows";
}
}
// Modern factory using static abstract interface members (C# 11/.NET 9)
public static class DocumentProcessorFactory
{
private static readonly Dictionary<string, Func<IDocumentProcessor>> _processors = new()
{
[".pdf"] = () => new PdfProcessor(),
[".docx"] = () => new WordProcessor(),
[".xlsx"] = () => new ExcelProcessor()
};
public static IDocumentProcessor? Create(string fileExtension)
{
return _processors.TryGetValue(fileExtension.ToLower(), out var factory)
? factory()
: null;
}
public static void RegisterProcessor(string extension, Func<IDocumentProcessor> factory)
{
_processors[extension.ToLower()] = factory;
}
}
// Usage
var processor = DocumentProcessorFactory.Create(".pdf");
if (processor != null)
{
var result = await processor.ProcessAsync(fileStream);
Console.WriteLine(result);
}
Enterprise Applications
1. Database Connection Factories
Create connections for different databases (SQL Server, PostgreSQL, MySQL) based on configuration without changing business logic.
2. Payment Gateway Integration
Support multiple payment providers (Stripe, PayPal, Square) through a common interface, switchable via configuration.
3. Logging Frameworks
Route logs to different destinations (file, database, cloud) based on environment or severity level.
4. Report Generators
Generate reports in various formats (PDF, Excel, CSV) using the same data source.
5. Authentication Providers
Support multiple authentication methods (JWT, OAuth, SAML) in a single application.
Benefits
- Loose Coupling: Client code depends on abstractions, not concrete classes
- Open/Closed Principle: Add new types without modifying existing code
- Single Responsibility: Object creation logic is separated from business logic
- Testability: Easy to mock factories in unit tests
When to Use
Use the Factory Method when:
- You don’t know ahead of time the exact types of objects your code will work with
- You want to provide a library of products and reveal only their interfaces
- You want to save system resources by reusing existing objects instead of rebuilding them
When NOT to Use
Avoid this pattern when:
- You have only one product type with no anticipated variations
- The creation logic is simple and unlikely to change
- The added abstraction creates unnecessary complexity
Key Takeaway: The Factory Method pattern transforms object creation from a rigid, hard-coded process into a flexible, extensible system. By programming to interfaces and delegating instantiation, you build software that adapts to change gracefully.

