new ist die direkteste Zeile in C#. Sie sagt: ich weiss genau, welche Klasse ich will, und ich weiss, wie man sie baut. Meistens stimmt beides.
Manchmal stimmt keines von beidem — und dann wird new zum Problem:
public void BestellungAbschliessen(Bestellung bestellung)
{
// ... speichern ...
var versender = new SmtpVersender(
host: "smtp.example.com",
port: 587,
benutzer: _config["Smtp:User"],
passwort: _config["Smtp:Password"],
timeout: TimeSpan.FromSeconds(30));
versender.Sende(bestellung.Email, "Danke für deine Bestellung");
}Drei Dinge sind hier schiefgegangen, und sie hängen zusammen.
Der Aufrufer kennt den Bauplan. BestellungAbschliessen weiss jetzt, dass E-Mails über SMTP gehen, welchen Port das braucht und wo die Zugangsdaten stehen. Das ist Wissen über eine ganz andere Ebene, als die Methode eigentlich hat.
Der Bauplan steht mehrfach da. Dieselben fünf Zeilen stehen auch im Passwort-Zurücksetzen, im Rechnungsversand und im Mahnlauf. Ändert sich der Timeout, änderst du ihn viermal — oder dreimal, und der vierte bleibt.
Der Test kann nichts dagegen tun. new SmtpVersender(...) steht fest im Methodenrumpf. Ein Test für BestellungAbschliessen schickt eine echte Mail oder läuft in einen Verbindungsfehler. Die Klasse hat keine Fuge, an der man ansetzen könnte.
Erzeugen und Benutzen sind zwei Aufgaben
Der gemeinsame Nenner: Eine Methode, deren Aufgabe «Bestellung abschliessen» ist, erledigt nebenbei die Aufgabe «einen Mailversender bauen». Alle Factory-Varianten machen dasselbe — sie trennen diese beiden Aufgaben voneinander.
Die einfachste Trennung ist keine Factory
Bevor du ein Pattern holst: In sehr vielen Fällen reicht es, das fertige Objekt hereinzureichen.
public sealed class BestellService
{
private readonly IVersender _versender;
public BestellService(IVersender versender) => _versender = versender;
public void BestellungAbschliessen(Bestellung bestellung)
{
// ... speichern ...
_versender.Sende(bestellung.Email, "Danke für deine Bestellung");
}
}Alle drei Probleme sind weg: kein Bauplan, keine Dopplung, ein Test kann eine Attrappe hereinreichen. Wer den SmtpVersender zusammensetzt, ist jetzt die DI-Registrierung in Program.cs — die Stelle, an der Zusammensetzen die eigentliche Aufgabe ist.
Constructor Injection löst den Normalfall. Eine Factory brauchst du erst, wenn sie ihn nicht löst. Und das hat einen konkreten Grund:
Der Fall für eine Factory
Wenn erst zur Laufzeit feststeht, welches Objekt es sein soll — weil die Entscheidung von einem Wert abhängt, den es beim Programmstart noch gar nicht gab. Der DI-Container baut beim Start; eine Factory baut, wenn die Bestellung schon da ist.
Konkret:
- Der Kunde hat «SMS» als Benachrichtigungskanal hinterlegt — das steht in der Datenbank, nicht in der Konfiguration.
- Pro Durchlauf einer Schleife braucht es ein frisches Objekt, kein geteiltes.
- Das Objekt braucht Konstruktor-Argumente, die aus den Daten kommen (eine Mandanten-ID, ein Dateipfad).
In diesen Fällen kannst du das fertige Objekt nicht hereinreichen — es gibt es beim Zusammenbauen noch nicht. Reichen kannst du nur die Fähigkeit, es zu bauen.