Das Single-Responsibility-Prinzip einfach erklärt.

von Sergej Kukshaus vor 6 monaten

Das Single-Responsibility-Prinzip ist eigentlich sehr, sehr einfach und schnell erklärt.

Deswegen wundert es mich, warum ich nach mehreren Stunden Recherche keine guten Artikel dazu im internet finde.

Die Autoren haben gute Ansätze, ohne Zweifel. Jedoch ist die Umsetzung nur mittelmäßig.

Es werden Beispiele, komplett fernab von der Realität aufgelistet.

Aus diesem Grunde, habe ich mich dazu entschlossen „mal eben“ einen eigenen Beitrag darüber zu schreiben.

In meinen Augen ist das Single-Responsibility-Prinzip (SRP) eines der fundamentalsten Prinzipien der Softwarearchitektur.

Wenn man das nicht versteht oder nicht richtig anwenden kann, kann man gleich einpacken.

Software, die die SRP verletzt, ist zum Scheitern verurteilt. Die Frage ist dann nämlich nicht ob, sondern wann. Mit sehr viel Glück auch nie.

SRP – Definition

Uncle Bob hat das Prinzip eigentlich ganz klar definiert:

A class or module should have one, and only one, reason to be changed.

Mit anderen Worten: Eine Klasse sollte nur einen Grund haben, um Änderungen zuzulassen. Hierzu muss zwangsläufig die Aufgabe der Klasse klar definiert sein. Wenn eine Klasse mehrere Aufgaben hat, gibt es auch mehrere Gründe diese Klasse zu ändern und somit würde das gegen das Single Responsibility Principle verstoßen.

Soweit so unklar? 🙂

Was ist eine typische Verletzung von SRP?

Jeder von uns hat schon einmal dieses Prinzip verletzt.

Ich beispielsweise bin:

  • Softwareentwickler
  • Blogger / Autor
  • Ehemann
  • Vater
  • Bester Kumpel (von den Kiddies :-))
  • Gärtner
  • Tischler
  • KFZ-Mechaniker
  • Maurer
  • Maler
  • usw.

Ganz schön viele Aufgaben, oder? Und ich wette, dir geht es nicht viel anders. Wenn du mal alle deine Aufgaben auflistest, ist deine Liste wahrscheinlich genauso lang oder sogar noch länger.

Das Problem dabei ist jedoch, einige dieser Aufgaben kann ich nicht oder nicht sehr gut erledigen.

Wie kann ich diese Aufgaben trotzdem meistern?

Naja, ganz einfach:

  1. Delegieren
  2. Wissen aneignen uns selber machen

Ich denke mal, wir wissen jedoch alle, dass der zweite Punkt nicht immer der beste ist. Wenn ich versuchen würde mein Auto zu reparieren, würde ich Angst haben damit zu fahren 🙂

Kann man zu viel SRP betreiben?

Während meiner Suche nach guten Artikeln bin ich auf eine interessante Frage auf StackExchange gestoßen.

Der Fragesteller erklärt, dass sie eine Software warten und der Teamleiter verlangt, dass SOLID Prinzipien eingehalten werden.

Somit ist die Software unwartbar, weil die Klassen so klein sind und manche Konstruktoren bereits über 20 Parameter besitzen.

Ähm, bitte WAS?

Wie bitte schafft man es eine Aufgabe zu erledigen und dabei zwanzig Abhängigkeiten zu haben?

Für mich klingt das nach deutlich mehr Aufgaben als nur diese eine.

Allerdings gebe ich ihm in einer Sache recht.

Betreibt man SRP, bekommt man zwangsläufig viele kleine Klassen, die es zu handhaben gilt.

Wenn jedoch „mein“ Prinzip hinzugenommen wird, dann ist es auch total egal, ob viele Klassen da sind oder nicht.

Wenn ich mein Auto zu dem KFZ-Mechaniker meines Vertrauens bringe, dann interessiert es mich absolut nicht, welches Stahl der Hersteller seines Maulschlüssels verwendet hat und woher das bezogen wurde.

Eine Methode für eine Autoreparatur könnte in etwa so aussehen:

void AutoReparieren(Werkstatt werkstatt) 
{
	auto.InDieWerkstattBringen(werkstatt);
	while (auto.ReparatorAbgeschlossen() == false)
	{
		Warten()
	}
	auto.AusDerWerkstattAbholen();
}

Allein durch diese Methode haben wir eine sehr komplexe Aufgabe in gerade einmal 4 Zeilen Quellcode abgebildet. Diese 4 Zeilen versteht jeder. Sogar der Vertriebler oder der Chef, die noch nie in ihrem Leben programmiert haben.

Wie du Quellcode nach dem Single-Responsibility-Prinzip schreibst

Ich möchte an dieser Stelle die Definition von „Uncle Bob Martin“ wie folgt abändern:

Jedes System, sollte nur eine Aufgabe erfüllen.

Ein System kann im Grunde genommen alles sein. Sei es eine Anwendung, eine Klassenbibliothek, eine Klasse, eine Methode oder nur eine einzelne Anweisung.

Im Grunde genommen, ist diese Anforderung gegeben, wenn du die genaue Aufgabe eines Systems mit nur einem Satz genau beschreiben kannst, ohne die Wörter und oder oder zu verwenden.

Richtig: Benutzer validieren.
Falsch: Benutzerdaten erfassen und validieren und speichern oder Fehlermeldung anzeigen.

SRP: Ausgangslage

Schauen wir uns mal dieses einfache Beispiel an:

Console.WriteLine(" - BENUTZER ERSTELLEN -");

Console.Write("Vorname  : ");
string vorname = Console.ReadLine();
Console.Write("Nachname : ");
string nachname = Console.ReadLine();

if (string.IsNullOrWhiteSpace(vorname))
{
    Console.WriteLine("Der eingegebene Vorname ist nicht gültig.");
    Console.WriteLine("Anwendung mit einer beliebigen Taste beenden.");
    Console.Read();
    return;
}

if (string.IsNullOrWhiteSpace(nachname))
{
    Console.WriteLine("Der eingegebene Nachname ist nicht gültig.");
    Console.WriteLine("Anwendung mit einer beliebigen Taste beenden.");
    Console.Read();
    return;
}

string email = $"{vorname}.{nachname}@acme.com";

string gültigeZeichenFürPasswörter = "abcdefghijklmnopqrstuvwxyz0123456789";
string passwort = "";
var zufallszahlGenerator = new Random((int) DateTime.Now.Ticks);

for (int i = 0; i < 8; i++)
{
    int zufallszahl = zufallszahlGenerator.Next(gültigeZeichenFürPasswörter.Length);
    passwort += gültigeZeichenFürPasswörter[zufallszahl];
}

Console.WriteLine(" - NEUER BENUTZER WURDE ANGELEGT - ");

Console.WriteLine($"Name     : {nachname}, {vorname}");
Console.WriteLine($"E-Mail   : {email}");
Console.WriteLine($"Passwort : {passwort}");

Die Ausgabe könnte wie folgt aussehen:

 - BENUTZER ERSTELLEN -
Vorname  : Sergej
Nachname : Kukshaus
 - NEUER BENUTZER WURDE ANGELEGT -
Name     : Kukshaus, Sergej
E-Mail   : Sergej.Kukshaus@acme.com
Passwort : yo9pk60j

Das ist zwar nicht viel Quellcode, aber selbst hier schon bin ich nicht in der Lage einen passenden Namen zu vergeben, ohne die Bindewörter zu verwenden und trotzdem alles abzudecken.

Lösung nach SRP

Zunächst einmal müssen wir hier die Aufgabe klar identifizieren. Hier hilft uns wieder der Trick mit den Bindewörtern.

Schauen wir mal direkt in die erste Zeile.

Der Benutzer wird hier begrüßt. Danach kommt bereits die nächste Verantwortlichkeit. Also können wir bereits hier eine neue Methode ableiten:

void BenutzerBegruessen()
{
    Console.WriteLine(" - BENUTZER ERSTELLEN -");
}

In nächsten Schritt werden die Benutzerdaten abgefragt. Zunächst der Vorname, dann der Nachname:

string VornameAbfragen()
{
    Console.Write("Vorname  : ");
    return Console.ReadLine();
}

string NachnameAbfragen()
{
    Console.Write("Nachname : ");
    return Console.ReadLine();
}

Anschließend wird geprüft ob die Eingabe gültig ist:

static bool IstDerVornameGueltig(string vorname)
{
    if (string.IsNullOrWhiteSpace(vorname))
    {
        Console.WriteLine("Der eingegebene Vorname ist nicht gültig.");
        return false;
    }

    return true;
}

Und so weiter. Ich glaube, du hast das Prinzip verstanden 🙂

Die überarbeitete Version könnte anschließend wie folgt aussehen:

static void Main(string[] args)
{
    BenutzerBegruessen();
    Benutzer benutzer = NeuenBenutzerAbfragen();

    if (SindDieBenutzerdatenGueltig(benutzer) == false)
    {
        AnwendungBeenden();
    }

    NeuenBenutzerSpeichern(benutzer);
    AnwendungBeenden();
}

Ich habe mir noch eine kleine Klasse „Benutzer“ spendiert um alle Benutzerinformationen zusammenzufassen.

Und wie könnten wir dieses System sinnvoll benennen?

Der passendste Name wäre doch eigentlich:

BenutzerdatenAbfragenUndValidierenUndSpeichern()

Das würde jedoch gegen die Regeln verstoßen. Stattdessen müssen wir uns ein Begriff überlegen, welches das alles zusammenfasst:

BenutzerAnlegen()

Kleine Anmerkung dazu:

Streng genommen, gehören die Aufgaben „BenutzerBegrüßen“ und „AnwendungBeenden“ nicht zu den Aufgaben dieses Systems und sind nur quasi ein Teil der „Architektur“. Bei der Validierung müsste eine Ausnahme ausgelöst werden.

Und somit erhalten wir diesen Quellcode:

void BenutzerAnlegen()
{
    Benutzer benutzer = BenutzerdatenAbfragen();

    if (SindDieBenutzerdatenGueltig(benutzer) == false)
    {
        throw new UngueltigeBenutzerdatenException();
    }

    NeuenBenutzerSpeichern(benutzer);
}

Die komplette Überarbeitung findest du bei github unter:
https://github.com/ByteBee/solid/blob/master/SRP/Program.cs

Die 5 Vorteile von SRP

Strickte Trennung der Verantwortlichkeiten

Das Grundprinzip hinter SRP ist es, die Verantwortlichkeiten komplett zu trennen. Eine Klasse oder auch eine Methode darf nicht mehr als eine Aufgabe erfüllen.

Der Logger darf z. B. nicht auch noch gleichzeitig eine Datenbankverbindung aufbauen, um da die Daten zu schreiben. Das wären in der Tat zwei Verantwortlichkeiten.

Warum? Weil nicht jeder Logger in eine Datenbank schreiben kann.

Quellcode wird verständlicher.

Vergleiche mal den oberen Code vor und nach der Optimierung. Aus dem ersten Beispiel geht nicht sofort hervor, was passiert. Es wird viel zu viel drumherum gemacht.

Ähnlich ist es auch bei einem Gespräch. Man redet viel zu viel, geht zu tief ins Detail ohne auf den Punkt zu kommen.

Nach der Optimierung sieht man sofort was Sache ist. Wenn man diese Details möchte, dann kann man in die Definition der jeweiligen Methoden gehen und eben da nachsehen, was genau passiert.

Hat man diese Ebenfalls nach SRP geschrieben, wird man auch hier wiederum kleine Methoden sehen, die ebenfalls „auf den Punkt kommen“.

Globale Änderung müssen nur einmal umgesetzt werden.

Wie oft ist es dir schon mal passiert, dass der Kunde nur eine kleine Änderung wünscht und du 294 Stellen im Code anpassen musstest?

Mir passiert sowas ständig. Leider.

Durch SRP kann man gezielt an einer Stelle Änderungen durchführen und es wirkt sich global auf das ganze System aus.

Im Umkehrschluss heißt es auch, wenn man hier ein Fehler einbaut, wird natürlich auch das ganze System darunter leiden und.

Hohe Kohäsion / Geringe Kopplung

Kohäsion und Kopplung sind Gegensätze. Man kann nie eine geringe Kohäsion und gleichzeitig eine geringe bzw. lose Kopplung haben.

In einem System, mit geringer Kohäsion gibt es viele Abhängigkeiten, ohne eine Möglichkeit diese auszutauschen.

Schon allein, wenn man im Quellcode direkt auf das Dateisystem zugreift, hat man ein wenig an Kohäsion verloren. Wir machen uns vom Dateisystem abhängig.

Der Code wird – streng genommen – untestbar.

Hat man hingegen eine hohe Kohäsion und damit eine lose Kopplung, werden alle Zugriffe auf das Dateisystem in einer dafür vorgesehenen Klasse ausgelagert.

Anstelle vom Dateisystem, kann man jetzt die Implementierung austauschen und bei Bedarf (z.B. Unit-Testing) etwas anderes verwenden.

Die Nachteile von SRP

Viele werden SRP vor, unübersichtlich zu sein, weil man viele kleine Klassen hat.

Aus meiner Sicht ist genau das gegenteil der Fall.

Bei einer vernünftigen Struktur wird der Quellcode übersichtlicher. Besser noch. Man muss noch nicht einmal den Quellcode ansehen um zu erahnen, wo etwas stehen könnte.

Als weiterer Nachteil wird genannt, dass Details verschleiert werden.

Siehe das Beispiel von oben mit dem Maulschlüssel vom KFZ-Mechaniker. Ob es jetzt ein Billig-Schlüssel vom Discounter oder ein hochwertiger Gedore, Würth, Hazet, oder was auch immer ist, ist mir total wurscht. Hauptsache diese gottverdammte Schraube ist ab und wieder dran.

Fazit

Als guter Entwickler, sollte man nicht nur mal etwas von dem Single Responsibility Principle gehört haben, sondern sehr gut kennen und auch anwenden können.

Wenn du das machst, bekommst du automatisch guten und lesbaren Quellcode.

Ein guter Quellcode muss langweilig sein.

  • Man muss den lesen und sofort verstehen könne, worum es sich handelt.
  • Man darf nicht draufschauen und denken: „WTF?“

Und wenn du strikt nach SRP arbeitest, dann wirst du solch einen Quellcode auch erreichen. Versprochen 🙂

Sei der erste und teile deine Meinung mit der Welt!
... und was meinst du dazu?
Deine E-Mail-Adresse wird nicht veröffentlicht.