Hook
Catchy Title Options
The Hook
Explain how sprawling new keywords scattered across a codebase create tight coupling, fragile architecture, and unit testing headaches.
Objective: Show how encapsulating instantiation logic makes your C# applications extensible, maintainable, and aligned with SOLID principles.
The Problem: Direct Instantiation & Tight Coupling
Scenario
A notification system sending alerts via Email, SMS, or Push.
Code Example (The Anti-Pattern)
Show client code directly instantiating new EmailService(), new SmsService(), etc., inside conditional branches.
Why it Breaks Down
Violates Open/Closed Principle (OCP): Adding a new channel forces modification of caller logic.
Dependency Rigidity: Hardcoding class names makes swapping implementations or mocking for unit tests difficult.
Leaky Abstractions: Configuration parameters (API keys, SMTP settings) leak into calling code.
Core Mechanics of the Factory Pattern
Plain-English Definition
A creational pattern that provides an interface for creating objects in a superclass, allowing subclasses to alter the type of objects that will be created.
Real-World Analogy
A custom furniture workshop. You order a "Chair"—you don't build it yourself; the workshop handles raw materials, assembly, and quality checks, delivering a finished IChair.
Real-World Scenario: Notification Delivery System
In an application, you often need to send notifications via different channels (Email, SMS, Push Notification). Using new EmailService() directly across your application tightly couples code to specific implementations and makes it difficult to add new notification channels.
The Factory Pattern delegates object creation to specialized subclass factories, encapsulating instantiation logic.
1. Product Interface & Concrete Products
Defines the contract for notification channels and implements specific delivery mechanisms.
// Product Interface
public interface INotification
{
Task SendAsync(string recipient, string message);
}
// Concrete Product A: Email
public class EmailNotification : INotification
{
public async Task SendAsync(string recipient, string message)
{
await Task.Delay(50); // Simulate SMTP API call
Console.WriteLine($"[Email] Sent to {recipient}: {message}");
}
}
// Concrete Product B: SMS
public class SmsNotification : INotification
{
public async Task SendAsync(string recipient, string message)
{
await Task.Delay(50); // Simulate Twilio API call
Console.WriteLine($"[SMS] Sent to {recipient}: {message}");
}
}
// Concrete Product C: Push
public class PushNotification : INotification
{
public async Task SendAsync(string recipient, string message)
{
await Task.Delay(50); // Simulate Firebase API call
Console.WriteLine($"[Push] Sent to device {recipient}: {message}");
}
}
2. Creator (Factory Base Class) & Concrete Creators
The creator class declares the factory method that returns new product instances.
// Abstract Creator
public abstract class NotificationFactory
{
// The Factory Method
public abstract INotification CreateNotification();
// Core business logic relies on the product interface, not concrete classes
public async Task PublishNotificationAsync(string recipient, string message)
{
INotification notification = CreateNotification();
await notification.SendAsync(recipient, message);
}
}
// Concrete Creator A
public class EmailNotificationFactory : NotificationFactory
{
public override INotification CreateNotification()
{
// Encapsulate any initialization logic here (e.g., loading SMTP configs)
return new EmailNotification();
}
}
// Concrete Creator B
public class SmsNotificationFactory : NotificationFactory
{
public override INotification CreateNotification()
{
return new SmsNotification();
}
}
// Concrete Creator C
public class PushNotificationFactory : NotificationFactory
{
public override INotification CreateNotification()
{
return new PushNotification();
}
}
3. Client Usage
class Program
{
static async Task Main()
{
// Instantiating factory dynamically based on runtime condition or configuration
NotificationFactory factory = GetFactoryByChannel("SMS");
await factory.PublishNotificationAsync("+1234567890", "Your OTP code is 482910");
}
private static NotificationFactory GetFactoryByChannel(string channelType)
{
return channelType.ToUpper() switch
{
"EMAIL" => new EmailNotificationFactory(),
"SMS" => new SmsNotificationFactory(),
"PUSH" => new PushNotificationFactory(),
_ => throw new ArgumentException("Unsupported notification channel")
};
}
}
Trade-Offs & Best Practices
Pros
Cons
Best Practices
Key Takeaways & Conclusion
Summary
The Factory Pattern isolates how objects are created from how they are used.
Rule of Thumb
If object creation requires complex setup or dynamic decision-making at runtime, delegate it to a Factory.
Summary
The Factory Pattern encapsulates object creation behind a common interface, reducing coupling between client code and concrete implementations. By centralizing instantiation logic, it becomes easier to extend applications with new object types, improve testability, and keep initialization concerns separate from business logic. This approach is especially useful when object creation involves complex setup or runtime selection of implementations.