Das Single Responsibility Principle wird meist zitiert als «eine Klasse soll eine Aufgabe haben». Das ist unbrauchbar, weil «eine Aufgabe» beliebig gross oder klein definiert werden kann — «Bestellungen verwalten» ist auch eine Aufgabe.
Robert C. Martins schärfere Fassung ist die nützliche:
Eine Klasse soll genau einen Grund haben, sich zu ändern.
Und weil Änderungen von Menschen angestossen werden, lautet die praktische Übersetzung: Eine Klasse soll genau einer Gruppe von Auftraggebern gegenüber verantwortlich sein.
public class Mitarbeiter
{
public decimal BerechneLohn() { … } // ändert die Buchhaltung
public void SpeichereInDb() { … } // ändert die Datenbank-Abteilung
public string AlsBerichtszeile() { … } // ändert das Controlling
}Drei Methoden, drei völlig verschiedene Auftraggeber. Wenn die Buchhaltung eine neue Lohnkomponente einführt, wird eine Datei geändert, in der auch die Berichtsformatierung des Controllings steht. Zwei Abteilungen teilen sich eine Datei — und damit einen Merge-Konflikt, ein Deployment und ein Testrisiko.
Der Ausweg sind drei Typen mit je einem Auftraggeber: Lohnrechner, MitarbeiterRepository, MitarbeiterBericht — und Mitarbeiter selbst als Datenträger, den alle drei benutzen.
Warum das keine Theorie ist
Die messbare Folge ist die Änderungsrate pro Datei. Eine Klasse, die in jedem zweiten Commit auftaucht, ist fast immer eine, die mehrere Gründe zur Änderung hat. git log --format=format: --name-only | sort | uniq -c | sort -rn zeigt sie dir in zehn Sekunden.
Kohäsion — der messbare Teil
Es gibt ein Signal, das ohne Interpretation auskommt: Benutzt jede Methode die meisten Felder?
public class BestellVerwaltung
{
private readonly IDbConnection _db; // von A, B
private readonly ISmtpClient _smtp; // nur von C
private readonly ILogger _logger; // von A, B, C
private readonly IPreisTabelle _preise; // nur von B
private readonly IPdfWriter _pdf; // nur von D
public Bestellung Lade(int id) { … } // A: _db, _logger
public decimal Berechne(Bestellung b) { … } // B: _db, _logger, _preise
public void SendeBestaetigung(Bestellung b) { … } // C: _smtp, _logger
public byte[] ErzeugeRechnung(Bestellung b) { … } // D: _pdf
}Kein Feld wird von allen Methoden benutzt, und ErzeugeRechnung teilt sich mit dem Rest gar nichts. Das ist keine Klasse — das sind vier Klassen, die zufällig dieselbe Datei bewohnen.
Die Bruchlinien sind sichtbar: Jede Gruppe aus Feldern und den Methoden, die sie benutzen, ist ein Kandidat für einen eigenen Typ.
Der Konstruktor als Warnlampe
Die schnellste Prüfung im Alltag: Wie viele Parameter hat der Konstruktor? Ab etwa fünf Abhängigkeiten ist die Wahrscheinlichkeit hoch, dass die Klasse mehrere Aufgaben hat — und die Zahl steht direkt am Anfang der Datei, ohne dass man etwas messen muss.
Und die Gegenrichtung
Es gibt auch zu viel des Guten. Wenn jede Klasse genau eine Methode hat und ein Ablauf durch elf Typen wandert, hat niemand etwas gewonnen — die Komplexität ist nur von innerhalb der Klassen nach zwischen die Klassen gewandert, wo sie schwerer zu sehen ist.
Ein brauchbarer Massstab: Kannst du in einem Satz sagen, wofür die Klasse zuständig ist, ohne «und»? Dann passt sie. Ob sie dabei 30 oder 300 Zeilen hat, ist zweitrangig.