Eine Klasse ist so testbar und so austauschbar, wie ihre Abhängigkeiten sichtbar sind. Und «sichtbar» heisst: im Konstruktor.
Dieselbe Klasse, zwei Bauformen
Versteckt
public class Mahnlauf
{
public void FuehreAus()
{
var db = new SqlConnection(
Konfiguration.Instanz
.Verbindung);
var heute = DateTime.Now;
SmtpVersender.Instanz.Sende(…);
}
}Sichtbar
public class Mahnlauf
{
private readonly IRechnungen _rechnungen;
private readonly IUhr _uhr;
private readonly IVersender _versender;
public Mahnlauf(
IRechnungen rechnungen,
IUhr uhr,
IVersender versender)
{ … }
public void FuehreAus() { … }
}Links behauptet der Konstruktor, die Klasse brauche nichts. Rechts steht in der Signatur: Datenbank, Zeit, Mailversand. Und alle drei lassen sich im Test ersetzen.
Der linke Fall hat drei versteckte Abhängigkeiten, und die dritte ist die tückischste: DateTime.Now. Sie sieht aus wie ein harmloser Wert, ist aber ein Zugriff auf globalen, ständig veränderlichen Zustand.
// Nicht testbar: Das Ergebnis hängt davon ab, wann der Test läuft.
if (rechnung.Faelligkeit < DateTime.Now.AddDays(-30))
Mahne(rechnung);`TimeProvider` — seit .NET 8 eingebaut
Du musst dafür keine eigene IUhr mehr erfinden. .NET 8 bringt TimeProvider mit; TimeProvider.System in der Anwendung, FakeTimeProvider aus Microsoft.Extensions.TimeProvider.Testing im Test — inklusive Zeitsprüngen für Timer.
public sealed class Mahnlauf
{
private readonly TimeProvider _zeit;
public Mahnlauf(TimeProvider zeit) => _zeit = zeit;
public bool IstUeberfaellig(Rechnung rechnung) =>
rechnung.Faelligkeit < _zeit.GetUtcNow().AddDays(-Mahnfrist);
}Dasselbe gilt für alles andere Unberechenbare: Guid.NewGuid(), Random, Environment.MachineName, Dateisystem, Umgebungsvariablen. Jeder dieser Zugriffe im Inneren einer Klasse ist eine Abhängigkeit, die ihre Signatur verschweigt.
Das Gesetz von Demeter
Die zweite Art versteckter Kopplung sind Zugriffsketten:
var strasse = bestellung.Kunde.Adresse.Strasse.ToUpper();Diese Zeile hängt von vier Typen ab: Bestellung, Kunde, Adresse und string. Ändert sich irgendeiner davon — und sei es nur, dass Adresse optional wird — bricht sie. Sie ist ausserdem eine NullReferenceException an drei Stellen gleichzeitig.
Das Gesetz von Demeter formuliert die Regel als: Rede nur mit deinen unmittelbaren Nachbarn. Praktisch:
Fragen und selber machen
if (bestellung.Kunde.Adresse.Land
== "CH")
{
var porto = 7.00m;
…
}Sagen, was gebraucht wird
var porto = bestellung.BerechnePorto();
// in Bestellung:
public decimal BerechnePorto() =>
_kunde.IstImInland
? InlandPorto
: AuslandPorto;«Tell, don't ask»: Statt einem Objekt seine Daten zu entlocken und selbst zu entscheiden, sag ihm, was du brauchst. Die Entscheidung wandert dorthin, wo die Daten liegen.
Die Ausnahme, die oft übersehen wird
Für reine Datenstrukturen — DTOs, Konfigurationsobjekte, Ergebnisse einer Abfrage — gilt das Gesetz von Demeter nicht. konfiguration.Smtp.Port ist völlig in Ordnung: Da ist kein Verhalten, das man verletzen könnte. Die Regel gilt für Objekte, die etwas tun.