Single Responsibility Principle

SOLID 

Die SOLID Design Principles sind Praktiken, die helfen den Code zu strukturieren, damit dieser verständlich, anpassungsfähig und wartbar bleibt, auch wenn das Projekt wächst.  
SOLID steht für die Anfangsbuchstaben der 5 Prinzipien.  

Die Prinzipien sind: 

  • Single Responsibility Principle 
  • Open-Closed Principle 
  • Liskov Substitution Principle 
  • Interface Segregation Principle 
  • Dependency Inversion Principle 

In meiner Serie möchte ich die einzelnen Prinzipien vorstellen und erklären. 

Single Responsibility Principle 

Softwareentwickler kennen das Problem. Neue Anforderungen kommen rein, sie werden umgesetzt und kurz danach ändern sie sich schon wieder. Dies geschieht in vielen Projekten schnell hintereinander, so dass der Code oft umgebaut und angepasst wird. Klassen wachsen dadurch schnell an und bekommen immer mehr Verantwortung. Dieser Effekt tritt auch ein, wenn sich die Anforderungen nur langsam ändern, die Codebasis aber nicht aufgeräumt wird. Teile des Quelltextes werden zwar immer wieder gelöscht, jedoch nur um neue, komplexere Anforderungen mit mehr Codezeilen umzusetzen.

Nach einiger Zeit ist es nur noch schwer nachzuvollziehen, was der Code überhaupt macht und wofür die einzelnen Klassen genau verantwortlich sind. Neue Kollegen lässt man am Besten gar nicht an die entsprechenden Quelltexte, denn auch für die alteingesessenen Entwickler sind sie schwer zu verstehen und dadurch natürlich auch schwer zu erklären.

Was hilft? 

Diesen Effekten soll das Single Responsibility Principle entgegenwirken. Es besagt, dass jede Klasse immer nur genau eine Aufgabe erfüllen soll. Das führt zu kleineren Klassen, deren Aufgaben man leichter überblicken kann. Die kleineren Klassen lassen sich auch leichter Testen und haben weniger Abhängigkeiten. Denn weniger Funktionalität führt automatisch zu weniger Abhängigkeit.

Beispiel 

Schauen wir folgendes, an der Praxis angelehntes Beispiel an.Wir wollen eine vorhandene Code Basis überarbeiten, damit sie das Single Responsibility Prinzip erfüllt.

Wir haben ein Programm, das unter anderem, Termine und Aufgaben verwaltet. Was die Applikation genau macht und sonst noch alles kann, ist hier nicht weiter wichtig.

In dem Klassendiagramm erkennen wir schon, dass der zentrale EventAndTaskService für die Verwaltung von Terminen und Aufgaben verantwortlich ist. Er verwaltet beide Datentypen, denn sie werden aufgrund ihrer Ähnlichkeit in derselben Datenstruktur abgespeichert.

Der Service lädt, speichert und erzeugt die entsprechenden Objekte und validiert diese. Außerdem fungiert er als Cache, denn er hält alle Objekte nach dem ersten Laden im Speicher, um Ladezeiten zu verringern.

Da der Druck verschiedene Vorbedingungen hat, muss dieser Befehl ebenfalls den Service passieren.

Angezeigt werden die Termine im CalendarView und die Aufgaben im ToDoView. Zusätzlich gibt es noch ein Dashboard zur Anzeige beider Typen. In dem CalendarView kann außerdem ein Druck für einen Termin gestartet werden. Dafür wird der PrintService verwendet.

Des weiteren lassen sich Aufgaben über die TaskImport Klasse in das Programm importieren. Und in der ContactManagement Klasse werden Termine für einen Kontakt angelegt.

Klassendiagram vor dem Umbau

Das ist sehr viel Verantwortung für nur eine einzige Klasse.  

Zeit, diese aufzuräumen. 

Zuerst bietet sich an, diese Klasse in zwei Services aufzuteilen, einen EventService und einen TaskService. Auch wenn beide Klassen auf Datenebene sehr ähnlich sind, werden sehr wahrscheinlich unterschiedliche Businesslogiken benötigt. Damit nutzt der CalendarView und das ContactManagement nur noch den EventService.  Und der TaskService wird lediglich vom TaskView und dem TaskImport verwendet. Das Dashboard benötigt Zugriff auf beide Services. 

Der CalendarView kann den PrintService direkt aufrufen und den entsprechenden Termin weitergeben. Die Logiken für das Drucken können dann entweder das Event Objekt oder den PrintService verschoben werden.

Dadurch haben sich die Abhängigkeiten schon etwas entschärft. Nun können die Aufgaben der beiden Services noch aufgeteilt werden. Das Laden und Speichern kann in eine eigene Repository-Klasse wandern. Das Caching lässt sich ebenfalls heraustrennen  indem man es als Adapter vor das Repository setzt. Der Cache kümmert sich dann selbstständig um das Aktualisieren der Daten und löst auch das Speichern im Repository aus. Hierfür muss er jedoch den Event erst noch validieren. Dies passiert ebenfalls in einer eigenen Validator Klasse.

Nur nicht übertreiben 

Das beschriebene Vorgehen ist nur ein Beispiel. Wie genau die Klassen geschnitten und aufgeteilt werden hängt von den Anforderungen, dem Team und auch der Infrastruktur ab. Es gibt kein einheitliches Rezept, dem man folgen kann.

Hier wird aber schon deutlich, dass das neue Klassendiagramm deutlich komplexer ist als das alte. Zwar sind die Verantwortungen aufgeteilt, aber es gibt viel mehr Elemente und Abhängigkeiten.  

Diese Abhängigkeiten können eine versteckte Gefahr darstellen, falls man es mit dem Single Responsibility Principle übertreibt. Denn in diesem Fall kann sich die Komplexität der Klassen auf die Abhängigkeiten übertragen. So wird es wieder schwerer den Überblick zu behalten, wodurch sich neue Fehlerquellen einschleichen. 

Das Single Responsibility Principle ist nicht nur auf Klassen anwendbar. Es kann ebenfalls auf die einzelnen Module im Programm oder auch auf die Methoden in einer Klasse angewendet werden. Jedes Modul und jede Methode sollte dann genau eine Anforderung erfüllen.