Zurück zum Blog

Das Factory-Method-Pattern in C#

2 Min. Lesezeit

Schwer testbarer Code hat meistens dasselbe Merkmal: irgendwo tief in einer Methode erzeugt ein new einen konkreten Typ. Das Factory-Method-Pattern verschiebt diese Entscheidung aus dem aufrufenden Code an eine Stelle, die du kontrollierst.

Das Problem

Angenommen, du verschickst Benachrichtigungen, und der Kanal hängt von den Kundeneinstellungen ab:

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);
    }
}

Die Methode weiß jetzt drei Dinge, die sie nichts angehen: welche Kanäle existieren, wie jeder Sender konstruiert wird und welche Zugangsdaten er braucht. Ein dritter Kanal bedeutet, genau diese Methode zu ändern. Und zum Testen brauchst du einen SMTP-Host.

Das Pattern

Zuerst das Produkt-Interface, dann eine Factory, die die Implementierung auswählt:

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);
}

Der aufrufende Code schrumpft auf etwas zusammen, das keine Meinung mehr zu Kanälen hat:

public Task NotifyAsync(Customer customer, string message) =>
    _factory.Create(customer).SendAsync(customer, message);

Was du wirklich gewonnen hast

Der interessante Teil ist der Test. Vorher brauchtest du Infrastruktur, jetzt genügt ein Stub:

[Fact]
public async Task Notify_verwendet_den_Sender_der_Factory()
{
    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, "hallo");
 
    sender.Verify(s => s.SendAsync(TestData.Customer, "hallo"), Times.Once);
}

Wann du darauf verzichten solltest

Eine Factory ist ein Typ, und jeder Typ kostet. Wenn es genau eine Implementierung gibt und keine plausible zweite, ist new völlig in Ordnung — eine Factory, die immer dieselbe Klasse zurückgibt, ist nur Umweg mit Zusatzschritten. Greif zum Pattern, wenn die Entscheidung echt ist, nicht wenn du vermutest, sie könnte eines Tages echt werden.