Factory Method

Factory Method

A restaurant kitchen has one menu, but different stations prepare the same dish their own way — the Italian station makes pasta, the French station makes a soufflé. The customer orders "the special" without knowing which station handles it. The ordering process is the same; the creation varies by station.

The Factory Method pattern works the same way: it defines an interface for creating an object but lets subclasses decide which class to instantiate. A creator class declares an abstract or virtual factory method that returns a product interface. Each concrete creator overrides this method to produce its specific product. The client code calls the factory method through the creator interface, never touching new directly — so adding a new product type means adding a new creator subclass, not editing existing code.

flowchart LR
    Client -->|calls| Creator
    Creator -->|declares| FactoryMethod["factory method"]
    subgraph Concrete Creators
        EmailCreator["EmailNotificationCreator"]
        SmsCreator["SmsNotificationCreator"]
        PushCreator["PushNotificationCreator"]
    end
    FactoryMethod -.->|overridden by| EmailCreator
    FactoryMethod -.->|overridden by| SmsCreator
    FactoryMethod -.->|overridden by| PushCreator
    EmailCreator -->|creates| EmailNotification
    SmsCreator -->|creates| SmsNotification
    PushCreator -->|creates| PushNotification
Factory Method vs Abstract Factory

Factory Method creates one product via inheritance — the subclass decides. Abstract Factory creates a family of related products via composition. If you only need one object type, Factory Method is simpler.

Problem

An OrderService needs to send notifications after order events. The naive approach hardcodes channel creation inline:

public class OrderService
{
    public async Task PlaceOrderAsync(Order order)
    {
        await SaveOrderAsync(order);

        // ⚠️ Switch on type — every new channel requires editing this method
        var channel = order.Customer.PreferredChannel;
        if (channel == "email")
        {
            var emailSender = new SmtpEmailSender("smtp.example.com", 587); // ⚠️ hardcoded config
            await emailSender.SendAsync(order.Customer.Email,
                "Order Confirmed", BuildEmailBody(order));
        }
        else if (channel == "sms")
        {
            var smsSender = new TwilioSmsSender(Environment.GetEnvironmentVariable("TWILIO_SID")!);
            await smsSender.SendAsync(order.Customer.Phone, BuildSmsBody(order));
        }
        else if (channel == "push")
        {
            var pushSender = new FirebasePushSender(Environment.GetEnvironmentVariable("FCM_KEY")!);
            await pushSender.SendAsync(order.Customer.DeviceToken, "Order Confirmed", BuildPushBody(order));
        }
        // ⚠️ Adding Slack, webhook, or WhatsApp means editing this method again
    }
}

Here's what breaks when requirements change: adding a Slack notification for B2B customers requires editing OrderService, touching production code that already works, and risking regressions in email/SMS paths.

Solution

Extract notification creation into a factory method. Each channel gets its own creator:

// Product interface
public interface INotificationSender
{
    Task SendOrderConfirmationAsync(Order order);
}

// Concrete products
public class EmailNotificationSender : INotificationSender
{
    private readonly SmtpEmailSender _smtp;
    public EmailNotificationSender(SmtpEmailSender smtp) => _smtp = smtp;

    public Task SendOrderConfirmationAsync(Order order) =>
        _smtp.SendAsync(order.Customer.Email, "Order Confirmed", BuildBody(order));

    private static string BuildBody(Order order) =>
        $"Hi {order.Customer.Name}, your order #{order.Id} for {order.Total:C} is confirmed.";
}

public class SmsNotificationSender : INotificationSender
{
    private readonly TwilioSmsSender _twilio;
    public SmsNotificationSender(TwilioSmsSender twilio) => _twilio = twilio;

    public Task SendOrderConfirmationAsync(Order order) =>
        _twilio.SendAsync(order.Customer.Phone,
            $"Order #{order.Id} confirmed. Total: {order.Total:C}");
}

public class SlackNotificationSender : INotificationSender // ✅ new channel = new class, zero edits elsewhere
{
    private readonly SlackClient _slack;
    public SlackNotificationSender(SlackClient slack) => _slack = slack;

    public Task SendOrderConfirmationAsync(Order order) =>
        _slack.PostAsync(order.Customer.SlackUserId,
            $":white_check_mark: Order #{order.Id} placed — {order.Total:C}");
}

// Creator — declares the factory method
public abstract class NotificationCreator
{
    public abstract INotificationSender CreateSender(); // ✅ factory method

    public async Task NotifyOrderConfirmedAsync(Order order)
    {
        var sender = CreateSender(); // ✅ creator doesn't know the concrete type
        await sender.SendOrderConfirmationAsync(order);
    }
}

// Concrete creators
public class EmailNotificationCreator(SmtpEmailSender smtp) : NotificationCreator
{
    public override INotificationSender CreateSender() => new EmailNotificationSender(smtp);
}

public class SmsNotificationCreator(TwilioSmsSender twilio) : NotificationCreator
{
    public override INotificationSender CreateSender() => new SmsNotificationSender(twilio);
}

// OrderService now depends on the abstraction
public class OrderService(NotificationCreator notificationCreator)
{
    public async Task PlaceOrderAsync(Order order)
    {
        await SaveOrderAsync(order);
        await notificationCreator.NotifyOrderConfirmedAsync(order); // ✅ no switch, no channel knowledge
    }
}

Adding a Slack channel now means a new SlackNotificationCreator class — zero changes to OrderService or any existing creator.

You Already Use This

ILoggerFactory.CreateLogger<T>()ILoggerFactory is the creator; CreateLogger<T>() is the factory method. The concrete factory (LoggerFactory) decides which ILogger implementation to return based on registered providers (Console, Serilog, Application Insights).

DbProviderFactoryADO.NET's abstract factory method base. SqlClientFactory.Instance.CreateConnection() returns a SqlConnection; NpgsqlFactory.Instance.CreateConnection() returns a NpgsqlConnection. The caller works against DbConnection without knowing the provider.

Task.FromResult<T>() — a factory method that creates a completed Task<T> without allocating a state machine. The static method decides the concrete Task subtype based on the value.

Questions

References


Whats next