The Factory Method pattern in C#
Most code that is hard to test has the same tell: somewhere deep inside a method, a new keyword
creates a concrete type. The Factory Method pattern moves that decision out of the calling code and
into a place you control.
The problem
Say you send notifications, and the channel depends on the customer's settings:
public void Notify(Customer customer, string message)
{
if (customer.PrefersSms)
{
var sender = new SmsSender(_smsApiKey);
sender.Send(customer.PhoneNumber, message);
}
else
{
var sender = new EmailSender(_smtpHost);
sender.Send(customer.Email, message);
}
}This method now knows three things it should not: which channels exist, how each sender is constructed, and which credentials each one needs. Adding a third channel means editing this method. Testing it means having an SMTP host.
The pattern
Define the product interface, then let a factory decide which implementation to hand back:
public interface INotificationSender
{
Task SendAsync(Customer customer, string message);
}
public interface INotificationSenderFactory
{
INotificationSender Create(Customer customer);
}
public sealed class NotificationSenderFactory : INotificationSenderFactory
{
private readonly IOptions<NotificationOptions> _options;
public NotificationSenderFactory(IOptions<NotificationOptions> options) => _options = options;
public INotificationSender Create(Customer customer) =>
customer.PrefersSms
? new SmsSender(_options.Value.SmsApiKey)
: new EmailSender(_options.Value.SmtpHost);
}The calling code shrinks to something that no longer has an opinion about channels:
public Task NotifyAsync(Customer customer, string message) =>
_factory.Create(customer).SendAsync(customer, message);What you actually gained
The interesting part is the test. Before, you needed infrastructure. Now you need a stub:
[Fact]
public async Task Notify_uses_the_sender_the_factory_returns()
{
var sender = new Mock<INotificationSender>();
var factory = new Mock<INotificationSenderFactory>();
factory.Setup(f => f.Create(It.IsAny<Customer>())).Returns(sender.Object);
await new NotificationService(factory.Object).NotifyAsync(TestData.Customer, "hello");
sender.Verify(s => s.SendAsync(TestData.Customer, "hello"), Times.Once);
}When to skip it
A factory is a type, and every type has a cost. If you have exactly one implementation and no
plausible second one, new is fine — a factory that always returns the same class is just
indirection with extra steps. Reach for the pattern when the decision is real, not when you
suspect it might become real one day.