Das Factory-Method-Pattern in C#
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.