Coding Best Practices  

Demystifying the Factory Pattern in Modern C#

Hook

Catchy Title Options

  • Clean Object Creation in C#: Mastering the Factory Pattern

  • Stop Using new Everywhere: Decouple Your C# Code with Factory Pattern

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

  • Decoupling

  • Centralizing object creation

  • Easier testing via mocks

Cons

  • Introduces extra classes/files

  • Can lead to over-engineering if direct instantiation is sufficient

Best Practices

  • Combine with Dependency Injection rather than building static service locators.

  • Keep initialization logic isolated inside the factory.

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.